telnyx-webrtc-client-flutter

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Telnyx 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 the
telnyx-webrtc-*
skill in your server language plugin (e.g.,
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.yaml
:
yaml
dependencies:
  telnyx_webrtc: ^latest_version
Then run:
bash
flutter pub get
pubspec.yaml
中添加依赖:
yaml
dependencies:
  telnyx_webrtc: ^latest_version
然后执行:
bash
flutter pub get

Platform Configuration

平台配置

Android

Android

Add to
AndroidManifest.xml
:
xml
<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.xml
中添加权限:
xml
<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.plist
:
xml
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) needs microphone access for calls</string>

Info.plist
中添加:
xml
<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

配置选项

ParameterTypeDescription
sipUser
/
sipToken
StringCredentials from Telnyx Portal
sipCallerIDName
StringCaller ID name displayed to recipients
sipCallerIDNumber
StringCaller ID number
notificationToken
String?FCM (Android) or APNS (iOS) token
autoReconnect
boolAuto-retry login on failure
debug
boolEnable call quality metrics
logLevel
LogLevelnone, error, warning, debug, info, all
ringTonePath
String?Custom ringtone asset path
ringbackPath
String?Custom ringback tone asset path

参数类型说明
sipUser
/
sipToken
String来自Telnyx门户的凭证
sipCallerIDName
String显示给通话接收方的主叫ID名称
sipCallerIDNumber
String主叫ID号码
notificationToken
String?FCM(Android)或APNS(iOS)令牌
autoReconnect
bool登录失败时自动重试
debug
bool启用通话质量指标
logLevel
LogLevelnone、error、warning、debug、info、all
ringTonePath
String?自定义铃声资源路径
ringbackPath
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
debug: true
in config:
dart
// 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 LevelMOS Range
excellent> 4.2
good4.1 - 4.2
fair3.7 - 4.0
poor3.1 - 3.6
bad≤ 3.0

在配置中设置
debug: true
以启用:
dart
// 发起通话时
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
good4.1 - 4.2
fair3.7 - 4.0
poor3.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

故障排除

IssueSolution
No audio on AndroidCheck RECORD_AUDIO permission
No audio on iOSCheck NSMicrophoneUsageDescription in Info.plist
Push not working (debug)Push only works in release mode
Login failsVerify SIP credentials in Telnyx Portal
10-second timeoutINVITE didn't arrive - check network/push setup
sender_id_mismatchFCM project mismatch between app and server
问题解决方案
Android无声音检查RECORD_AUDIO权限
iOS无声音检查Info.plist中的NSMicrophoneUsageDescription
推送通知在调试模式下不工作推送通知仅在发布模式下生效
登录失败验证Telnyx门户中的SIP凭证
10秒超时INVITE未到达 - 检查网络/推送配置
sender_id_mismatch应用与服务端的FCM项目不匹配

Resources

资源