avatar_kit 1.0.0-beta.13 copy "avatar_kit: ^1.0.0-beta.13" to clipboard
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(),
    );
  }
}
0
likes
140
points
759
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for AvatarKit which provides avatar rendering with audio driving support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on avatar_kit

Packages that implement avatar_kit