flutter_auto_echo_cancellation_websocket 1.0.0 copy "flutter_auto_echo_cancellation_websocket: ^1.0.0" to clipboard
flutter_auto_echo_cancellation_websocket: ^1.0.0 copied to clipboard

A Flutter plugin for real-time voice communication with automatic echo cancellation (AEC) over WebSocket. Supports native AEC on iOS (VoiceProcessingIO) and Android (AcousticEchoCanceler).

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_auto_echo_cancellation_websocket/flutter_auto_echo_cancellation_websocket.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Voice Plugin Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const VoiceScreen(),
    );
  }
}

class VoiceScreen extends StatefulWidget {
  const VoiceScreen({super.key});

  @override
  State<VoiceScreen> createState() => _VoiceScreenState();
}

class _VoiceScreenState extends State<VoiceScreen> {
  final VoicePlugin _plugin = VoicePlugin();
  
  final List<TranscriptMessage> _transcripts = [];
  bool _isConnecting = false;
  String? _errorMessage;
  bool _aecEnabled = false;
  bool _aecSupported = false;
  double _inputLevel = 0;
  double _outputLevel = 0;

  @override
  void initState() {
    super.initState();
    _setupPlugin();
  }

  void _setupPlugin() {
    // Listen to the event stream
    _plugin.eventStream.listen((event) {
      if (event is ConnectionStateEvent) {
        setState(() {
          _isConnecting = event.state == VoiceConnectionState.connecting;
          if (event.state == VoiceConnectionState.failed) {
            _errorMessage = event.reason ?? 'Connection failed';
          }
        });
      } else if (event is AgentStateEvent) {
        setState(() {});
      } else if (event is TranscriptEvent) {
        setState(() {
          _transcripts.add(TranscriptMessage(
            text: event.text,
            isUser: event.isUser,
            timestamp: event.timestamp,
          ));
        });
      } else if (event is ErrorEvent) {
        setState(() {
          _errorMessage = event.message;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Error: ${event.message}'),
            backgroundColor: Colors.red,
          ),
        );
      } else if (event is AECStatusEvent) {
        setState(() {
          _aecEnabled = event.isEnabled;
          _aecSupported = event.isSupported;
        });
      } else if (event is AudioLevelEvent) {
        setState(() {
          if (event.isInput) {
            _inputLevel = event.level;
          } else {
            _outputLevel = event.level;
          }
        });
      }
    });
  }

  Future<void> _startCall() async {
    setState(() {
      _isConnecting = true;
      _errorMessage = null;
      _transcripts.clear();
    });

    final success = await _plugin.connect(
      VoicePluginConfig(
        // Replace with your actual WebSocket endpoint
        endpoint: 'wss://api-west.millis.ai:8080/millis',
        agentId: 'YOUR_AGENT_ID',
        publicKey: 'YOUR_PUBLIC_KEY',
        metadata: {
          'user_id': 'demo-user',
          'name': 'Demo User',
          'email': '[email protected]',
        },
        includeMetadataInPrompt: true,
        enableNoiseSuppression: true,
        enableAutoGainControl: true,
        autoReconnect: true,
        maxReconnectAttempts: 3,
      ),
    );

    if (!success) {
      setState(() {
        _isConnecting = false;
        _errorMessage = 'Failed to connect';
      });
    }
  }

  void _endCall() {
    _plugin.disconnect();
    setState(() {
      _inputLevel = 0;
      _outputLevel = 0;
    });
  }

  void _toggleMute() {
    _plugin.toggleMicrophoneMute();
    setState(() {});
  }

  void _toggleSpeaker() {
    _plugin.toggleSpeakerMute();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Voice Agent Demo'),
      ),
      body: Column(
        children: [
          // Status bar
          Container(
            padding: const EdgeInsets.all(16),
            color: _getStatusColor(),
            child: Row(
              children: [
                Icon(
                  _getStatusIcon(),
                  color: Colors.white,
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    _getStatusText(),
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                if (_aecSupported)
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 8,
                      vertical: 4,
                    ),
                    decoration: BoxDecoration(
                      color: _aecEnabled ? Colors.green : Colors.orange,
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      _aecEnabled ? 'AEC ON' : 'AEC OFF',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                      ),
                    ),
                  ),
              ],
            ),
          ),
          
          // Audio levels
          if (_plugin.isConnected) ...[
            Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  Expanded(
                    child: Column(
                      children: [
                        const Text('Microphone'),
                        const SizedBox(height: 4),
                        LinearProgressIndicator(
                          value: _inputLevel,
                          backgroundColor: Colors.grey[300],
                          valueColor: AlwaysStoppedAnimation<Color>(
                            _plugin.isMicrophoneMuted 
                                ? Colors.grey 
                                : Colors.green,
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      children: [
                        const Text('Speaker'),
                        const SizedBox(height: 4),
                        LinearProgressIndicator(
                          value: _outputLevel,
                          backgroundColor: Colors.grey[300],
                          valueColor: AlwaysStoppedAnimation<Color>(
                            _plugin.isSpeakerMuted 
                                ? Colors.grey 
                                : Colors.blue,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ],
          
          // Transcripts
          Expanded(
            child: _transcripts.isEmpty
                ? Center(
                    child: Text(
                      _plugin.isConnected
                          ? 'Start speaking...'
                          : 'Press the button to start a call',
                      style: TextStyle(
                        color: Colors.grey[600],
                        fontSize: 16,
                      ),
                    ),
                  )
                : ListView.builder(
                    padding: const EdgeInsets.all(16),
                    itemCount: _transcripts.length,
                    itemBuilder: (context, index) {
                      final transcript = _transcripts[index];
                      return _buildTranscriptBubble(transcript);
                    },
                  ),
          ),
          
          // Error message
          if (_errorMessage != null)
            Container(
              padding: const EdgeInsets.all(8),
              color: Colors.red[100],
              child: Row(
                children: [
                  const Icon(Icons.error, color: Colors.red),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      _errorMessage!,
                      style: const TextStyle(color: Colors.red),
                    ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.close),
                    onPressed: () => setState(() => _errorMessage = null),
                  ),
                ],
              ),
            ),
          
          // Control buttons
          Container(
            padding: const EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                if (_plugin.isConnected) ...[
                  // Mute button
                  FloatingActionButton(
                    heroTag: 'mute',
                    onPressed: _toggleMute,
                    backgroundColor: _plugin.isMicrophoneMuted
                        ? Colors.red
                        : Colors.grey,
                    child: Icon(
                      _plugin.isMicrophoneMuted ? Icons.mic_off : Icons.mic,
                    ),
                  ),
                  
                  // End call button
                  FloatingActionButton.large(
                    heroTag: 'endCall',
                    onPressed: _endCall,
                    backgroundColor: Colors.red,
                    child: const Icon(Icons.call_end, size: 36),
                  ),
                  
                  // Speaker button
                  FloatingActionButton(
                    heroTag: 'speaker',
                    onPressed: _toggleSpeaker,
                    backgroundColor: _plugin.isSpeakerMuted
                        ? Colors.red
                        : Colors.grey,
                    child: Icon(
                      _plugin.isSpeakerMuted
                          ? Icons.volume_off
                          : Icons.volume_up,
                    ),
                  ),
                ] else ...[
                  // Start call button
                  FloatingActionButton.large(
                    heroTag: 'startCall',
                    onPressed: _isConnecting ? null : _startCall,
                    backgroundColor: Colors.green,
                    child: _isConnecting
                        ? const CircularProgressIndicator(color: Colors.white)
                        : const Icon(Icons.call, size: 36),
                  ),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTranscriptBubble(TranscriptMessage transcript) {
    return Align(
      alignment:
          transcript.isUser ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 4),
        padding: const EdgeInsets.all(12),
        constraints: BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width * 0.75,
        ),
        decoration: BoxDecoration(
          color: transcript.isUser
              ? Theme.of(context).colorScheme.primary
              : Colors.grey[300],
          borderRadius: BorderRadius.circular(16),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              transcript.text,
              style: TextStyle(
                color: transcript.isUser ? Colors.white : Colors.black87,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              _formatTime(transcript.timestamp),
              style: TextStyle(
                fontSize: 10,
                color: transcript.isUser ? Colors.white70 : Colors.black45,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Color _getStatusColor() {
    switch (_plugin.connectionState) {
      case VoiceConnectionState.connected:
        return Colors.green;
      case VoiceConnectionState.connecting:
      case VoiceConnectionState.reconnecting:
        return Colors.orange;
      case VoiceConnectionState.failed:
        return Colors.red;
      case VoiceConnectionState.disconnected:
        return Colors.grey;
    }
  }

  IconData _getStatusIcon() {
    switch (_plugin.agentState) {
      case VoiceAgentState.listening:
        return Icons.hearing;
      case VoiceAgentState.speaking:
        return Icons.record_voice_over;
      case VoiceAgentState.thinking:
        return Icons.psychology;
      case VoiceAgentState.paused:
        return Icons.pause;
      case VoiceAgentState.idle:
        return Icons.mic_none;
    }
  }

  String _getStatusText() {
    if (!_plugin.isConnected) {
      return _plugin.connectionState.toString().split('.').last;
    }
    return _plugin.agentState.toString().split('.').last;
  }

  String _formatTime(DateTime time) {
    return '${time.hour.toString().padLeft(2, '0')}:'
        '${time.minute.toString().padLeft(2, '0')}:'
        '${time.second.toString().padLeft(2, '0')}';
  }

  @override
  void dispose() {
    _plugin.dispose();
    super.dispose();
  }
}

class TranscriptMessage {
  final String text;
  final bool isUser;
  final DateTime timestamp;

  TranscriptMessage({
    required this.text,
    required this.isUser,
    required this.timestamp,
  });
}
3
likes
140
points
125
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for real-time voice communication with automatic echo cancellation (AEC) over WebSocket. Supports native AEC on iOS (VoiceProcessingIO) and Android (AcousticEchoCanceler).

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_auto_echo_cancellation_websocket

Packages that implement flutter_auto_echo_cancellation_websocket