音視訊通話
使用者可以在 App 中發起一對一和群內的語音、視訊通話。Flutter SDK 負責通話的全部信令和媒體連線:發起、來電振鈴、接聽、拒絕、結束通話,多臺裝置同時振鈴,斷線後的恢復,連線媒體服務、開啟麥克風和攝像頭;在 App 不在前臺時,還負責把來電報告給系統的來電介面(iOS 的 CallKit、Android 的全屏來電通知)。你的 App 只需要顯示通話介面,用 SDK 提供的元件顯示畫面。
音影片資料由 IM 服務提供的媒體服務承載,你不需要部署或配置任何媒體伺服器,也不需要接入其他音影片 SDK:依賴 deeprespond_im_call 包即可。
import 'package:deeprespond_im_call/deeprespond_im_call.dart';
// 发起一对一视频通话
final call = await im.calls.start(toUser: 'bob', media: CallMedia.video);
// 对方和自己的画面(Widget,放进你的通话界面)
final remote = call.remoteView('bob');
final local = call.localView();
// 挂断
await call.hangup();通話的完整規則(忙線、多臺裝置、群通話的人數、結束原因、計費)見服務端文件音視訊通話。
| 要做什麼 | 看哪裡 |
|---|---|
| 第一次接入通話 | 準備工作 |
| 發起一對一或群通話 | 發起一對一通話、發起群通話 |
| 顯示來電、接聽和拒絕 | 接收來電、接聽與拒絕 |
| 寫通話介面:狀態、畫面、靜音、切換攝像頭 | 顯示通話狀態、顯示畫面、通話中的操作 |
| 結束通話,顯示結束原因 | 結束通話與結束 |
| 群聊頁面上的“正在通話”橫幅 | 群通話橫幅 |
| 斷網、App 被殺後的通話 | 斷線與恢復 |
| 沒有麥克風、攝像頭許可權 | 麥克風和攝像頭 |
| 通話按鈕不顯示或發起被拒 | 開啟音影片、執行策略與套餐 |
準備工作
開啟音影片
應用預設關閉音影片。在執行策略開啟 rtc_enabled;新計費模式下還需當前應用音影片餘額為正。切換前仍受歷史套餐開關限制,應用狀態、共享併發和媒體服務也必須允許。
使用 im.config?.rtcEnabled 控制通話入口,監聽配置變化。餘額和服務狀態可能在顯示按鈕後變化,最終仍以服務端發起/接聽的結果為準:
void updateCallButtons() => setCallButtonsVisible(im.config?.rtcEnabled ?? false);
updateCallButtons();
im.on<ConfigChanged>().listen((_) => updateCallButtons());配置不允許時以 permission_denied(rtc_disabled)拒絕;收到該錯誤後核對應用策略、當前應用餘額、充值審批和切換日,不要只重複嘗試撥號。
新增依賴
dependencies:
deeprespond_im_flutter: ^1.0.0-alpha.4
deeprespond_im_push: ^1.0.0-alpha.4
deeprespond_im_call: ^1.0.0-alpha.4deeprespond_im_call 依賴即生效:通話的媒體連線和畫面元件隨之可用。App 不在前臺時的來電要經推送送達,請先按離線推送接入推送(iOS 的 APNs,Android 的 FCM 和國內廠商通道)。
建立客戶端時給出通話選項
給出 DRClientOptions.call 時,SDK 接入系統的來電介面(iOS 和 Android):
final im = await DRClient.create(DRClientOptions(
appKey: '1575529652#demo',
apiUrl: Uri.parse('https://im.example.com'),
platform: DRFlutterPlatform.instance,
push: const DRPushOptions(
credentials: {PushChannel.apns: 'ios-prod', PushChannel.fcm: 'fcm-global'},
androidChannels: [
AndroidChannel(id: 'im_message', name: '消息', kind: AndroidChannelKind.message),
AndroidChannel(id: 'im_call', name: '来电', kind: AndroidChannelKind.call),
],
),
call: const DRCallOptions(),
));| 選項 | 預設值 | 說明 |
|---|---|---|
useCallKit | true | iOS:接入 CallKit 和 PushKit(VoIP 推送),來電顯示系統的來電介面。在中國大陸 App Store 上架的版本必須設為 false,見中國大陸上架的版本 |
useConnectionService | true | Android:用 Core-Telecom 把通話登記為系統的通話,系統據此處理音訊焦點、藍芽耳機的接聽鍵,運營商來電時可以保持 IM 通話 |
autoBusy | true | 正在打電話(運營商電話或其他 App 的通話)時,自動以忙線拒絕來電 |
ringtone | null | iOS 來電鈴聲的聲音檔名(放在 App 的資源中)。在 AppDelegate 中呼叫了 DRCallKit.setUp() 的,請在那裡給出鈴聲,見下一節 |
不給出 call 選項時,App 仍然可以發起和接聽通話,但 App 不在前臺時沒有系統的來電介面,DRCallUI.of(im) 為 null。
iOS 的配置
在 Xcode 中為 Runner 目標新增以下能力:
- Push Notifications;
- Background Modes:勾選 Voice over IP、Audio, AirPlay, and Picture in Picture、Remote notifications。
在 Info.plist 中寫明麥克風和攝像頭的用途(系統在第一次使用時向使用者顯示):
<key>NSMicrophoneUsageDescription</key>
<string>用于语音和视频通话</string>
<key>NSCameraUsageDescription</key>
<string>用于视频通话</string>
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
<string>audio</string>
<string>remote-notification</string>
</array>在 AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 中同步呼叫 DRCallKit.setUp()。App 被 VoIP 推送在後臺拉起時,系統要求立即向 CallKit 報告來電,這一步要早於 Flutter 引擎和外掛的註冊:
import Flutter
import UIKit
import deeprespond_im_call
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
#if !CN_APP_STORE
// 来电界面上显示的 App 名称默认取 CFBundleDisplayName;ringtone 为 App 资源中的声音文件
DRCallKit.setUp(ringtone: "ringtone.caf")
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}VoIP 推送使用 APNs 憑據:用 .p8 金鑰的憑據可以直接發 VoIP 推送,用 .p12 證書的要求證書中登記了 {Bundle ID}.voip,見推送配置。VoIP 令牌預設用 apns 的憑據登記,需要另一份憑據時在 DRPushOptions.voipCredential 中給出。
中國大陸上架的版本
App Store 不允許在中國大陸上架的 App 啟用 CallKit。在中國大陸 App Store 上架的版本:
- 以
DRCallOptions(useCallKit: false)建立客戶端; - 不呼叫
DRCallKit.setUp()(上面的例子用編譯條件CN_APP_STORE區分,在這個版本的構建配置的 Swift Compiler - Custom Flags 中定義它)。
這時 SDK 不登記 VoIP 令牌,App 不在前臺時的來電以普通的高優先順序通知送達,使用者點選通知進入 App 後,由你顯示來電介面,見點選來電通知。用 --dart-define 區分兩個版本的 Dart 程式碼:
// flutter build ipa --dart-define=CN_APP_STORE=true
const cnAppStore = bool.fromEnvironment('CN_APP_STORE');
final callOptions = DRCallOptions(useCallKit: !cnAppStore);Android 的配置
- 許可權:通話需要的許可權(麥克風、攝像頭、
MANAGE_OWN_CALLS、全屏通知、前臺服務等)已在deeprespond_im_call的清單中宣告,構建時合併進 App,不需要你另外宣告。 - 來電渠道:在
DRPushOptions.androidChannels中給出一個AndroidChannelKind.call的渠道,全屏來電通知使用它;沒有給出時 SDK 自建一個名為“來電”的渠道。來電的鈴聲和振動由這個渠道決定。 - FCM 的後臺處理函式:App 在後臺或被殺時的來電經 FCM 的資料訊息送達,必須按離線推送登記後臺處理函式並在其中呼叫
drFirebaseBackgroundHandler。 - 全屏通知的宣告:Android 14 起,全屏來電通知需要單獨的許可權,在 Google Play 上架的要在 Play Console 中宣告用途,見 Android 14 的全屏通知許可權。
桌面端
macOS 等桌面端可以發起和接聽通話,但沒有系統的來電介面,來電只以 CallIncoming 事件交給 App。macOS 要在 Info.plist 中寫明麥克風和攝像頭的用途,並在 entitlements 中開啟 com.apple.security.device.audio-input 和 com.apple.security.device.camera。
發起一對一通話
start() 指定對方的使用者名稱和媒體型別:CallMedia.audio 語音,CallMedia.video 影片(通話中不能切換)。可以附帶自定義欄位 ext(如業務訂單號),隨來電送達對方。
try {
final call = await im.calls.start(toUser: 'bob', media: CallMedia.video, ext: {'order_id': '8812'});
final info = call.snapshot.call;
if (info.status == 'ended' && info.endReason == 'busy') {
showToast('对方忙线中');
} else {
openCallPage(call);
}
} on DRException catch (e) {
if (e.code == 'call_in_progress') {
showToast('你正在另一个通话中,请先挂断');
} else if (e.code == 'user_blocked') {
showToast('对方拒绝了你的通话');
} else {
rethrow;
}
}- 發起成功後你就進入了通話,SDK 立即連線媒體服務、開啟麥克風(視訊通話還有攝像頭),等待對方接聽;這時可以顯示自己的預覽畫面。
- 對方正在另一個通話中時,
start()照常返回,但返回的通話已經結束(endReason為busy),不會振鈴,也不連線媒體。 - 你自己已在一個通話中時,以
call_in_progress拒絕,details['call_id']為你正在進行的通話。使用者確認後,先結束通話它再發起。 - 與傳送單聊訊息的規則相同:對方把你加入了黑名單時以
user_blocked拒絕,應用開啟了只能給好友發訊息而你們不是好友時以not_friend拒絕,你被全域性禁言單聊時以user_muted拒絕。 - 發起太頻繁時以
rate_limited拒絕,SDK 不自動重試;網路錯誤時 SDK 自動重試,不會重複發起。 - 在 iOS 上接入了 CallKit、在 Android 上開啟了
useConnectionService的,SDK 同時把這個通話登記為系統的外呼。
接下來,對方接聽後通話的 status 變為 active,見顯示通話狀態。
發起群通話
在一個群裡發起群通話,usernames 為要振鈴的群成員;也可以不邀請任何人,等其他成員從群通話橫幅加入。
try {
final call = await im.calls.start(toGroup: groupId, usernames: ['lisi', 'wangwu'], media: CallMedia.audio);
// 没有振铃的人(正在其他通话中、在群里被禁言等)
final notRinging = call.snapshot.call.members.where((m) => m.role == 'invitee' && m.state != 'ringing').map((m) => m.username);
if (notRinging.isNotEmpty) showToast('${notRinging.join('、')} 暂时无法接听');
} on DRException catch (e) {
if (e.code == 'already_exists') {
// 群里已有进行中的群通话:改为加入它
final existing = await im.calls.get('${e.details?['call_id']}');
await existing.join();
} else {
rethrow;
}
}- 發起人必須能在群裡發言(不被群禁言、全員禁言時是群主或管理員)。被邀請的人必須是群成員、能在群裡發言,否則不振鈴;正在其他通話中的人記為忙線(
busy),不振鈴。 - 每個群同時只有一個群通話。群裡已有進行中的群通話時以
already_exists拒絕,details['call_id']為那個通話,請改為加入它。 - 同時在通話中的人數上限為執行配置的
maxGroupCallParticipants(預設 16,含發起人),一次邀請的人數不能超過它減一,超出時 SDK 在本地以local_validation(too_many_invitees)拒絕。 - 群通話不檢查個人黑名單。
接收來電
來電(一對一來電或群通話的邀請)按 App 當時的狀態,以不同的方式送達:
| App 的狀態 | iOS | Android |
|---|---|---|
| 在前臺 | 長連線送達,SDK 發出 CallIncoming,由你顯示 App 內的來電介面 | 同左 |
| 在後臺、鎖屏、被殺 | 接入 CallKit 的:VoIP 推送喚醒 App,顯示系統的來電介面 不接入 CallKit 的(中國大陸版本):普通的高優先順序通知 | FCM:全屏來電通知(鎖屏時直接顯示來電介面) 國內廠商通道:普通的高優先順序通知 |
同一個來電可能經長連線、推送等幾條路先後到達,SDK 按通話 ID 去重,不會報告兩次。
App 在前臺
App 在前臺時,SDK 只發出 CallIncoming 事件,不顯示系統的來電介面,也不播放鈴聲:由你顯示 App 內的來電介面,用你自己的音訊外掛播放鈴聲。
im.on<CallIncoming>().listen((e) {
final call = e.call;
openCallPage(call); // 来电界面:snapshot.inviter 为邀请人,snapshot.group 为群通话的群
// App 不在前台时由系统来电界面负责振铃,这里只在前台播放铃声
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) startRingtone();
// 停止振铃:对方取消、超时、在其他设备上接听或拒绝、本人接听或拒绝
late final StreamSubscription<CallSnapshot> sub;
sub = call.stream.listen((s) {
if (s.ringing) return;
stopRingtone();
if (s.call.status == 'ended' || s.self?.state != 'joined') closeCallPage(call);
unawaited(sub.cancel());
});
});來電介面可以顯示的資訊:
final s = call.snapshot;
final inviter = s.inviter;
final from = (inviter?.nickname.isNotEmpty ?? false) ? inviter!.nickname : inviter?.username;
final avatar = inviter?.avatarUrl;
final groupName = s.group?.name; // 群通话的群名,一对一为 null
final isVideo = s.call.media == 'video'; // 'audio' 或 'video'App 在後臺或被殺
這時 SDK 把來電報告給系統的來電介面,你不需要寫程式碼:
- iOS(接入 CallKit):VoIP 推送喚醒 App,SDK 在原生程式碼中立即向 CallKit 報告來電,系統顯示來電介面(鎖屏時為全屏)。之後 SDK 查詢通話,更新主叫的名稱;通話已經結束、或你已在其他裝置上處理了的,結束這個來電。
- Android(FCM):FCM 的後臺處理函式收到來電,顯示全屏來電通知(鎖屏時直接顯示來電介面,否則為浮動通知),帶“接聽”和“拒絕”按鈕。沒有全屏通知許可權時,顯示為鎖屏上展開、常駐的浮動通知,見 Android 14 的全屏通知許可權。
- App 在後臺、長連線還在時:來電由長連線送達,SDK 同樣報告給系統的來電介面。
主叫取消、振鈴超時、你在其他裝置上接聽或拒絕後,SDK 結束系統介面上的來電。裝置沒有及時收到這些訊息的(如手機離線),在 App 下次開啟、回到前臺、重新連線時清除。
系統來電介面上的操作
使用者在系統來電介面上接聽、拒絕、結束通話時,SDK 已經執行了這個操作,再以 DRCallUI.of(im).actions 告訴你,你據此開啟或關閉通話介面。請在建立客戶端後立即訂閱,以免錯過冷啟動時的操作:
DRCallUI.of(im)?.actions.listen((a) async {
switch (a.action) {
case 'answer': // 在系统来电界面或通知上接听了:SDK 已经接听,打开通话界面
case 'show': // 点击了全屏来电通知:打开来电界面
openCallPage(await im.calls.get(a.callId));
case 'reject':
case 'hangup':
closeCallPageById(a.callId);
case 'open': // 点击了未接来电的通知
openCallHistory();
}
});action 的取值:answer 接聽、reject 拒絕、hangup 結束通話、mute 和 unmute 靜音與取消靜音(iOS CallKit 介面上的操作,SDK 已經開關了麥克風)、show 點選了全屏來電通知、open 點選了未接來電的通知。
- 在 Android 的通知上點選“接聽”會啟動 App,App 開啟後 SDK 接聽;點選“拒絕”時 App 不必啟動,沒有執行的在下次啟動時處理(振鈴超時後服務端也會結束這個來電)。
- App 被殺後由系統來電介面啟動的,SDK 在恢復登入後執行接聽,之後
im.calls.current就是這個通話。
點選來電通知
經普通通知送達的來電(iOS 不接入 CallKit 的版本、Android 的國內廠商通道),使用者點選通知進入 App,你在處理通知的點選中按通話 ID 顯示來電介面:
Future<void> onCallPushOpened(CallPush payload) async {
if (payload.event != 'incoming') return;
final call = await im.calls.get(payload.callId);
if (call.snapshot.ringing) {
openCallPage(call); // 仍在振铃:显示来电界面
} else {
showToast('通话已结束');
}
}
DRPush.of(im)?.onOpened.listen((p) {
if (p is CallPush) unawaited(onCallPushOpened(p));
});來電的其他規則
- 正在振鈴的來電:
im.calls.incoming是此刻正在振鈴的全部來電。使用者可能同時收到多個來電,由他選擇接聽哪一個。 - 離線時的來電:App 開啟、網路恢復後,SDK 查詢正在振鈴的來電,沒有通知過的同樣發出
CallIncoming;同一個來電不會重複發出。 - 振鈴時間:由執行策略
rtc_ring_timeout_seconds決定(預設 60 秒)。到時間沒有接聽,SDK 停止振鈴(快照的ringing變為false),一對一通話以no_answer結束,群通話中你的邀請記為未接聽(missed)。 - 多臺裝置同時振鈴:你在其他裝置上(另一臺手機、電腦)接聽或拒絕後,這裡停止振鈴。在其他裝置上接聽時,
CallChanged事件的change.kind為answered_elsewhere,可以顯示“已在其他裝置接聽”。 - 忙線:你正在 IM 通話中時,新的一對一來電由服務端直接判定為忙線,不會振鈴;群通話的邀請記為忙線。你正在打運營商電話或其他 App 的通話時,SDK 自動以忙線拒絕來電(
DRCallOptions.autoBusy):iOS 能發現運營商電話、FaceTime 和其他接入 CallKit 的 App 的通話;Android 按系統的通話音訊模式判斷,能發現運營商通話和大多數通話 App。
接聽與拒絕
// “接听”按钮
Future<void> onAccept() async {
await call.join();
openCallPage(call);
}
// “拒绝”按钮
Future<void> onDecline() async {
await call.reject();
}join()接聽來電或加入群通話,成功後 SDK 連線媒體服務、開啟麥克風和攝像頭,通話進入active(群通話中第一次有兩個人在通話中時)。reject()拒絕。一對一通話隨之結束(endReason為rejected),群通話中只是你的邀請結束(declined),不影響其他人。你的其他裝置隨之停止振鈴。reject(reason: RejectReason.busy)以忙線拒絕。- 你已在另一個通話中時,
join()以call_in_progress拒絕,details['call_id']為那個通話,請先結束通話它。 - 通話已經結束(對方已取消)時以
call_ended拒絕;你已在其他裝置上接聽時以call_in_progress(joined_on_other_device)拒絕。 - 群通話已滿時以
limit_exceeded(call_full)拒絕。 - 在 App 內的介面上接聽的,SDK 同時告訴系統(iOS 的 CallKit、Android 的系統通話)這個來電已接聽,並關閉來電通知。
顯示通話狀態
通話的全部狀態在控制代碼的快照 call.snapshot 中。call.stream 訂閱時先發出當前快照,之後每次變化發出新快照,用 StreamBuilder 即可更新介面:
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/material.dart';
class CallStatusText extends StatelessWidget {
const CallStatusText({super.key, required this.call});
final CallHandle call;
@override
Widget build(BuildContext context) => StreamBuilder<CallSnapshot>(
stream: call.stream,
initialData: call.snapshot,
builder: (context, snapshot) {
final s = snapshot.data!;
final text = switch (s.call.status) {
'ringing' => s.ringing ? '邀请你通话' : '等待对方接听',
'active' => s.media == 'reconnecting' ? '网络不佳,正在重新连接' : '通话中 ${s.call.durationSeconds} 秒',
_ => '通话已结束',
};
return Text(text);
},
);
}| 快照欄位 | 說明 |
|---|---|
call | 通話,見 Call:狀態 status、成員 members、結束原因 endReason 等 |
self | 你在通話中的狀態:state 為你的成員狀態,joinedOnThisDevice 為你是否就在這臺裝置上通話 |
ringing | 這裡是否正在振鈴 |
media | 這臺裝置的媒體連線:none 沒有 / connecting 正在連線 / connected 已連線 / reconnecting 網路波動,正在重新連線 / disconnected 已斷開 |
local | 你的麥克風和攝像頭是否開啟 |
remote | 其他成員的媒體:每人一項,audio、video 為他的麥克風、攝像頭是否開啟 |
resumable | 見 App 被殺後恢復通話 |
durationSeconds 是服務端給出的時長,只在通話有變化時更新。顯示走秒的計時器請按 answeredAt 自己計算。
需要知道“發生了什麼”(誰加入、誰拒絕、誰離開)時,監聽 CallChanged 事件,change.kind 說明變化:
im.on<CallChanged>().listen((e) {
final change = e.change;
if (change == null) return;
final who = change.usernames.whereType<String>().join('、');
switch (change.kind) {
case 'joined':
showToast('$who 加入了通话');
case 'declined':
showToast('$who 拒绝了邀请');
case 'left':
showToast('$who 离开了通话');
case 'answered_elsewhere':
showToast('已在其他设备接听');
case 'ended':
showToast('通话已结束:${e.call.snapshot.call.endReason}');
}
});change.kind | 說明 |
|---|---|
created | 你在其他裝置上發起了通話,這裡可以顯示“正在通話”並提供結束通話 |
invited | 群通話中有新的邀請 |
joined | 有人接聽或加入 |
answered_elsewhere | 你在另一臺裝置上接聽了 |
declined | 有人拒絕 |
missed | 群通話的邀請超時未接聽 |
left | 有人離開或被移出,reason 為離開原因 |
canceled | 群通話中正在振鈴的邀請被取消 |
ended | 通話結束,reason 為結束原因 |
你自己在這臺裝置上的操作(接聽、拒絕、結束通話)也會發出 CallChanged,change 為 null。
顯示畫面
deeprespond_im_call 為通話控制代碼提供了畫面元件,按成員的使用者名稱顯示:
call.localView():自己的攝像頭畫面(映象顯示);call.remoteView(username):某個成員的畫面。
元件自己跟蹤軌道的變化:成員開啟、關閉攝像頭,加入、離開時自動更新;沒有畫面時顯示 placeholder(預設為空白)。聲音由 SDK 自動播放,不需要元件。
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:deeprespond_im_call/deeprespond_im_call.dart';
import 'package:flutter/material.dart';
/// 一对一视频通话:对方的画面铺满,自己的画面在右上角
class OneToOneVideo extends StatelessWidget {
const OneToOneVideo({super.key, required this.call, required this.peer});
final CallHandle call;
final String peer;
@override
Widget build(BuildContext context) => Stack(children: [
Positioned.fill(
child: call.remoteView(peer, placeholder: (_) => const ColoredBox(color: Colors.black)),
),
Positioned(right: 16, top: 48, width: 120, height: 160, child: call.localView()),
]);
}
/// 群通话:在通话中的每个成员一格
class GroupVideoGrid extends StatelessWidget {
const GroupVideoGrid({super.key, required this.call, required this.me});
final CallHandle call;
final String me;
@override
Widget build(BuildContext context) => StreamBuilder<CallSnapshot>(
stream: call.stream,
initialData: call.snapshot,
builder: (context, snapshot) {
final others = snapshot.data!.call.members
.where((m) => m.state == 'joined' && m.username != null && m.username != me)
.map((m) => m.username!)
.toList();
return GridView.count(crossAxisCount: 2, children: [
call.localView(),
for (final username in others)
call.remoteView(username, fit: BoxFit.contain, placeholder: (_) => Center(child: Text(username))),
]);
},
);
}fit為BoxFit.cover(預設,鋪滿並裁剪)或BoxFit.contain(完整顯示,留黑邊)。- 成員的順序和暱稱取自快照的
call.members。顯示“對方已靜音”“對方關閉了攝像頭”,用快照的remote中每人的audio、video。 - 需要自己控制元件的,可以直接使用
CallVideoView(call: call, local: true)或CallVideoView(call: call, username: 'bob')。 - 語音通話沒有畫面,不需要這些元件。
通話中的操作
靜音與開關攝像頭
// 静音 / 取消静音
await call.setMicrophoneEnabled(!call.snapshot.local.microphone);
// 关闭 / 打开摄像头(只用于视频通话)
await call.setCameraEnabled(!call.snapshot.local.camera);- 其他成員會看到你的狀態變化(他們快照中的
remote)。在 iOS 的 CallKit 介面上靜音,同樣會關閉麥克風。 - 語音通話中開啟攝像頭以
invalid_state(audio_call)拒絕。視訊通話關閉攝像頭後,仍按視訊通話記錄。 - 媒體還沒有連上時以
invalid_state(media_not_connected)拒絕。
切換攝像頭與揚聲器
// 在前置和后置摄像头之间切换
await call.switchCamera();
// 打开扬声器(免提);false 为听筒或耳机
await call.setSpeakerphoneOn(true);兩者只在媒體連上後有效,之前呼叫不起作用。
邀請更多人
群通話中,在通話中的成員可以繼續邀請其他群成員。拒絕過、未接聽、離開過的成員可以再次被邀請,重新振鈴。返回每個人的結果,某個人失敗不影響其他人:
final results = await call.invite(['zhaoliu', 'sunqi']);
for (final r in results) {
if (!r.ok) {
showToast('${r.username} 无法邀请:${r.code}');
} else if (r.status == 'busy') {
showToast('${r.username} 正在通话中');
}
}成功的 status 為 invited(已振鈴)、busy(正在其他通話中,沒有振鈴)或 already_in_call(已在本通話中);失敗的帶 code,如 not_group_member、group_muted、rate_limited(邀請太頻繁)、limit_exceeded(通話已滿)。一對一通話不能邀請他人。
移出成員
客戶端不能把別人移出通話。需要移出時(如處理違規),由你的服務端呼叫通話管理介面;被移出的人快照中通話照常更新,他的媒體連線隨之斷開。
傳送資料訊息
sendData() 透過媒體連線向通話中的其他成員傳送一段資料,適合同步“舉手”、白板筆跡這類與通話同時進行的狀態。資料只發給此刻在通話中的人,不儲存。
import 'dart:convert';
await call.sendData(utf8.encode(jsonEncode({'action': 'raise_hand'})));
call.data.listen((m) {
final payload = jsonDecode(utf8.decode(m.data)) as Map<String, Object?>;
if (payload['action'] == 'raise_hand') showToast('${m.username} 举手了');
});“對方已靜音”等狀態 SDK 已經提供(快照的 remote),不需要用資料訊息同步。
結束通話與結束
// 挂断:接通前为取消,接通后为离开,正在振铃时等同拒绝
await call.hangup();
// 结束群通话:全部成员离开(只有发起人、群主和群管理员可以)
await call.end();- 一對一通話中任一方結束通話,通話結束。群通話中結束通話只是你離開,其他人繼續通話;離開後可以再次
join()加入。 - 群通話的發起人、群主和群管理員可以用
end()結束整個群通話,其他人呼叫以permission_denied(not_host)拒絕。對一對一通話,end()等同結束通話。 - 群通話在通話中的人都離開後自動結束;只剩一人、且沒有正在振鈴的邀請,持續 60 秒後也自動結束。
- 你的任何一臺裝置都可以結束通話。在系統的來電介面、Android 通話中的通知上結束通話,效果相同。
- 結束通話後 SDK 斷開媒體、關閉麥克風和攝像頭,結束系統介面上的通話。
通話結束後,快照的 call.status 為 ended,call.endReason 為結束原因,之後快照不再變化。介面上的提示由你決定,下表僅供參考,完整的說明見結束原因:
endReason | 發起方的提示 | 接聽方的提示 |
|---|---|---|
completed | 通話時長 | 通話時長 |
canceled | 已取消 | 對方已取消 |
rejected | 對方已拒絕 | 已拒絕 |
busy | 對方忙線中 | 未接來電 |
no_answer | 對方無應答 | 未接來電 |
connection_lost | 通話中斷 | 通話中斷 |
| 其他 | 通話已結束 | 通話已結束 |
以後可能增加新的結束原因,遇到不認識的值時請顯示為“通話已結束”。
群通話橫幅
群通話開始、在通話中的人數變化和結束時,群的全部線上成員收到通知,用於在群聊頁面顯示“3 人正在通話”的橫幅和加入入口。SDK 把它記在本地,發出 GroupCallChanged 事件,im.calls.groupCallBanner(groupId) 同步返回這個群當前的橫幅(沒有時為 null)。
App 重新開啟後、或者通知發出時你還不是群成員,本地可能還不知道群裡的通話。開啟一個群的聊天頁面時,請呼叫 im.calls.groupCall(groupId) 查詢一次:
void refreshBanner() {
final banner = im.calls.groupCallBanner(groupId);
if (banner == null || banner.status == 'ended') {
renderBanner(null);
} else {
renderBanner(banner.joinedCount > 1 ? '${banner.joinedCount} 人正在通话' : '等待其他成员加入');
}
}
// 打开群聊页面时
await im.calls.groupCall(groupId).catchError((Object _) => null);
refreshBanner();
final sub = im.on<GroupCallChanged>().where((e) => e.groupId == groupId).listen((_) => refreshBanner());
// 点击横幅加入
Future<void> onJoinFromBanner() async {
final call = await im.calls.groupCall(groupId);
if (call != null) await call.join();
}橫幅的人數可能有短暫的誤差,準確的成員以通話快照為準。離開群聊頁面時取消訂閱(sub.cancel())。
斷線與恢復
心跳和媒體憑據的續期由 SDK 自動進行,你不需要處理。
- 網路波動、切換網路:媒體連線自動重連,期間快照的
media為reconnecting,可以顯示“網路不佳,正在重新連線”。只要在 45 秒內恢復,通話不受影響。 - 斷網超過 45 秒:服務端判定你已掉線。一對一通話以
connection_lost結束;群通話中你離開(connection_lost),網路恢復後可以重新join()。 - 網路恢復、回到前臺後:SDK 查詢你正在進行的通話和正在振鈴的來電,與本地核對:已經結束的通話快照更新為
ended,斷網期間的來電開始振鈴,系統介面上已經失效的來電隨之結束。 - 切到後臺:通話進行中切到後臺不會中斷。Android 上 SDK 在接聽或發起時啟動通話的前臺服務,通知欄中顯示“通話中”和結束通話按鈕;iOS 上由 CallKit 和音訊的後臺模式保持(不接入 CallKit 的版本依靠 Audio 後臺模式)。
App 被殺後恢復通話
使用者在通話中把 App 劃掉(或 App 被系統結束)時,服務端仍認為他在通話中。App 重新開啟、恢復登入後,SDK 查詢到這個通話,im.calls.current 為它,快照的 resumable 為 true,並立即代為傳送心跳保住通話。呼叫 resume() 重新連線媒體、開啟麥克風和攝像頭。請儘快恢復:45 秒內沒有恢復,服務端會判定掉線。
Future<void> checkResumable() async {
final call = im.calls.current;
if (call == null || !call.snapshot.resumable) return;
await call.resume();
openCallPage(call);
}
// 登录、重新连接后,SDK 同步完通话状态时
im.on<SyncCompleted>().where((e) => e.modules.contains(SyncModule.calls)).listen((_) => unawaited(checkResumable()));- 也可以先顯示“點選恢復通話”,由使用者決定。
resumable不為true時呼叫resume()以invalid_state(not_resumable)拒絕。- 接聽後媒體服務連線失敗(快照的
media為disconnected)時,resumable同樣變為true,可以用resume()重試。
麥克風和攝像頭
接通、恢復通話以及 setMicrophoneEnabled(true)、setCameraEnabled(true) 時,SDK 開啟麥克風和攝像頭。還沒有授權的,由系統彈出授權提示。使用者在系統來電介面上接聽時不方便彈出提示,建議在使用者第一次使用通話前(如進入通話設定、第一次發起通話時),用 permission_handler 等外掛先請求麥克風和攝像頭許可權。
開啟裝置失敗時:
- 使用者沒有授權時,以
permission_required拒絕,details['permissions']列出缺少的許可權(microphone、camera); - 裝置不存在或被其他程式佔用時,以
device_error拒絕,details['device']為microphone或camera,details['reason']為not_found或busy。
join() 和 resume() 遇到這兩種錯誤時,通話已經接通,只是沒有開啟該裝置(快照的 local 中為 false)。對方能聽到你的聲音之前,需要你引導使用者授權,再呼叫 setMicrophoneEnabled(true):
try {
await call.join();
} on DRException catch (e) {
if (e.code == 'permission_required') {
showPermissionGuide(e.details?['permissions']); // 引导用户在系统设置中允许麦克风、摄像头
} else if (e.code == 'device_error') {
showToast(e.details?['device'] == 'camera' ? '摄像头不可用' : '麦克风不可用');
} else {
rethrow;
}
}
// 用户授权后
await call.setMicrophoneEnabled(true);發起通話時,SDK 在 start() 返回之後才連線媒體、開啟裝置,許可權問題不會讓 start() 失敗。媒體連上(快照的 media 為 connected)而 local.microphone 仍為 false 時,呼叫 setMicrophoneEnabled(true) 可以得到具體的錯誤。
Android 14 的全屏通知許可權
Android 14 起,targetSdk 34 及以上的 App 顯示全屏通知需要“全屏通知”許可權。Google Play 只對在 Play Console 中宣告核心功能是通話或鬧鐘、並獲得批准的 App 預設授予。沒有這個許可權時,來電在鎖屏上顯示為展開、常駐的浮動通知,不會直接亮屏顯示來電介面。
- 在 Google Play 上架的,請在 Play Console 的“全屏 intent”宣告中說明 App 的通話用途;
- 在 App 中檢查許可權,沒有時引導使用者開啟:
final ui = DRCallUI.of(im);
if (ui != null && !await ui.canUseFullScreenIntent()) {
// 向用户说明:开启后,锁屏时来电可以直接显示来电界面
await ui.openFullScreenIntentSettings();
}canUseFullScreenIntent() 在 Android 13 及以下、iOS 上總是 true。
執行策略與套餐
| 設定 | 影響 |
|---|---|
開啟音視訊通話 rtc_enabled | 關閉時不能發起、接聽、加入通話和邀請他人,以 permission_denied(rtc_disabled)拒絕;進行中的通話不受影響 |
來電振鈴時間 rtc_ring_timeout_seconds | 預設 60 秒 |
群通話人數上限 max_group_call_participants | 含發起人,由套餐決定,0 表示不限 |
通話記錄 rtc_call_record_enabled | 開啟時,通話結束後在會話中寫入一條通話記錄訊息 |
以上見執行策略。此外:
- 切換前套餐禁用影響
rtcEnabled;新模式音影片餘額不足同樣會拒絕新准入,實際規則見音影片餘額。 - 應用同時進行的通話數達到套餐的額度時,發起以
limit_exceeded(app_concurrent_calls)拒絕; - 應用處於只讀狀態(如欠費)時,發起和邀請以
app_unavailable拒絕,進行中的通話不受影響; - 本應用的音影片被平臺暫停時,發起以
permission_denied(rtc_suspended)拒絕;音影片服務暫不可用時為permission_denied(rtc_not_configured)。這兩種情況只在發起時才知道,請按原因提示使用者; - 以
permission_denied(media_provider_unsupported)拒絕時,說明當前版本的 Flutter SDK 不支援平臺為應用提供的媒體服務,請升級 SDK,並提示使用者“當前版本不支援通話”。
通話記錄
通話歷史
im.calls.history() 分頁返回你參與過的通話(包括未接聽、拒絕的),按時間從新到舊,保留 180 天:
final page = await im.calls.history(limit: 20);
for (final c in page.items) {
final target = c.type == 'single' ? c.peer : c.groupId;
print('$target ${c.media} ${c.endReason} ${c.durationSeconds} ${c.createdAt}');
}會話中的通話記錄訊息
通話結束後,服務端在會話中寫入一條型別為 call 的訊息(一對一寫入雙方的單聊會話,群通話寫入群會話)。用顯示輔助 renderCallRecord() 得到“通話時長 01:32”“對方已拒絕”這樣的文字:
import 'package:deeprespond_im_flutter/render.dart';
if (message.type == 'call') {
final text = renderCallRecord(message, self: im.auth.currentUser?.username ?? '');
print(text);
}介面參考
返回 Future 的方法失敗時丟擲 DRException;沒有登入時為 not_signed_in,網路錯誤為 network_error、timeout。錯誤碼見事件與錯誤處理和服務端的錯誤碼。
im.calls.start()
發起一對一通話或群通話。發起成功後你就在通話中,SDK 隨即連線媒體服務。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
toUser | String? | 二選一(命名引數) | 一對一通話的對方使用者名稱 |
toGroup | String? | 二選一(命名引數) | 群通話所在的群 ID |
usernames | List<String>? | 否(命名引數) | 群通話中要振鈴的成員,不超過 maxGroupCallParticipants 減一,不能包含自己 |
media | CallMedia | 是(命名引數) | CallMedia.audio 語音或 CallMedia.video 影片,通話中不能切換 |
ext | Map<String, String>? | 否(命名引數) | 自定義欄位,最多 16 項,JSON 編碼後不超過 1 KB,隨來電送達對方 |
返回值:Future<CallHandle>。一對一通話的對方正在通話中時,返回的通話已經結束(endReason 為 busy)。
可能的錯誤:permission_denied(rtc_disabled 應用沒有開啟音影片;rtc_suspended、rtc_not_configured 音影片暫不可用;media_provider_unsupported 當前版本不支援)、call_in_progress(你已在通話中,details['call_id'] 為那個通話)、already_exists(群裡已有進行中的群通話,details['call_id'] 為那個通話)、user_blocked、not_friend、user_muted、not_group_member、group_muted、group_disabled、not_found(對方或群不存在)、limit_exceeded(app_concurrent_calls)、app_unavailable、rate_limited(call_rate、pair_call_rate、group_call_rate)、invalid_argument(如 self_call 呼叫自己、invalid_ext)、local_validation(缺少引數、同時給出了 toUser 和 toGroup、too_many_invitees)。
im.calls.current
只讀屬性,CallHandle?:你正在進行的通話(你的成員狀態為 joined),包括在其他裝置上進行的(self.joinedOnThisDevice 為 false);沒有時為 null。
im.calls.incoming
只讀屬性,List<CallHandle>:此刻正在振鈴的來電。
im.calls.get()
按通話 ID 取得通話的控制代碼,本地已有的直接返回,否則向服務端查詢。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
callId | String | 是 | 通話 ID |
返回值:Future<CallHandle>。
可能的錯誤:not_found(通話不存在,或你看不到它:一對一通話只有雙方能查到,群通話是群的當前成員能查到)。
im.calls.history()
分頁返回你參與過的通話,按時間從新到舊。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
cursor | String? | 否(命名引數) | 上一頁返回的 nextCursor |
limit | int? | 否(命名引數) | 每頁條數,預設 20,最多 50 |
返回值:Future<DRPage<CallSummary>>。
可能的錯誤:rate_limited(query_rate)。
im.calls.groupCall()
查詢一個群當前進行中的群通話,同時更新本地的群通話橫幅。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
groupId | String | 是 | 群 ID |
返回值:Future<CallHandle?>,沒有進行中的群通話時為 null。
可能的錯誤:not_found(群不存在)、not_group_member、rate_limited。
im.calls.groupCallBanner()
同步返回本地記下的這個群的群通話橫幅。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
groupId | String | 是 | 群 ID |
返回值:GroupCallBanner?。null 表示沒有進行中的群通話,或本地還不知道(App 重新開啟後、沒有查詢過的群),見群通話橫幅。
call.snapshot
只讀屬性,CallSnapshot:通話當前的快照(不可變,變化時整體替換),見 CallSnapshot。
call.stream
只讀屬性,Stream<CallSnapshot>:訂閱時先發出當前快照,之後每次變化發出新快照;通話結束後不再變化。
call.tracks
只讀屬性,Stream<CallTrackEvent>:媒體軌道的增減,包括你自己的(local 為 true)和其他成員的。畫面元件已經處理了軌道,通常不需要訂閱。
call.data
只讀屬性,Stream<CallDataMessage>:收到其他成員經 sendData() 發來的資料。
call.join()
接聽來電、接聽群通話的邀請、從橫幅加入群通話,或離開群通話後重新加入。
返回值:Future<void>,接聽成功並連上媒體後完成。
可能的錯誤:call_in_progress(in_other_call 你在另一個通話中,details['call_id'] 為那個通話;joined_on_other_device 你已在其他裝置上接聽)、call_ended(通話已經結束)、limit_exceeded(call_full 群通話已滿;call_member_limit 群通話累計涉及的成員已達上限)、version_conflict(你當前的狀態不能接聽,如已經拒絕過的一對一來電)、permission_denied(rtc_disabled 等)、not_group_member、group_muted、user_muted、permission_required、device_error(這兩種情況已經接聽,見麥克風和攝像頭)、network_error(media_connect_failed,已經接聽而媒體服務連線失敗,見 App 被殺後恢復通話)、rate_limited(signal_rate,等待不超過 5 秒的 SDK 會自動重試一次)。
call.reject()
拒絕來電或群通話的邀請。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
reason | RejectReason | 否(命名引數) | RejectReason.declined 拒絕(預設),RejectReason.busy 以忙線拒絕 |
返回值:Future<void>。重複拒絕視為成功。
可能的錯誤:version_conflict(你已經接聽,或沒有被邀請)、rate_limited。
call.hangup()
結束通話:接通前的發起方為取消,在通話中為離開,正在振鈴時等同拒絕。不論成功與否,都會斷開這臺裝置的媒體。
返回值:Future<void>。結束通話已結束的通話視為成功。
可能的錯誤:rate_limited、network_error 等網路錯誤。
call.end()
結束整個群通話,全部成員離開,正在振鈴的邀請取消。只有發起人、群主和群管理員可以;對一對一通話等同結束通話。
返回值:Future<void>。
可能的錯誤:permission_denied(not_host)、rate_limited。
call.invite()
在群通話中邀請更多群成員。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
usernames | List<String> | 是 | 要邀請的群成員,1 到 1000 個,不能包含自己 |
返回值:Future<List<CallInviteResult>>,每個人的結果,見邀請更多人。
可能的錯誤:invalid_argument(group_call_only 一對一通話不能邀請;invalid_usernames;too_many_invitees)、permission_denied(not_in_call 你不在通話中;rtc_disabled)、call_ended、app_unavailable、local_validation(usernames 為空)。
call.resume()
App 被殺後重新開啟、或媒體連線失敗後,在這臺裝置上恢復通話:重新連線媒體、開啟麥克風和攝像頭。只在快照的 resumable 為 true 時可用。
返回值:Future<void>。
可能的錯誤:invalid_state(not_resumable)、call_ended、version_conflict(你已不在通話中)、permission_required、device_error、network_error。
call.setMicrophoneEnabled()
開啟或關閉麥克風(靜音)。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
on | bool | 是 | true 開啟,false 關閉 |
返回值:Future<void>。
可能的錯誤:invalid_state(media_not_connected 媒體還沒有連上)、permission_required、device_error。
call.setCameraEnabled()
開啟或關閉攝像頭,只用於視訊通話。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
on | bool | 是 | true 開啟,false 關閉 |
返回值:Future<void>。
可能的錯誤:invalid_state(audio_call 語音通話不能開啟攝像頭;media_not_connected)、permission_required、device_error。
call.sendData()
透過媒體連線向通話中的其他成員傳送資料。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
data | List<int> | 是 | 資料,字串請先用 utf8.encode() 編碼 |
返回值:Future<void>。
可能的錯誤:invalid_state(media_not_connected)。
call.localView()、call.remoteView()
deeprespond_im_call 提供的擴充套件方法,返回顯示畫面的 Widget。
| 方法 | 引數 | 說明 |
|---|---|---|
localView({BoxFit fit = BoxFit.cover, WidgetBuilder? placeholder}) | fit:縮放方式;placeholder:沒有畫面時顯示的內容 | 自己的攝像頭畫面,映象顯示 |
remoteView(String username, {BoxFit fit = BoxFit.cover, WidgetBuilder? placeholder}) | username:成員的使用者名稱;其他同上 | 某個成員的畫面 |
返回值:Widget(CallVideoView)。fit 只區分 BoxFit.contain 和其他(按 cover 處理)。
call.switchCamera()
deeprespond_im_call 提供的擴充套件方法:在前置和後置攝像頭之間切換。沒有開啟攝像頭時什麼也不做。
返回值:Future<void>。
call.setSpeakerphoneOn()
deeprespond_im_call 提供的擴充套件方法:開啟或關閉揚聲器(擴音)。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
on | bool | 是 | true 使用揚聲器,false 使用聽筒或耳機 |
返回值:Future<void>。
DRCallUI.of()
取得客戶端的系統來電介面。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
client | DRClient | 是 | 客戶端 |
返回值:DRCallUI?,建立客戶端時沒有給出 call 選項、或不是 iOS 和 Android 時為 null。
callUI.actions
只讀屬性,Stream<CallAction>:使用者在系統來電介面、來電通知上的操作。SDK 已經執行了接聽、拒絕、結束通話,你據此開啟或關閉通話介面,見系統來電介面上的操作。
callUI.canUseFullScreenIntent()
Android 14 起,App 能否顯示全屏通知。
返回值:Future<bool>。Android 13 及以下、iOS 總是 true。
callUI.openFullScreenIntentSettings()
開啟系統設定中本 App 的全屏通知許可權頁面(Android 13 及以下開啟通知設定頁)。
返回值:Future<void>。
DRCallKit.setUp()(iOS 原生)
deeprespond_im_call 的 Swift API,在 AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 中同步呼叫,儘早建立 PushKit 和 CallKit 的物件。中國大陸 App Store 上架的版本不要呼叫。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
appName | String? | 否 | 來電介面上顯示的 App 名稱,預設取 CFBundleDisplayName |
ringtone | String? | 否 | 鈴聲,App 資源中的聲音檔名 |
事件
以下事件用 im.on<T>() 監聽,見事件與錯誤處理。
| 事件 | 欄位 | 說明 |
|---|---|---|
CallIncoming | call(CallHandle) | 收到來電,開始振鈴。App 在前臺時由你顯示來電介面 |
CallChanged | call(CallHandle);change(CallChange?) | 通話有變化,見顯示通話狀態。本裝置自己的操作 change 為 null |
GroupCallChanged | groupId(String);banner(GroupCallBanner?) | 某個群的群通話橫幅有變化;群裡沒有通話時 banner 為 null |
資料結構
CallSnapshot
控制代碼的快照。
| 欄位 | 型別 | 說明 |
|---|---|---|
call | Call | 通話 |
self | CallSelf? | 你在通話中的狀態:state 為你的成員狀態(不是成員時為 null),joinedOnThisDevice 為你是否就在這臺裝置上通話 |
inviter | CallInviter? | 來電的邀請人:username、nickname、avatarUrl,只在來電中有 |
group | CallGroup? | 群通話所在的群:groupId、name、avatarUrl;一對一為 null,只在來電中有 |
media | String | 這臺裝置的媒體連線:none、connecting、connected、reconnecting、disconnected |
local | CallLocalMedia | 你的麥克風、攝像頭是否開啟:microphone、camera |
remote | List<CallRemoteMedia> | 其他成員的媒體,每人一項:username、mediaUid、audio、video |
ringing | bool | 這裡是否正在振鈴,到振鈴截止時間自動變為 false |
resumable | bool | 你在這臺裝置上的通話沒有媒體連線,可以呼叫 resume() 恢復 |
Call
通話,原始資料在 raw 中,欄位見服務端的通話物件。
| 欄位 | 型別 | 說明 |
|---|---|---|
callId | String | 通話 ID |
type | String | single 一對一 / group 群通話 |
groupId | String? | 群通話所在的群,一對一為 null |
media | String | audio 語音 / video 影片 |
status | String | ringing 等待接通 / active 已接通 / ended 已結束,只按這個順序前進 |
initiator | String? | 發起人的使用者名稱 |
maxParticipants | int | 同時在通話中的人數上限,發起時確定 |
joinedCount | int | 此刻在通話中的人數 |
members | List<CallMember> | 成員;通話歷史中為空 |
createdAt | DateTime? | 發起時間 |
answeredAt | DateTime? | 接通時間,沒有接通為 null |
endedAt | DateTime? | 結束時間 |
endReason | String? | 結束原因,見結束通話與結束 |
endedBy | String? | 結束通話的使用者;服務端、平臺和系統結束的為 null |
durationSeconds | int | 接通到結束的秒數,未接通為 0 |
version | int | 版本號,每次變化加一 |
ext | Map<String, String>? | 發起時的自定義欄位 |
self | CallSelf? | 你在這個通話中的狀態(通話歷史、查詢的結果中有) |
member(username) 返回某個成員,沒有時為 null。
CallMember
通話的成員。
| 欄位 | 型別 | 說明 |
|---|---|---|
username | String? | 使用者名稱,已刪除的使用者為 null |
role | String | initiator 發起人 / invitee 被邀請的人 / joiner 自己加入群通話的人 |
state | String | ringing 正在振鈴 / joined 在通話中 / left 已離開 / declined 已拒絕 / busy 忙線 / missed 未接聽 / canceled 邀請被取消 |
mediaUid | int | 成員在這個通話中的編號 |
invitedBy | String? | 最近一次邀請他的人 |
ringExpiresAt | DateTime? | 振鈴截止時間 |
joinedAt | DateTime? | 最近一次加入的時間 |
leftAt | DateTime? | 最近一次離開的時間 |
leaveReason | String? | 最近一次離開的原因,見離開原因 |
CallSummary
im.calls.history() 的一項:Call(不含 members),另有:
| 欄位 | 型別 | 說明 |
|---|---|---|
peer | String? | 一對一通話的對方使用者名稱,群通話為 null |
CallChange
CallChanged 事件中的變化。
| 欄位 | 型別 | 說明 |
|---|---|---|
kind | String | 變化的型別,見顯示通話狀態 |
usernames | List<String?> | 狀態變化的成員 |
actor | String? | 操作人,你的服務端、平臺和系統操作時為 null |
reason | String? | 拒絕的原因、離開原因或結束原因 |
CallInviteResult
invite() 中一個人的結果。
| 欄位 | 型別 | 說明 |
|---|---|---|
username | String | 使用者名稱 |
ok | bool | 是否成功(沒有 code) |
status | String? | 成功時:invited、busy、already_in_call |
code | String? | 失敗時的錯誤碼 |
message | String? | 失敗時的說明 |
details | Map<String, Object?>? | 失敗的詳細原因 |
GroupCallBanner
群通話橫幅。
| 欄位 | 型別 | 說明 |
|---|---|---|
groupId | String | 群 ID |
callId | String | 通話 ID |
media | String | audio 語音 / video 影片 |
status | String | 通話狀態:ringing、active、ended |
joinedCount | int | 在通話中的人數,可能有短暫的誤差 |
initiator | String? | 發起人 |
version | int | 版本號 |
CallTrackEvent
| 欄位 | 型別 | 說明 |
|---|---|---|
added | bool | true 為新增,false 為移除 |
username | String? | 成員的使用者名稱 |
mediaUid | int | 成員在通話中的編號 |
kind | String | audio 或 video |
local | bool | 是否為你自己的軌道 |
track | Object | 媒體軌道物件,交給畫面元件使用 |
CallDataMessage
| 欄位 | 型別 | 說明 |
|---|---|---|
username | String? | 傳送者的使用者名稱 |
mediaUid | int | 傳送者在通話中的編號 |
data | List<int> | 資料 |
CallAction
| 欄位 | 型別 | 說明 |
|---|---|---|
action | String | answer、reject、hangup、mute、unmute、show、open,見系統來電介面上的操作 |
callId | String | 通話 ID |
DRCallOptions
| 欄位 | 型別 | 說明 |
|---|---|---|
useCallKit | bool | iOS 接入 CallKit 和 PushKit,預設 true |
useConnectionService | bool | Android 用 Core-Telecom 登記系統通話,預設 true |
autoBusy | bool | 正在打電話時自動以忙線拒絕來電,預設 true |
ringtone | String? | iOS 來電鈴聲的聲音檔名 |
獨立頻道
本頁的呼叫、振鈴和接聽屬於原通話。無需 IM 群的會議 / 語音房使用獨立 RTC 頻道,不自動使用來電 UI 或通知;媒體佔用與原通話共享,計費口徑分別定義。
