mugib_voice 0.4.0
mugib_voice: ^0.4.0 copied to clipboard
Official Flutter SDK for Mugib voice agents. One client for VAPI and Google Gemini Live — pass apiKey and agentId only.
mugib_voice #
Official Flutter SDK for Mugib real-time voice agents.
Add a voice call to your mobile app: the user talks to your Mugib AI agent by microphone, hears the reply, sees live transcripts, and can mute or hang up. You do not choose or configure a voice provider — Mugib does that on the server. Your app only needs a project API key and a voice agent id.
What you need (from Mugib dashboard) #
| Item | Where |
|---|---|
| Project API key | Project → Settings → API Key |
| Voice agent id | Project → Voice Agents → numeric id |
| API base URL | https://api.mugib.com (default; use your self-hosted URL if applicable) |
Everything else — knowledge base, system/voice prompts, language, voice personality, and which voice engine runs the call — is configured in the Mugib project. No provider keys or assistant IDs in your app.
Install #
dependencies:
mugib_voice: ^0.4.0
flutter pub get
Requirements: Flutter 3.24+ / Dart 3.5+ · iOS & Android (see permissions below).
Monorepo dev:
path: ../sdk/flutter/mugib_voice
Platform permissions #
iOS — ios/Runner/Info.plist #
<key>NSMicrophoneUsageDescription</key>
<string>We need the microphone for voice conversations with our support agent.</string>
ios/Podfile: platform :ios, '12.0'
Android — AndroidManifest.xml #
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
minSdkVersion 23 or higher.
Quick start #
import 'package:mugib_voice/mugib_voice.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await MugibVoice.initialize(); // once at startup
runApp(const MyApp());
}
// Inside your screen / button handler:
final call = MugibVoiceClient(
apiKey: 'YOUR_PROJECT_API_KEY',
agentId: 34,
);
call.state.listen((state) {
// idle → connecting → connected → listening / speaking → ended / error
});
call.transcripts.listen((line) {
// line.role: "user" | "assistant"
// line.text: live transcription
});
call.errors.listen((message) {
// show snackbar / dialog
});
await call.start(); // Mugib connects the call — you do nothing else
// … user talks …
call.mute();
call.unmute();
await call.end(); // hang up
await call.start(); // same client can call again
await call.dispose(); // when screen is destroyed
That is the full integration surface for most apps: apiKey + agentId +
start().
Optional: voice agent picker #
If the project has multiple voice agents, list them and let the user pick
one. You only need id and display fields (name, language, …) — not the
provider.
final agents = await MugibVoice.listAgents(apiKey: 'YOUR_PROJECT_API_KEY');
// Example UI: show agent.name, agent.language
final selectedId = agents.first.id;
final call = MugibVoiceClient(apiKey: 'YOUR_PROJECT_API_KEY', agentId: selectedId);
await call.start();
The provider field on each agent is informational only (e.g. for an
admin/debug label). The SDK reads it from Mugib when connecting; your code
must not branch on it.
Call states #
MugibCallState |
Meaning |
|---|---|
idle |
Not in a call |
connecting |
Opening the Mugib voice session |
connected |
Call is live |
listening |
Agent is waiting for the user |
speaking |
Agent is talking |
ended |
Call finished normally |
error |
Failed — see errors / errorEvents streams |
API reference #
MugibVoice #
| Method | Description |
|---|---|
initialize() |
Call once in main() before any voice call |
listAgents({apiKey, baseUrl}) |
Active voice agents for a project (picker UI) |
MugibVoiceClient #
| Parameter | Description |
|---|---|
apiKey |
Project API key |
agentId |
Voice agent id from Mugib dashboard |
baseUrl |
Default https://api.mugib.com |
| Parameter (optional) | Description |
|---|---|
feedThresholdFrames |
Gemini Live playback buffer in output samples (default 4000 ≈ 166ms). Lower = less latency, higher underrun risk |
maxReconnectAttempts |
Gemini Live auto-reconnect attempts after a drop (default 3) |
reconnectBackoff |
Base delay between reconnect attempts, multiplied by attempt number (default 600ms) |
| Method / property | Description |
|---|---|
start() |
Start a voice call (Mugib picks connection details) |
end() |
End the call |
mute() / unmute() / toggleMute() |
Microphone control |
dispose() |
Release resources; client cannot be reused |
state |
Stream of MugibCallState |
transcripts |
Stream of MugibTranscript |
errors |
Stream of error message strings |
errorEvents |
Stream of MugibVoiceError (with MugibVoiceErrorCode) |
metrics / currentMetrics |
Stream / snapshot of MugibCallMetrics (latency) |
callDuration / durationTicks |
Elapsed call time (snapshot / per-second stream) |
token |
Last session metadata from Mugib (optional; for debugging) |
Errors, metrics & call timer #
// Branch on a machine-readable code instead of matching strings.
call.errorEvents.listen((e) {
switch (e.code) {
case MugibVoiceErrorCode.microphonePermissionDenied:
// prompt the user to enable mic
break;
case MugibVoiceErrorCode.quotaExceeded:
// show "upgrade plan"
break;
case MugibVoiceErrorCode.network:
// SDK auto-reconnects (Gemini); show a "reconnecting…" hint
break;
default:
// e.message has a human-readable description
}
});
// Latency metrics (time-to-first-audio, per-response latency).
call.metrics.listen((m) {
print('first audio: ${m.timeToFirstAudio?.inMilliseconds} ms');
print('avg response: ${m.averageResponseLatency?.inMilliseconds} ms');
});
// Live call timer.
call.durationTicks.listen((d) => setState(() => _elapsed = d));
Error codes — MugibVoiceErrorCode #
Every MugibVoiceError (and MugibVoiceException.code) carries one of these.
Branch on the code; show error.message for a human-readable description.
| Code | Meaning / typical fix |
|---|---|
microphonePermissionDenied |
User denied mic access — prompt to enable it in settings |
invalidApiKey |
API key missing/invalid (HTTP 401/403 or UNAUTHORIZED) |
agentNotFound |
agentId does not exist (HTTP 404 or NOT_FOUND) |
agentInactive |
Agent exists but is disabled — activate it in the dashboard |
notConfigured |
Provider/credentials not set on the server, or missing token fields |
quotaExceeded |
Monthly voice-minutes quota reached (HTTP 429) — upgrade plan |
network |
Transport failure / WebSocket dropped (auto-reconnect retried first) |
connectionTimeout |
Session did not become ready within the timeout |
alreadyInProgress |
start() called while a call is active — end() first |
disposed |
Client/session was disposed — create a new client |
providerError |
The voice provider reported an error mid-call |
unsupportedProvider |
Provider returned by Mugib is not supported by this SDK version |
unknown |
Cause could not be determined (e.g. malformed server response) |
Latency metrics — MugibCallMetrics #
All fields are null until the relevant event occurs. Per-response latency is
only populated for Gemini Live; VAPI reports timeToFirstAudio only.
| Field | Meaning |
|---|---|
timeToFirstAudio |
start() → first audio frame (connection + greeting) |
lastResponseLatency |
User stopped speaking → first audio of the reply |
averageResponseLatency |
Rolling average of lastResponseLatency |
responseSamples |
Number of measured responses in the average |
Auto-reconnect & session resumption (Gemini Live) #
If the WebSocket drops unexpectedly mid-call, the SDK reconnects automatically
(state goes back to connecting, then connected) while the mic and player
keep running. Since v0.4.0 the SDK uses Google's native session
resumption — the reconnected session continues with the same conversation
context (no repeated greeting, no lost history). The Mugib server relays a
resumption handle periodically; the SDK stores it in memory and sends it back
on reconnect. If Google signals an imminent disconnect (goAway), the SDK
reconnects proactively before the connection closes.
After maxReconnectAttempts failures it emits a network error. Tune with
maxReconnectAttempts and reconnectBackoff. VAPI handles reconnection
internally.
Latency tuning (Gemini Live) #
Gemini Live has no provider-side latency knob; the only client-tunable lever is the playback buffer. Lower it for snappier audio at the cost of underrun risk:
final call = MugibVoiceClient(
apiKey: 'YOUR_KEY',
agentId: 34,
feedThresholdFrames: 2400, // ≈100ms (default 4000 ≈ 166ms)
);
What Mugib handles for you #
You do not configure these in the app:
- Knowledge base & MCP tool calls during the call
- System / voice prompts and agent personality
- Language, dialect, voice, greeting
- Voice transport and provider selection
- Barge-in (user interrupting the agent)
The SDK handles microphone capture, playback, transcripts, mute, and hang-up on the device. Mugib handles the AI and business logic on the server.
Architecture (for curious developers) #
Your app Mugib cloud
──────── ───────────
apiKey + agentId → validates project & agent
→ loads KB, prompts, voice settings
→ starts the right voice backend (transparent)
← audio, transcripts, call state
Your integration code stays the same even if the project owner changes the voice backend in the Mugib dashboard.
Troubleshooting #
| Symptom | Check |
|---|---|
Microphone permission denied |
iOS Info.plist / Android RECORD_AUDIO |
Invalid API Key |
API key from correct project |
Voice agent not found |
agentId exists and is active in Voice Agents |
Call already in progress |
Call end() before start() again, or use a new client after dispose() |
| No audio on emulator | Test on a real device; emulators have unreliable audio |
Example app #
See example/ in this package for a minimal call screen with agent picker.
Maintainers #
cd sdk/flutter/mugib_voice
make help
make check
make release-patch # bump version + publish to pub.dev
Links #
- Package: https://pub.dev/packages/mugib_voice
- Mugib: https://mugib.com
- Integration guide:
docs/voice-integration.mdin the Mugib repo
License #
MIT — see LICENSE.