avatar_kit 1.0.0-beta.13
avatar_kit: ^1.0.0-beta.13 copied to clipboard
A Flutter plugin for AvatarKit which provides avatar rendering with audio driving support.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart' hide ConnectionState, Transform;
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:avatar_kit/avatar_kit_plugin.dart';
// see: https://docs.spatialreal.ai/overview/test-avatars
final String avatarID = '';
final int sampleRate = 16000;
void main() {
runApp(const AvatarKitApp());
}
class AvatarKitApp extends StatefulWidget {
const AvatarKitApp({super.key});
@override
State<AvatarKitApp> createState() => _AvatarKitAppState();
}
class _AvatarKitAppState extends State<AvatarKitApp> {
String version = '';
bool supportsCurrentDevice = false;
bool isInitializing = false;
@override
void initState() {
super.initState();
_initialize();
}
void _initialize() async {
setState(() {
isInitializing = true;
});
final version = await AvatarSDK.version();
final supportsCurrentDevice = await AvatarSDK.supportsCurrentDevice();
setState(() {
this.version = version;
this.supportsCurrentDevice = supportsCurrentDevice;
isInitializing = false;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(
version: version,
supportsCurrentDevice: supportsCurrentDevice,
isInitializing: isInitializing,
),
);
}
}
class HomePage extends StatelessWidget {
final String version;
final bool supportsCurrentDevice;
final bool isInitializing;
const HomePage({
super.key,
required this.version,
required this.supportsCurrentDevice,
required this.isInitializing,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Example')),
body: SafeArea(
child: Center(
child: isInitializing
? const CircularProgressIndicator()
: Column(
spacing: 6,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(),
Text('Select Driving Service Mode'),
ElevatedButton(
onPressed: () {
_onSelectDrivingServiceMode(
context,
DrivingServiceMode.sdk,
);
},
child: const Text('SDK'),
),
ElevatedButton(
onPressed: () {
_onSelectDrivingServiceMode(
context,
DrivingServiceMode.host,
);
},
child: const Text('Host'),
),
Spacer(),
Text(
'AvatarKit Version $version',
style: TextStyle(color: Colors.grey),
),
Text(
'Supports Current Device ${supportsCurrentDevice ? '✅' : '❌'}',
style: TextStyle(color: Colors.grey),
),
],
),
),
),
);
}
void _onSelectDrivingServiceMode(
BuildContext context,
DrivingServiceMode drivingServiceMode,
) async {
await AvatarSDK.initialize(
appID: 'yourAppID',
configuration: Configuration(
environment: Environment.intl,
audioFormat: AudioFormat(sampleRate: sampleRate),
drivingServiceMode: drivingServiceMode,
logLevel: LogLevel.all,
),
);
if (drivingServiceMode == DrivingServiceMode.sdk) {
await AvatarSDK.setSessionToken('');
}
await AvatarSDK.setUserID('');
if (context.mounted) {
if (drivingServiceMode == DrivingServiceMode.sdk) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SDKDrivingServiceModePage()),
);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => HostDrivingServiceModePage()),
);
}
}
}
}
class SDKDrivingServiceModePage extends StatefulWidget {
const SDKDrivingServiceModePage({super.key});
@override
State<SDKDrivingServiceModePage> createState() =>
_SDKDrivingServiceModePageState();
}
class _SDKDrivingServiceModePageState extends State<SDKDrivingServiceModePage> {
bool _isLoading = false;
ConnectionState _connectionState = ConnectionState.disconnected;
ConversationState _conversationState = ConversationState.idle;
Avatar? _avatar;
AvatarController? _avatarController;
void _loadAvatar() async {
try {
setState(() {
_isLoading = true;
});
// Mock: load from cache
// Avatar avatar = await AvatarManager.shared.derive('assets/avatar'); // Compatible: assets/avatar/
Avatar? avatar = await AvatarManager.shared.retrieve(id: avatarID);
if (avatar != null) {
debugPrint('retrieve cached avatar.');
} else {
avatar = await AvatarManager.shared.load(
id: avatarID,
onProgress: (progress) {
debugPrint('load avatar progress: $progress');
},
);
debugPrint('load avatar succeeded');
}
setState(() {
_avatar = avatar;
});
} on AvatarError catch (e) {
debugPrint('load avatar error: ${e.name}');
} catch (e) {
debugPrint('load avatar failed: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
void _onPlatformViewCreated(AvatarController controller) async {
controller.onFirstRendering = () {
debugPrint('onFirstRendering');
};
controller.onConnectionState = (state, errorMessage) {
debugPrint(
'onConnectionState: ${state.name}. ${errorMessage != null ? 'error: $errorMessage' : ''}',
);
setState(() {
_connectionState = state;
});
};
controller.onConversationState = (state) {
debugPrint('onConversationState: ${state.name}');
setState(() {
_conversationState = state;
});
};
controller.onError = (error) {
debugPrint('onError: ${error.name}');
};
await controller.setVolume(1.0);
final pointCount = await controller.pointCount();
debugPrint('pointCount: $pointCount');
await controller.setOpaque(false);
await controller.setContentTransform(Transform.identity);
setState(() {
_avatarController = controller;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('SDK Driving Service Mode')),
body: SafeArea(
child: Center(
child: _isLoading
? const CircularProgressIndicator()
: _avatar == null
? ElevatedButton(
onPressed: _loadAvatar,
child: const Text('Load Avatar'),
)
: _buildAvatar(),
),
),
),
);
}
Widget _buildAvatar() {
if (_avatar == null) {
return const CircularProgressIndicator();
}
return Column(
spacing: 6,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: 260,
height: 260,
child: AvatarWidget(
avatar: _avatar!,
onPlatformViewCreated: _onPlatformViewCreated,
),
),
),
ElevatedButton(
onPressed: () {
_avatarController?.start();
},
child: const Text('Start'),
),
ElevatedButton(
onPressed: () async {
final audioData = await rootBundle
.load('assets/audio_01.pcm')
.then((value) => value.buffer.asUint8List());
final conversationID = await _avatarController?.send(
audioData,
end: false,
);
debugPrint('send audio 01, conversationID: $conversationID');
},
child: const Text('Send Audio 01'),
),
ElevatedButton(
onPressed: () async {
final audioData = await rootBundle
.load('assets/audio_02.pcm')
.then((value) => value.buffer.asUint8List());
final conversationID = await _avatarController?.send(
audioData,
end: false,
);
debugPrint('send audio 02, conversationID: $conversationID');
},
child: const Text('Send Audio 02'),
),
ElevatedButton(
onPressed: () async {
final audioData = await rootBundle
.load('assets/audio_03.pcm')
.then((value) => value.buffer.asUint8List());
final conversationID = await _avatarController?.send(
audioData,
end: true,
);
debugPrint('send audio 03, conversationID: $conversationID');
},
child: const Text('Send Audio 03'),
),
ElevatedButton(
onPressed: () {
_avatarController?.close();
},
child: const Text('Close'),
),
ElevatedButton(
onPressed: () {
_avatarController?.interrupt();
},
child: const Text('Interrupt'),
),
Spacer(),
Text('Connection State: ${_connectionState.name}'),
Text('Conversation State: ${_conversationState.name}'),
],
);
}
}
class HostDrivingServiceModePage extends StatefulWidget {
const HostDrivingServiceModePage({super.key});
@override
State<HostDrivingServiceModePage> createState() =>
_HostDrivingServiceModePageState();
}
class _HostDrivingServiceModePageState
extends State<HostDrivingServiceModePage> {
bool _isLoading = false;
ConversationState _conversationState = ConversationState.idle;
Avatar? _avatar;
AvatarController? _avatarController;
final Map<int, _MockData> _mockData = {};
int _selectedSampleRate = 16000;
void _loadAvatar() async {
try {
setState(() {
_isLoading = true;
});
// Mock: load from asset path
// Avatar avatar = await AvatarManager.shared.derive('assets/avatar'); // Compatible: assets/avatar/
Avatar? avatar = await AvatarManager.shared.retrieve(id: avatarID);
if (avatar != null) {
debugPrint('retrieve cached avatar.');
} else {
avatar = await AvatarManager.shared.load(
id: avatarID,
onProgress: (progress) {
debugPrint('load avatar progress: $progress');
},
);
debugPrint('load avatar succeeded');
}
setState(() {
_avatar = avatar;
});
} on AvatarError catch (e) {
debugPrint('load avatar error: ${e.name}');
} catch (e) {
debugPrint('load avatar failed: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
void _onPlatformViewCreated(AvatarController controller) async {
controller.onFirstRendering = () {
debugPrint('onFirstRendering');
};
controller.onConversationState = (state) {
debugPrint('onConversationState: ${state.name}');
setState(() {
_conversationState = state;
});
};
controller.onError = (error) {
debugPrint('onError: ${error.name}');
};
await controller.setVolume(1.0);
final pointCount = await controller.pointCount();
debugPrint('pointCount: $pointCount');
await controller.setOpaque(false);
await controller.setContentTransform(Transform.identity);
setState(() {
_avatarController = controller;
});
}
Future<void> _loadMockData() async {
if (_mockData.containsKey(_selectedSampleRate)) {
return;
}
try {
final response = await http.post(
Uri.parse('https://python-sdk-mock-demo.spatialwalk.cn/generate'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'sample_rate': '$_selectedSampleRate'}),
);
if (response.statusCode == 200) {
final data = _MockData.fromJson(jsonDecode(response.body));
setState(() {
_mockData[_selectedSampleRate] = data;
});
}
} catch (e) {
debugPrint('Fetching data failed: $e');
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Host Driving Service Mode')),
body: SafeArea(
child: Center(
child: _isLoading
? const CircularProgressIndicator()
: _avatar == null
? ElevatedButton(
onPressed: _loadAvatar,
child: const Text('Load Avatar'),
)
: _buildAvatar(),
),
),
),
);
}
Widget _buildAvatar() {
if (_avatar == null) {
return const CircularProgressIndicator();
}
return Column(
spacing: 6,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: 260,
height: 260,
child: AvatarWidget(
avatar: _avatar!,
onPlatformViewCreated: _onPlatformViewCreated,
),
),
),
Spacer(),
RadioGroup<int>(
groupValue: _selectedSampleRate,
onChanged: (value) {
setState(() {
_selectedSampleRate = value!;
});
},
child: Wrap(
alignment: WrapAlignment.center,
children: [8000, 16000, 22050, 24000, 32000, 44100, 48000].map((
rate,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Radio<int>(value: rate),
Text('$rate'),
],
);
}).toList(),
),
),
ElevatedButton(
onPressed: () {
_loadMockData();
},
child: const Text('Load Mock Data'),
),
ElevatedButton(
onPressed: () async {
final data = _mockData[_selectedSampleRate];
if (data == null || _avatarController == null) {
return;
}
final audioData = data.audio;
final animations = data.animations;
// Simulates the stream playing of the audio and animations.
final conversationID = await _avatarController!.yieldAudioData(
audioData,
end: true,
audioFormat: AudioFormat(sampleRate: _selectedSampleRate),
);
for (var i = 0; i < animations.length; i++) {
final animation = animations[i];
Future.delayed(Duration(seconds: i + 1), () {
_avatarController?.yieldAnimations([
animation,
], conversationID: conversationID);
});
}
},
child: const Text('Play Stream'),
),
ElevatedButton(
onPressed: () async {
final data = _mockData[_selectedSampleRate];
if (data == null || _avatarController == null) {
return;
}
final audioData = data.audio;
final animations = data.animations;
// Simulates the playback of the audio and animations.
final conversationID = await _avatarController!.yieldAudioData(
audioData,
end: true,
audioFormat: AudioFormat(sampleRate: _selectedSampleRate),
);
_avatarController!.yieldAnimations(
animations,
conversationID: conversationID,
);
},
child: const Text('Playback'),
),
ElevatedButton(
onPressed: () {
_avatarController?.interrupt();
},
child: const Text('Interrupt'),
),
Spacer(),
Text(
'Mock Data Loaded: ${_mockData.containsKey(_selectedSampleRate) ? '✅' : '❌'}',
),
Text('Conversation State: ${_conversationState.name}'),
],
);
}
}
class _MockData {
final Uint8List audio;
final List<Uint8List> animations;
_MockData({required this.audio, required this.animations});
factory _MockData.fromJson(Map<String, dynamic> json) {
return _MockData(
audio: base64Decode(json['audio_base64']),
animations: (json['animation_messages_base64'] as List<dynamic>)
.map<Uint8List>((animation) => base64Decode(animation as String))
.toList(),
);
}
}