实现单呼转群聊
更新时间: 2026/06/05 10:19:48
自 V4.7.0 版本起,呼叫组件支持单呼转群呼功能。本文主要介绍如何通过集成呼叫组件(无 UI),在已接通的 1v1 通话中继续邀请其他用户加入当前通话,实现单呼平滑升级为多人通话。无 UI 接入方式下,通话界面由业务侧根据自身产品形态自行实现。
该功能并不是原有的群呼能力。单呼转群呼基于当前 1v1 信令房间和 RTC 房间,通过 NECallEngine.inviteMembers 邀请新成员加入,不需要重新发起群呼。
注意事项
- 呼叫组件基于网易云信 NIM SDK 和 NERTC SDK 实现通话呼叫。
- 针对呼叫组件中的回调信息,开发者要做好相应回调数据的上报及存储,以便于后期上线之后排查问题。
- 参与单呼转群呼的端都需要使用支持该能力的新版本 SDK,并开启
enableSingleToGroupCall。任一端未开启或版本不支持时,canInviteMembers返回false。 - 单呼转群呼功能仅允许在 1v1 呼叫已接通后发起。初始被叫未接听前,不支持邀请其他用户。
- 多人通话人数上限为 10 人。SDK 会按当前已加入成员、待接听成员和本次邀请账号数做校验。
- 邀请发送成功只表示邀请信令已发出,不表示对方已接听或已加入通话。被邀请方真正成为通话成员以
onCallMembersChanged中成员状态变为NECallMemberState.JOINED为准。 - 通话一旦进入多人模式,本次通话内会保持多人模式;即使后续只剩 2 人,也不会恢复 1v1 大画面和音视频切换能力。
- 进入多人模式后,不支持通话中音视频类型切换,业务侧应隐藏或禁用切换入口。
- 单呼转群呼话单由云信服务端生成,需要联系云信技术支持开通。开启
enableSingleToGroupCall后,本地 SDK 默认 1v1 话单发送会被跳过;目前暂不支持通过setCallRecordProvider自行实现单呼转群呼话单。如需自定义话单,请联系云信技术支持。
基本概念
account_id:account_id是 IM 账号 ID,用于登录 IM。注册 IM 账号时,IM 服务器会返回对应的账号 ID(account_id)和密钥(Token),应用客户端需要负责保存 account_id 和 IM Token 的映射关系。Token:呼叫组件中涉及的 Token 包括 IM Token,用于登录 IM 时进行 IM 账号鉴权。应用服务器调用 IM 服务器的 注册账号 API,获取的 IM Token。RTC uid:用户加入 RTC 房间时使用的 ID,由呼叫组件在通话过程中维护,业务侧通常不需要在单呼转群呼接入中单独处理。callId:CallKit 业务通话 ID,用于回调、日志和邀请批次关联,而并非 NIM 信令房间 ID。channelId:NIM 信令房间 ID。业务通常不需要直接处理,可用于日志和问题排查。
开发环境
| 环境要求 | 说明 |
|---|---|
| Android Studio 版本 | Android Studio 5.0 及以上版本。 |
| Android API 版本 | Level 为 21 及以上版本。 |
| Android SDK 版本 | Android SDK 31、Android SDK Platform-Tools 31.x.x 及以上版本。 |
| Gradle 及所需的依赖库 | 在 Gradle Services 页面下载对应版本的 Gradle 及所需的依赖库。
|
| kotlin | 1.6.21 及以上版本。 |
| CPU 架构 | ARM 64、ARMV7。 |
| IDE | Android Studio。 |
| 其他 | 依赖 Androidx,不支持 support 库。 Android 系统 5.0 及以上版本的真机。 |
准备工作
根据本文操作前,请确保您已经完成了以下设置:
实现单呼转群呼
-
初始化呼叫组件。
在初始化时需要在
NESetupConfig中开启enableSingleToGroupCall,并注册NECallEngineDelegate。javaNESetupConfig config = new NESetupConfig.Builder(appKey) .enableSingleToGroupCall(true) .build(); NECallEngine.sharedInstance().setup(context.getApplicationContext(), config); NECallEngine.sharedInstance().addCallDelegate(callDelegate); -
发起 1v1 呼叫。
javaNECallParam param = new NECallParam.Builder("callee_account_id") .callType(NECallType.VIDEO) .extraInfo("business attachment") .globalExtraCopy("business global extra") .build(); NECallEngine.sharedInstance() .call( param, result -> { if (result == null || !result.isSuccessful()) { showToast(result != null ? result.msg : "呼叫失败"); return; } NECallInfo callInfo = result.data; log("call sent, callId: " + callInfo.callId); }); -
处理普通来电和多人邀请来电。
收到邀请时,通过
NEInviteInfo.multiCallInvite区分普通 1v1 来电和多人邀请。javaprivate final NECallEngineDelegate callDelegate = new NECallEngineDelegateAbs() { @Override public void onReceiveInvited(NEInviteInfo info) { if (info.multiCallInvite) { showMultiInvitePage(info.callerAccId, info.callType, info.extraInfo); } else { showOneToOneIncomingPage(info.callerAccId, info.callType, info.extraInfo); } } };接听javaNECallEngine.sharedInstance() .accept( result -> { if (result == null || !result.isSuccessful()) { showToast(result != null ? result.msg : "接听失败"); return; } if (currentIncomingIsMultiInvite) { enterMultiCallUi(); refreshMembers(NECallEngine.sharedInstance().currentMembers()); } });拒绝、取消或挂断当前呼叫javaNEHangupParam param = new NEHangupParam("business hangup extra"); NECallEngine.sharedInstance().hangup(param, result -> { if (result == null || !result.isSuccessful()) { showToast(result != null ? result.msg : "挂断失败"); } }); -
展示邀请入口。
业务侧应在 1v1 接通后调用
canInviteMembers判断是否允许展示邀请入口。javaprivate void refreshInviteButton() { boolean canInvite = NECallEngine.sharedInstance().canInviteMembers(); inviteButton.setVisibility(canInvite ? View.VISIBLE : View.GONE); } @Override public void onCallConnected(NECallInfo info) { // 这里只代表 1v1 通话已建立,不用于判断单呼转群呼是否应切多人 UI。 refreshInviteButton(); }canInviteMembers会综合当前通话状态、是否开启单呼转群呼、对端能力、当前通话信息等条件。业务侧不要只根据本地开关决定是否展示入口。 -
发起通话中邀请。
当用户在业务 UI 中选择成员后,调用
inviteMembers。javaprivate void inviteUsers(List<String> userIds) { if (!NECallEngine.sharedInstance().canInviteMembers()) { showToast("当前通话不支持邀请成员"); return; } NECallPushConfig pushConfig = new NECallPushConfig(true, "多人通话邀请", "邀请你加入多人通话", null); NECallInviteParam param = new NECallInviteParam.Builder(userIds) .attachment("invite attachment") .globalExtra("invite global extra") .pushConfig(pushConfig) .maxMembers(10) .build(); NECallEngine.sharedInstance() .inviteMembers( param, result -> { if (result == null || !result.isSuccessful()) { showToast(result != null ? result.msg : "邀请失败"); return; } NECallInviteResult inviteResult = result.data; int successCount = 0; int failedCount = 0; for (NECallInviteItemResult item : inviteResult.results) { if (item.success) { successCount++; } else { failedCount++; log("invite failed, user: " + item.inviteeUserID + ", code: " + item.code + ", msg: " + item.message); } } if (successCount > 0 && failedCount == 0) { showToast("邀请已发送"); } else if (successCount > 0) { showToast("部分邀请已发送,部分失败"); } else { showToast("邀请失败"); } }); }NECallInviteParam字段说明:字段 说明 userIDs被邀请账号列表。SDK 会自动忽略无效账号、本端账号、已在通话成员和仍处于待接听的成员。 attachment业务透传扩展,会透传到被邀请端 NEInviteInfo.extraInfo。globalExtra全局抄送扩展。 pushConfig多人邀请通知和离线推送配置。 maxMembers本次通话人数上限。不设置或小于等于 0 时使用默认值 10。 -
切换多人 UI。
不同角色切换多人 UI 的时机不同:
- 原 1v1 通话方:收到
onCallModeChanged且newMode == NECallMode.MULTI时,切换多人布局。 - 第三方被邀请人:
onReceiveInvited中info.multiCallInvite == true表示这是多人邀请;用户点击接听后,accept成功即可切换多人布局。 - 兜底刷新:如果先收到
onCallMembersChanged,且isInMultiCall() == true或成员快照中出现NECallMemberState.WAITING,也可以先切换多人布局再刷新成员。
切换多人 UI 的逻辑建议做成幂等,避免多个回调连续触发时重复创建页面。
onReceiveInvited(info): if info.multiCallInvite == true: 展示多人邀请来电页 记录当前来电为多人邀请 accept completion(result): if result 成功 且 当前来电是多人邀请: 切换到多人 UI 先用 currentMembers 渲染已有成员 等待 onCallMembersChanged 补齐成员列表和媒体状态 onCallModeChanged(info): if info.newMode == NECallMode.MULTI: 切换到多人布局 隐藏或禁用音视频类型切换入口NECallModeChangeInfo字段说明:字段 说明 oldMode变化前通话模式。 newMode变化后通话模式。 memberCount当前有效成员数量,只统计已加入成员。 hasEverMulti本次通话是否已经进入过多人模式;成功发起多人邀请并出现待接听成员后即为 true。 - 原 1v1 通话方:收到
-
监听成员变化并刷新 UI。
邀请发出后,业务侧应根据
onCallMembersChanged刷新完整成员列表。onCallMembersChanged不作为切换多人 UI 的唯一入口。第三方被邀请人刚接听成功时,成员快照可能先只有自己,随后才逐步补齐原 1v1 双方;因此应先切换多人 UI,再用该回调刷新宫格内容。onCallMembersChanged(info): members = info.members if 当前还未进入多人 UI 且 (isInMultiCall() == true 或 members 中存在 WAITING 成员): 切换到多人 UI 按 members 重建或刷新多人宫格: - WAITING 成员:展示头像 / 昵称 / 等待接听占位 - JOINED 且 videoAvailable == true 且 videoMuted == false:展示视频画面 - JOINED 但未开视频:展示头像或音频占位 - LEAVING 成员:从宫格中移除,或展示离开态后移除成员状态说明:
状态 说明 UI 建议 NECallMemberState.WAITING待接听,还未加入 RTC。 展示头像/昵称占位和“等待接听”。 NECallMemberState.JOINED已加入 RTC。 展示音视频画面或音频头像。 NECallMemberState.LEAVING正在离开或已离开。 从列表移除或展示离开态后移除。 业务侧可以随时调用
currentMembers获取当前完整成员快照:java
List<NECallMemberInfo> members = NECallEngine.sharedInstance().currentMembers(); -
监听邀请生命周期。
onCallInviteStateChanged只通知本端发出的邀请,不会通知被邀请端收到邀请或接听动作。java@Override public void onCallInviteStateChanged(List<NECallInviteStateInfo> infos) { for (NECallInviteStateInfo info : infos) { switch (info.state) { case NECallInviteState.SENT: showInviteWaiting(info.inviteeUserID); break; case NECallInviteState.JOINED: showToast("对方已加入通话"); break; case NECallInviteState.REJECTED: showToast("对方已拒绝"); break; case NECallInviteState.TIMEOUT: showToast("对方未接听"); break; case NECallInviteState.BUSY: showToast("对方正在通话中"); break; case NECallInviteState.UNSUPPORTED: showToast("对方客户端不支持多人通话"); break; case NECallInviteState.FAILED: case NECallInviteState.CANCELED: showToast("邀请已结束"); break; default: break; } } }NECallInviteStateInfo字段说明:字段 说明 callId当前通话 ID。 channelId当前信令房间 ID。 inviteBatchId本次批量邀请 ID,可关联 inviteMembers的返回结果。requestId当前账号本次邀请请求 ID。 inviterUserID邀请人账号。 inviteeUserID被邀请人账号。 state邀请生命周期状态。 reasonCode状态原因码,可用于区分拒绝、忙线、超时、加入失败等。 message兜底描述。UI 展示建议优先使用业务侧本地化文案。 邀请生命周期状态:
状态 说明 NECallInviteState.SENT邀请已发送,进入待接听。 NECallInviteState.JOINED被邀请方已加入通话。 NECallInviteState.REJECTED被邀请方拒绝。 NECallInviteState.TIMEOUT邀请超时或接听后加入 RTC 超时。 NECallInviteState.BUSY被邀请方忙线。 NECallInviteState.FAILED邀请发送或加入通话失败。 NECallInviteState.CANCELED邀请被取消。 NECallInviteState.UNSUPPORTED被邀请端不支持多人通话。 -
渲染多人音视频画面。
无 UI 场景下,多人宫格建议以
NECallMemberInfo为数据源。对已加入成员:- 本端用户:使用 NERTC 本地画布接口绑定本地视图。
- 远端用户:使用
member.uid调用 NERTC 远端画布接口。 - 待接听成员:不要绑定 RTC 画布,只展示占位。
javaprivate void bindVideoForMember(NECallMemberInfo member, NERtcVideoView videoView) { if (member.state != NECallMemberState.JOINED) { return; } videoView.setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_FILL); String currentAccId = NIMClient.getCurrentAccount(); if (TextUtils.equals(member.userID, currentAccId)) { NERtcEx.getInstance().setupLocalVideoCanvas(videoView); } else { NERtcEx.getInstance().setupRemoteVideoCanvas(videoView, member.uid); } }NECallEngine.setupRemoteView主要面向 1v1 远端画面。多人宫格中需要按成员uid分别绑定远端画布,建议直接使用 NERTC SDK 的setupRemoteVideoCanvas。 -
处理成员媒体状态变化。
Demo / CallKit-UI 的单呼转群呼页面主监听
onCallMembersChanged:成员加入、离开、待接听占位,以及成员快照里的当前音视频状态都从NECallMemberChangeInfo.members获取。单呼转群呼场景下,Core 在远端视频开始、停止、mute 状态变化时也会更新成员媒体状态,并通过onCallMembersChanged下发新的成员快照。同时,Demo 也保留了
onVideoMuted和onVideoAvailable,用于对单个成员格子做视频状态的增量刷新。因此无 UI 接入建议以onCallMembersChanged为主入口,再按需补充视频和音频回调。推荐无 UI 接入按相同方式处理:
onCallMembersChanged:主监听。刷新完整多人成员列表,并读取NECallMemberInfo.audioMuted、videoMuted、videoAvailable作为当前快照状态。onVideoMuted:补充监听。远端或本端视频 mute 状态变化时,更新对应成员的视频开关状态。onVideoAvailable:补充监听。远端视频流可用性变化时,更新对应成员的视频画面显示。onAudioMuted:如果业务 UI 需要展示麦克风图标,再监听该音频 mute 回调。
处理逻辑可参考以下伪代码:
onCallMembersChanged(info): members = info.members,如果为空则读取 currentMembers 对每个 member 刷新宫格数据: - 记录 member.userID / uid / state - 读取 member.videoMuted 和 member.videoAvailable,决定展示视频画面还是头像占位 - 读取 member.audioMuted,决定是否展示麦克风关闭图标 onVideoMuted(userId, muted): 找到 userId 对应成员 更新该成员 videoMuted = muted 只刷新该成员格子的视频开关状态 onVideoAvailable(userId, available): 找到 userId 对应成员 更新该成员 videoAvailable = available available == false 时隐藏视频画面,available == true 时恢复视频画面 onAudioMuted(userId, muted): 如果业务展示麦克风状态,更新 userId 对应成员的音频 mute 图标如果业务不展示成员麦克风状态,只处理
onCallMembersChanged、onVideoMuted和onVideoAvailable即可满足 Demo 同款多人视频宫格刷新。
API 参考
| API | 说明 |
|---|---|
NESetupConfig.Builder.enableSingleToGroupCall |
是否开启单呼转群呼能力,默认 false。 |
NECallEngine.canInviteMembers |
当前通话是否允许继续邀请成员。 |
NECallEngine.isInMultiCall |
当前通话是否已经进入过多人模式。 |
NECallEngine.currentMembers |
当前完整成员快照。 |
accept |
接听来电。第三方被邀请人接听 multiCallInvite == true 的多人邀请成功后,即可切换多人 UI。 |
inviteMembers |
通话中邀请成员加入当前通话。 |
onCallConnected |
当前端 1v1 通话建立回调,不用于判断单呼转群呼是否应切多人 UI。 |
onCallModeChanged |
通话模式变化,原 1v1 通话方首次进入多人模式时触发,适合切换多人布局。 |
onCallMembersChanged |
通话成员或成员媒体状态变化,返回完整成员快照;用于刷新多人宫格,不应等成员数达到 3 才切换多人 UI。 |
onVideoMuted |
视频 mute 状态变化,用于单成员视频状态增量刷新。 |
onVideoAvailable |
远端视频流可用性变化,用于单成员视频画面增量刷新。 |
onAudioMuted |
音频 mute 状态变化,业务展示麦克风状态时监听。 |
onCallInviteStateChanged |
本端发出的邀请生命周期变化。 |
onReceiveInvited |
收到普通 1v1 邀请或多人邀请。多人邀请时 multiCallInvite == true。 |
常见问题
为什么 1v1 接通后没有展示邀请入口?
请确认是否满足以下条件:
- 本端初始化时是否设置
enableSingleToGroupCall(true)。 - 是否已建立 1v1 通话,且当前状态为通话中。
- 对端是否为支持单呼转群呼的新版本,并同样开启能力。
- 当前是否已达到人数上限。
业务侧建议直接以 NECallEngine.sharedInstance().canInviteMembers() 的返回值作为入口展示依据。
inviteMembers 返回成功后,为什么成员还没出现在通话中?
inviteMembers 的 observer 只表示邀请信令发送结果。成员真正加入以 onCallMembersChanged 中 NECallMemberState.JOINED 为准。邀请发送后可以先展示 NECallMemberState.WAITING 占位。
被邀请方如何区分普通 1v1 来电和多人邀请?
在 onReceiveInvited 中判断 NEInviteInfo.multiCallInvite。
javaif (info.multiCallInvite) {
// 多人邀请
} else {
// 普通 1v1 呼叫
}
多人通话退回 2 人后,可以恢复 1v1 UI 吗?
不建议恢复。目前在进入多人模式后,本次通话保持多人模式;退回 2 人时仍展示多人双人宫格,并继续禁用音视频切换。
可以直接用 NEGroupCall 发起多人通话吗?
单呼转群呼不使用旧版 NEGroupCall 群呼链路。该能力是在已有 1v1 通话内邀请成员加入当前房间,请使用 NECallEngine.inviteMembers。
无 UI 场景必须处理哪些回调?
至少需要处理以下回调:
onReceiveInvited:展示普通来电页或多人邀请页;multiCallInvite == true时记录为多人邀请。accept:第三方被邀请人接听多人邀请成功后切换多人 UI。onCallConnected:1v1 通话建立后展示通话中页面,并刷新邀请入口。onCallModeChanged:原 1v1 通话方进入多人模式后切换多人布局。onCallMembersChanged:刷新成员列表、待接听占位和音视频画面。onCallInviteStateChanged:展示邀请拒绝、超时、忙线、不支持等提示。onCallEnd:收口页面和释放资源。




