音视频通话
用户可以在 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();通话的完整规则(忙线、多台设备、群通话的人数、结束原因、计费)见服务端文档音视频通话。
准备工作
开启音视频
应用默认不开启音视频。在控制台的运行策略中开启“开启音视频通话”(rtc_enabled),并且应用的套餐包含音视频后,用户才能发起和接听通话。
客户端的运行配置 im.config?.rtcEnabled 在策略和套餐都允许时为 true,请据此显示通话的入口;策略或套餐变化时运行配置随之更新,发出 ConfigChanged:
void updateCallButtons() => setCallButtonsVisible(im.config?.rtcEnabled ?? false);
updateCallButtons();
im.on<ConfigChanged>().listen((_) => updateCallButtons());rtcEnabled 为 false 时,im.calls.start() 以 permission_denied(rtc_disabled)拒绝。
添加依赖
dependencies:
deeprespond_im: ^2.0.0
deeprespond_im_flutter: ^2.0.0
deeprespond_im_push: ^2.0.0
deeprespond_im_call: ^2.0.0deeprespond_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/deeprespond_im.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/deeprespond_im.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 | 默认 16,含发起人 |
通话记录 rtc_call_record_enabled | 开启时,通话结束后在会话中写入一条通话记录消息 |
以上见运行策略。此外:
- 应用的套餐不包含音视频时,
rtcEnabled为false; - 应用同时进行的通话数达到套餐的额度时,发起以
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/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 到 31 个,不能包含自己 |
返回值: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 或通知;媒体占用与原通话共享,计费口径分别定义。
