telnyx-webrtc-client-flutter
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTelnyx WebRTC - Flutter SDK
Telnyx WebRTC - Flutter SDK
Build real-time voice communication into Flutter applications (Android, iOS, Web).
Prerequisites: Create WebRTC credentials and generate a login token using the Telnyx server-side SDK. See theskill in your server language plugin (e.g.,telnyx-webrtc-*,telnyx-python).telnyx-javascript
在Flutter应用(Android、iOS、Web)中构建实时语音通信功能。
前提条件:使用Telnyx服务端SDK创建WebRTC凭证并生成登录令牌。请查看您所用服务端语言插件中的技能(例如telnyx-webrtc-*、telnyx-python)。telnyx-javascript
Quick Start Option
快速开始方案
For faster implementation, consider Telnyx Common - a higher-level abstraction that simplifies WebRTC integration with minimal setup.
如需更快速的实现,可考虑使用Telnyx Common——这是一个高层抽象库,可通过最少配置简化WebRTC集成。
Installation
安装
Add to :
pubspec.yamlyaml
dependencies:
telnyx_webrtc: ^latest_versionThen run:
bash
flutter pub get在中添加依赖:
pubspec.yamlyaml
dependencies:
telnyx_webrtc: ^latest_version然后执行:
bash
flutter pub getPlatform Configuration
平台配置
Android
Android
Add to :
AndroidManifest.xmlxml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />在中添加权限:
AndroidManifest.xmlxml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />iOS
iOS
Add to :
Info.plistxml
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) needs microphone access for calls</string>在中添加:
Info.plistxml
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME)需要麦克风权限以进行通话</string>Authentication
身份验证
Option 1: Credential-Based Login
方案1:基于凭证的登录
dart
final telnyxClient = TelnyxClient();
final credentialConfig = CredentialConfig(
sipUser: 'your_sip_username',
sipPassword: 'your_sip_password',
sipCallerIDName: 'Display Name',
sipCallerIDNumber: '+15551234567',
notificationToken: fcmOrApnsToken, // Optional: for push
autoReconnect: true,
debug: true,
logLevel: LogLevel.debug,
);
telnyxClient.connectWithCredential(credentialConfig);dart
final telnyxClient = TelnyxClient();
final credentialConfig = CredentialConfig(
sipUser: 'your_sip_username',
sipPassword: 'your_sip_password',
sipCallerIDName: 'Display Name',
sipCallerIDNumber: '+15551234567',
notificationToken: fcmOrApnsToken, // 可选:用于推送通知
autoReconnect: true,
debug: true,
logLevel: LogLevel.debug,
);
telnyxClient.connectWithCredential(credentialConfig);Option 2: Token-Based Login (JWT)
方案2:基于令牌的登录(JWT)
dart
final tokenConfig = TokenConfig(
sipToken: 'your_jwt_token',
sipCallerIDName: 'Display Name',
sipCallerIDNumber: '+15551234567',
notificationToken: fcmOrApnsToken,
autoReconnect: true,
debug: true,
);
telnyxClient.connectWithToken(tokenConfig);dart
final tokenConfig = TokenConfig(
sipToken: 'your_jwt_token',
sipCallerIDName: 'Display Name',
sipCallerIDNumber: '+15551234567',
notificationToken: fcmOrApnsToken,
autoReconnect: true,
debug: true,
);
telnyxClient.connectWithToken(tokenConfig);Configuration Options
配置选项
| Parameter | Type | Description |
|---|---|---|
| String | Credentials from Telnyx Portal |
| String | Caller ID name displayed to recipients |
| String | Caller ID number |
| String? | FCM (Android) or APNS (iOS) token |
| bool | Auto-retry login on failure |
| bool | Enable call quality metrics |
| LogLevel | none, error, warning, debug, info, all |
| String? | Custom ringtone asset path |
| String? | Custom ringback tone asset path |
| 参数 | 类型 | 说明 |
|---|---|---|
| String | 来自Telnyx门户的凭证 |
| String | 显示给通话接收方的主叫ID名称 |
| String | 主叫ID号码 |
| String? | FCM(Android)或APNS(iOS)令牌 |
| bool | 登录失败时自动重试 |
| bool | 启用通话质量指标 |
| LogLevel | none、error、warning、debug、info、all |
| String? | 自定义铃声资源路径 |
| String? | 自定义回铃音资源路径 |
Making Outbound Calls
发起外呼
dart
telnyxClient.call.newInvite(
'John Doe', // callerName
'+15551234567', // callerNumber
'+15559876543', // destinationNumber
'my-custom-state', // clientState
);dart
telnyxClient.call.newInvite(
'John Doe', // 主叫名称
'+15551234567', // 主叫号码
'+15559876543', // 被叫号码
'my-custom-state', // 客户端状态
);Receiving Inbound Calls
接听呼入
Listen for socket events:
dart
InviteParams? _incomingInvite;
Call? _currentCall;
telnyxClient.onSocketMessageReceived = (TelnyxMessage message) {
switch (message.socketMethod) {
case SocketMethod.CLIENT_READY:
// Ready to make/receive calls
break;
case SocketMethod.LOGIN:
// Successfully logged in
break;
case SocketMethod.INVITE:
// Incoming call!
_incomingInvite = message.message.inviteParams;
// Show incoming call UI...
break;
case SocketMethod.ANSWER:
// Call was answered
break;
case SocketMethod.BYE:
// Call ended
break;
}
};
// Accept the incoming call
void acceptCall() {
if (_incomingInvite != null) {
_currentCall = telnyxClient.acceptCall(
_incomingInvite!,
'My Name',
'+15551234567',
'state',
);
}
}监听Socket事件:
dart
InviteParams? _incomingInvite;
Call? _currentCall;
telnyxClient.onSocketMessageReceived = (TelnyxMessage message) {
switch (message.socketMethod) {
case SocketMethod.CLIENT_READY:
// 已准备好发起/接听通话
break;
case SocketMethod.LOGIN:
// 登录成功
break;
case SocketMethod.INVITE:
// 有呼入通话!
_incomingInvite = message.message.inviteParams;
// 显示呼入通话界面...
break;
case SocketMethod.ANSWER:
// 通话已接通
break;
case SocketMethod.BYE:
// 通话已结束
break;
}
};
// 接听呼入通话
void acceptCall() {
if (_incomingInvite != null) {
_currentCall = telnyxClient.acceptCall(
_incomingInvite!,
'My Name',
'+15551234567',
'state',
);
}
}Call Controls
通话控制
dart
// End call
telnyxClient.call.endCall(telnyxClient.call.callId);
// Decline incoming call
telnyxClient.createCall().endCall(_incomingInvite?.callID);
// Mute/Unmute
telnyxClient.call.onMuteUnmutePressed();
// Hold/Unhold
telnyxClient.call.onHoldUnholdPressed();
// Toggle speaker
telnyxClient.call.enableSpeakerPhone(true);
// Send DTMF tone
telnyxClient.call.dtmf(telnyxClient.call.callId, '1');dart
// 结束通话
telnyxClient.call.endCall(telnyxClient.call.callId);
// 拒绝呼入通话
telnyxClient.createCall().endCall(_incomingInvite?.callID);
// 静音/取消静音
telnyxClient.call.onMuteUnmutePressed();
// 保持/取消保持
telnyxClient.call.onHoldUnholdPressed();
// 切换扬声器
telnyxClient.call.enableSpeakerPhone(true);
// 发送DTMF音
telnyxClient.call.dtmf(telnyxClient.call.callId, '1');Push Notifications - Android (FCM)
推送通知 - Android(FCM)
1. Setup Firebase
1. 配置Firebase
dart
// main.dart
('vm:entry-point')
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (defaultTargetPlatform == TargetPlatform.android) {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
}
runApp(const MyApp());
}dart
// main.dart
('vm:entry-point')
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (defaultTargetPlatform == TargetPlatform.android) {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
}
runApp(const MyApp());
}2. Background Handler
2. 后台处理器
dart
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
// Show notification (e.g., using flutter_callkit_incoming)
showIncomingCallNotification(message);
// Listen for user action
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
switch (event!.event) {
case Event.actionCallAccept:
TelnyxClient.setPushMetaData(
message.data,
isAnswer: true,
isDecline: false,
);
break;
case Event.actionCallDecline:
TelnyxClient.setPushMetaData(
message.data,
isAnswer: false,
isDecline: true, // SDK handles decline automatically
);
break;
}
});
}dart
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
// 显示通知(例如使用flutter_callkit_incoming)
showIncomingCallNotification(message);
// 监听用户操作
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
switch (event!.event) {
case Event.actionCallAccept:
TelnyxClient.setPushMetaData(
message.data,
isAnswer: true,
isDecline: false,
);
break;
case Event.actionCallDecline:
TelnyxClient.setPushMetaData(
message.data,
isAnswer: false,
isDecline: true, // SDK会自动处理拒绝操作
);
break;
}
});
}3. Handle Push When App Opens
3. 应用启动时处理推送
dart
Future<void> _handlePushNotification() async {
final data = await TelnyxClient.getPushMetaData();
if (data != null) {
PushMetaData pushMetaData = PushMetaData.fromJson(data);
telnyxClient.handlePushNotification(
pushMetaData,
credentialConfig,
tokenConfig,
);
}
}dart
Future<void> _handlePushNotification() async {
final data = await TelnyxClient.getPushMetaData();
if (data != null) {
PushMetaData pushMetaData = PushMetaData.fromJson(data);
telnyxClient.handlePushNotification(
pushMetaData,
credentialConfig,
tokenConfig,
);
}
}Early Accept/Decline Handling
提前处理接听/拒绝
dart
bool _waitingForInvite = false;
void acceptCall() {
if (_incomingInvite != null) {
_currentCall = telnyxClient.acceptCall(...);
} else {
// Set flag if invite hasn't arrived yet
_waitingForInvite = true;
}
}
// In socket message handler:
case SocketMethod.INVITE:
_incomingInvite = message.message.inviteParams;
if (_waitingForInvite) {
acceptCall(); // Accept now that invite arrived
_waitingForInvite = false;
}
break;dart
bool _waitingForInvite = false;
void acceptCall() {
if (_incomingInvite != null) {
_currentCall = telnyxClient.acceptCall(...);
} else {
// 如果邀请尚未到达,设置标记
_waitingForInvite = true;
}
}
// 在Socket消息处理器中:
case SocketMethod.INVITE:
_incomingInvite = message.message.inviteParams;
if (_waitingForInvite) {
acceptCall(); // 邀请到达后立即接听
_waitingForInvite = false;
}
break;Push Notifications - iOS (APNS + PushKit)
推送通知 - iOS(APNS + PushKit)
1. AppDelegate Setup
1. AppDelegate配置
swift
// AppDelegate.swift
func pushRegistry(_ registry: PKPushRegistry,
didUpdate credentials: PKPushCredentials,
for type: PKPushType) {
let deviceToken = credentials.token.map {
String(format: "%02x", $0)
}.joined()
SwiftFlutterCallkitIncomingPlugin.sharedInstance?
.setDevicePushTokenVoIP(deviceToken)
}
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
guard type == .voIP else { return }
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] {
let callerName = (metadata["caller_name"] as? String) ?? ""
let callerNumber = (metadata["caller_number"] as? String) ?? ""
let callId = (metadata["call_id"] as? String) ?? UUID().uuidString
let data = flutter_callkit_incoming.Data(
id: callId,
nameCaller: callerName,
handle: callerNumber,
type: 0
)
data.extra = payload.dictionaryPayload as NSDictionary
SwiftFlutterCallkitIncomingPlugin.sharedInstance?
.showCallkitIncoming(data, fromPushKit: true)
}
}swift
// AppDelegate.swift
func pushRegistry(_ registry: PKPushRegistry,
didUpdate credentials: PKPushCredentials,
for type: PKPushType) {
let deviceToken = credentials.token.map {
String(format: "%02x", $0)
}.joined()
SwiftFlutterCallkitIncomingPlugin.sharedInstance?
.setDevicePushTokenVoIP(deviceToken)
}
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
guard type == .voIP else { return }
if let metadata = payload.dictionaryPayload["metadata"] as? [String: Any] {
let callerName = (metadata["caller_name"] as? String) ?? ""
let callerNumber = (metadata["caller_number"] as? String) ?? ""
let callId = (metadata["call_id"] as? String) ?? UUID().uuidString
let data = flutter_callkit_incoming.Data(
id: callId,
nameCaller: callerName,
handle: callerNumber,
type: 0
)
data.extra = payload.dictionaryPayload as NSDictionary
SwiftFlutterCallkitIncomingPlugin.sharedInstance?
.showCallkitIncoming(data, fromPushKit: true)
}
}2. Handle in Flutter
2. 在Flutter中处理
dart
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
switch (event!.event) {
case Event.actionCallIncoming:
PushMetaData? pushMetaData = PushMetaData.fromJson(
event.body['extra']['metadata']
);
telnyxClient.handlePushNotification(
pushMetaData,
credentialConfig,
tokenConfig,
);
break;
case Event.actionCallAccept:
// Handle accept
break;
}
});dart
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
switch (event!.event) {
case Event.actionCallIncoming:
PushMetaData? pushMetaData = PushMetaData.fromJson(
event.body['extra']['metadata']
);
telnyxClient.handlePushNotification(
pushMetaData,
credentialConfig,
tokenConfig,
);
break;
case Event.actionCallAccept:
// 处理接听操作
break;
}
});Handling Late Notifications
处理延迟通知
dart
const CALL_MISSED_TIMEOUT = 60; // seconds
void handlePushMessage(RemoteMessage message) {
DateTime now = DateTime.now();
Duration? diff = now.difference(message.sentTime!);
if (diff.inSeconds > CALL_MISSED_TIMEOUT) {
showMissedCallNotification(message);
return;
}
// Handle normal incoming call...
}dart
const CALL_MISSED_TIMEOUT = 60; // 秒
void handlePushMessage(RemoteMessage message) {
DateTime now = DateTime.now();
Duration? diff = now.difference(message.sentTime!);
if (diff.inSeconds > CALL_MISSED_TIMEOUT) {
showMissedCallNotification(message);
return;
}
// 处理正常呼入通话...
}Call Quality Metrics
通话质量指标
Enable with in config:
debug: truedart
// When making a call
call.newInvite(
callerName: 'John',
callerNumber: '+15551234567',
destinationNumber: '+15559876543',
clientState: 'state',
debug: true,
);
// Listen for quality updates
call.onCallQualityChange = (CallQualityMetrics metrics) {
print('MOS: ${metrics.mos}');
print('Jitter: ${metrics.jitter * 1000} ms');
print('RTT: ${metrics.rtt * 1000} ms');
print('Quality: ${metrics.quality}'); // excellent, good, fair, poor, bad
};| Quality Level | MOS Range |
|---|---|
| excellent | > 4.2 |
| good | 4.1 - 4.2 |
| fair | 3.7 - 4.0 |
| poor | 3.1 - 3.6 |
| bad | ≤ 3.0 |
在配置中设置以启用:
debug: truedart
// 发起通话时
call.newInvite(
callerName: 'John',
callerNumber: '+15551234567',
destinationNumber: '+15559876543',
clientState: 'state',
debug: true,
);
// 监听质量更新
call.onCallQualityChange = (CallQualityMetrics metrics) {
print('MOS: ${metrics.mos}');
print('抖动: ${metrics.jitter * 1000} ms');
print('往返延迟: ${metrics.rtt * 1000} ms');
print('质量等级: ${metrics.quality}'); // excellent、good、fair、poor、bad
};| 质量等级 | MOS范围 |
|---|---|
| excellent | > 4.2 |
| good | 4.1 - 4.2 |
| fair | 3.7 - 4.0 |
| poor | 3.1 - 3.6 |
| bad | ≤ 3.0 |
AI Agent Integration
AI Agent集成
Connect to a Telnyx Voice AI Agent:
连接到Telnyx语音AI Agent:
1. Anonymous Login
1. 匿名登录
dart
try {
await telnyxClient.anonymousLogin(
targetId: 'your_ai_assistant_id',
targetType: 'ai_assistant', // Default
targetVersionId: 'optional_version_id', // Optional
);
} catch (e) {
print('Login failed: $e');
}dart
try {
await telnyxClient.anonymousLogin(
targetId: 'your_ai_assistant_id',
targetType: 'ai_assistant', // 默认值
targetVersionId: 'optional_version_id', // 可选
);
} catch (e) {
print('登录失败: $e');
}2. Start Conversation
2. 开始对话
dart
telnyxClient.newInvite(
'User Name',
'+15551234567',
'', // Destination ignored for AI Agent
'state',
customHeaders: {
'X-Account-Number': '123', // Maps to {{account_number}}
'X-User-Tier': 'premium', // Maps to {{user_tier}}
},
);dart
telnyxClient.newInvite(
'User Name',
'+15551234567',
'', // AI Agent场景下忽略被叫号码
'state',
customHeaders: {
'X-Account-Number': '123', // 映射到{{account_number}}
'X-User-Tier': 'premium', // 映射到{{user_tier}}
},
);3. Receive Transcripts
3. 接收转录文本
dart
telnyxClient.onTranscriptUpdate = (List<TranscriptItem> transcript) {
for (var item in transcript) {
print('${item.role}: ${item.content}');
// role: 'user' or 'assistant'
// content: transcribed text
// timestamp: when received
}
};
// Get current transcript anytime
List<TranscriptItem> current = telnyxClient.transcript;
// Clear transcript
telnyxClient.clearTranscript();dart
telnyxClient.onTranscriptUpdate = (List<TranscriptItem> transcript) {
for (var item in transcript) {
print('${item.role}: ${item.content}');
// role: 'user'或'assistant'
// content: 转录文本
// timestamp: 接收时间
}
};
// 随时获取当前转录文本
List<TranscriptItem> current = telnyxClient.transcript;
// 清空转录文本
telnyxClient.clearTranscript();4. Send Text to AI Agent
4. 向AI Agent发送文本消息
dart
Call? activeCall = telnyxClient.calls.values.firstOrNull;
if (activeCall != null) {
activeCall.sendConversationMessage(
'Hello, I need help with my account'
);
}dart
Call? activeCall = telnyxClient.calls.values.firstOrNull;
if (activeCall != null) {
activeCall.sendConversationMessage(
'您好,我需要账户相关帮助'
);
}Custom Logging
自定义日志
dart
class MyCustomLogger extends CustomLogger {
log(LogLevel level, String message) {
print('[$level] $message');
// Send to analytics, file, server, etc.
}
}
final config = CredentialConfig(
// ... other config
logLevel: LogLevel.debug,
customLogger: MyCustomLogger(),
);dart
class MyCustomLogger extends CustomLogger {
log(LogLevel level, String message) {
print('[$level] $message');
// 发送到分析平台、文件、服务器等
}
}
final config = CredentialConfig(
// ... 其他配置
logLevel: LogLevel.debug,
customLogger: MyCustomLogger(),
);Troubleshooting
故障排除
| Issue | Solution |
|---|---|
| No audio on Android | Check RECORD_AUDIO permission |
| No audio on iOS | Check NSMicrophoneUsageDescription in Info.plist |
| Push not working (debug) | Push only works in release mode |
| Login fails | Verify SIP credentials in Telnyx Portal |
| 10-second timeout | INVITE didn't arrive - check network/push setup |
| sender_id_mismatch | FCM project mismatch between app and server |
| 问题 | 解决方案 |
|---|---|
| Android无声音 | 检查RECORD_AUDIO权限 |
| iOS无声音 | 检查Info.plist中的NSMicrophoneUsageDescription |
| 推送通知在调试模式下不工作 | 推送通知仅在发布模式下生效 |
| 登录失败 | 验证Telnyx门户中的SIP凭证 |
| 10秒超时 | INVITE未到达 - 检查网络/推送配置 |
| sender_id_mismatch | 应用与服务端的FCM项目不匹配 |