Skip to navigation

Flutter

View as Markdown

For real-time voice on iOS and Android, the Flutter SDK (sarvamconv_ai_sdk) wraps the WebSocket interface in SamvaadAgent, with DefaultAudioInterface for microphone capture and playback.

Install

Add the SDK to pubspec.yaml:

dependencies:
sarvamconv_ai_sdk: ^1.0.0
flutter pub get

iOS. Add microphone permission to ios/Runner/Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice conversations</string>

Android. Add permissions to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />

Start a voice session

import 'package:flutter/material.dart';
import 'package:sarvamconv_ai_sdk/sarvamconv_ai_sdk.dart';
class VoiceChat extends StatefulWidget {
const VoiceChat({super.key});
@override
State<VoiceChat> createState() => _VoiceChatState();
}
class _VoiceChatState extends State<VoiceChat> {
SamvaadAgent? _agent;
DefaultAudioInterface? _audioInterface;
bool _isConnected = false;
String _transcript = '';
Future<void> startConversation() async {
_audioInterface = DefaultAudioInterface(inputSampleRate: 16000);
final config = InteractionConfig(
orgId: 'your_org_id',
workspaceId: 'your_workspace_id',
appId: 'your_app_id',
userIdentifier: 'user123',
userIdentifierType: UserIdentifierType.custom,
interactionType: InteractionType.call,
sampleRate: 16000,
);
_agent = SamvaadAgent(
apiKey: 'your_api_key',
config: config,
audioInterface: _audioInterface,
textCallback: (msg) async {
if (msg is ServerTextChunkMsg) {
setState(() => _transcript += msg.text);
}
},
eventCallback: (event) async {
if (event is ServerInteractionConnectedEvent) {
setState(() => _isConnected = true);
} else if (event is ServerInteractionEndEvent) {
setState(() => _isConnected = false);
}
},
);
await _agent!.start();
final connected = await _agent!.waitForConnect(
timeout: const Duration(seconds: 10),
);
if (!connected) {
throw Exception('Connection timeout');
}
}
Future<void> stopConversation() async {
await _agent?.stop();
_agent = null;
_audioInterface = null;
setState(() => _isConnected = false);
}
@override
void dispose() {
stopConversation();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Voice Chat')),
body: Column(
children: [
ElevatedButton(
onPressed: _isConnected ? stopConversation : startConversation,
child: Text(_isConnected ? 'Stop Voice Chat' : 'Start Voice Chat'),
),
Expanded(
child: SingleChildScrollView(
child: Text('Transcript: $_transcript'),
),
),
],
),
);
}
}

DefaultAudioInterface captures the microphone and plays agent audio (16-bit PCM mono at 8 kHz, 16 kHz, or 48 kHz). start() fetches a signed WebSocket URL and begins streaming.

InteractionConfig

FieldRequiredDescription
userIdentifierTypeYesUserIdentifierType.custom, .email, .phoneNumber, or .unknown
userIdentifierYesThe identifier value; also what you search by in Log Analyser
orgIdYesYour organization ID
workspaceIdYesYour workspace ID
appIdYesThe agent to connect to
interactionTypeYesInteractionType.call for a voice session
sampleRateYes8000, 16000, or 48000 (16-bit PCM mono)
versionNoPins a specific committed agent version. If omitted, the SDK uses the latest committed version, and the connection fails if the agent has no committed version
agentVariablesNoSeed agent variables at session start
initialLanguageNameNoStarting language; must be one of the agent’s allowed languages (for example SarvamToolLanguageName.hindi)
initialStateNameNoStarting state, if the agent uses states
initialBotMessageNoFirst message from the agent

Callbacks

Pass any of these to SamvaadAgent to react to what happens during the call:

CallbackFires withUse it for
textCallbackServerTextMsg or ServerTextChunkMsgStreaming or complete agent text
audioCallbackaudio chunk (audioBytes, sampleRate)Raw agent audio, if you’re handling playback yourself instead of using audioInterface
eventCallbacksession eventServerInteractionConnectedEvent, ServerUserInterruptEvent, ServerInteractionEndEvent, speech start/end, language change, state transition, variable update

DefaultAudioInterface

final audioInterface = DefaultAudioInterface(
inputSampleRate: 16000,
outputSampleRate: 16000,
);

To bring your own I/O, implement AudioInterface:

class CustomAudioInterface implements AudioInterface {
@override
Future<void> start(AudioInputCallback inputCallback) async {
// Capture audio and call inputCallback with chunks
}
@override
Future<void> output(Uint8List audio, {int? sampleRate}) async {
// Play audio through the speaker
}
@override
void interrupt() {
// Stop queued playback
}
@override
Future<void> stop() async {
// Cleanup
}
}

Session methods

MethodDescription
await agent.start()Fetch a signed WebSocket URL and connect
await agent.waitForConnect(timeout: Duration(seconds: 10))Wait until connected (returns false on timeout)
await agent.sendAudio(audioBytes)Send raw 16-bit PCM mono at config.sampleRate
agent.isConnectedCurrent connection status
agent.interactionIdThe current interaction (call) ID, once connected
await agent.waitForDisconnect()Wait until the session ends
await agent.stop()Close the connection and clean up

Call await agent.stop() in dispose() so the WebSocket and audio interface are cleaned up. Reconnection is not supported: each WebSocket URL is single-use. If the connection drops, call stop() and create a new SamvaadAgent.

Stop the agent when the app goes to the background:

class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_agent?.stop();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_agent?.stop();
}
}
}

Troubleshooting

Microphone permission denied. Request the microphone before starting:

import 'package:permission_handler/permission_handler.dart';
final status = await Permission.microphone.request();
if (!status.isGranted) {
// Ask the user to enable the permission
}

No audio output. Check device volume, that DefaultAudioInterface is initialized, and that sampleRate matches the session config.

Connection timeout. Check connectivity, API key, orgId / workspaceId / appId, and that the agent has a committed version.

Never embed your API key in the app binary. Omit apiKey, set baseUrl to a backend you control, and pass your app auth in headers:

final agent = SamvaadAgent(
config: config,
baseUrl: 'https://your-proxy-server.com/sarvam-proxy/',
headers: {
'Authorization': 'Bearer user_session_token',
},
audioInterface: DefaultAudioInterface(inputSampleRate: 16000),
);