实现单呼转群聊
更新时间: 2026/06/05 10:19:48
自 V4.7.0 版本起,呼叫组件支持单呼转群呼功能。本文主要介绍如何通过集成呼叫组件(无 UI),在已接通的 1v1 通话中继续邀请其他用户加入当前通话,实现单呼平滑升级为多人通话。无 UI 接入方式下,通话界面由业务侧根据自身产品形态自行实现。
该功能并不是原有的群呼能力。单呼转群呼基于当前 1v1 信令房间和 RTC 房间,通过 NECallEngine inviteMembers:completion: 邀请新成员加入,不需要重新发起群呼。
注意事项
- 呼叫组件基于网易云信 NIM SDK 和 NERTC SDK 实现通话呼叫。
- 针对呼叫组件中的回调信息,开发者要做好相应回调数据的上报及存储,以便于后期上线之后排查问题。
- 参与单呼转群呼的端都需要使用支持该能力的新版本 SDK,并开启
enableSingleToGroupCall。任一端未开启或版本不支持时,canInviteMembers返回NO。 - 单呼转群呼功能仅允许在 1v1 呼叫已接通后发起。初始被叫未接听前,不支持邀请其他用户。
- 多人通话人数上限为 10 人。SDK 会按当前已加入成员、待接听成员和本次邀请账号数做校验。
- 邀请发送成功只表示邀请信令已发出,不表示对方已接听或已加入通话。被邀请方真正成为通话成员以
onCallMembersChanged中成员状态变为NECallMemberStateJoined为准。 - 通话一旦进入多人模式,本次通话内会保持多人模式;即使后续只剩 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。业务通常不需要直接处理,可用于日志和问题排查。
开发环境
在开始运行工程之前,请您准备以下开发环境:
- Xcode 14 及以上版本。
- iOS 10.0 及以上版本的 iOS 设备。
- 已安装 CocoaPods。
准备工作
根据本文操作前,请确保您已经完成了以下设置:
实现单呼转群呼
-
初始化呼叫组件。
在初始化时需要在
NESetupConfig中开启enableSingleToGroupCall,并注册NECallEngineDelegate。objective-c#import <NERtcCallKit/NERtcCallKit.h> @interface CallManager () <NECallEngineDelegate> @end @implementation CallManager - (void)setupCallEngineWithAppKey:(NSString *)appKey { NESetupConfig *config = [[NESetupConfig alloc] initWithAppkey:appKey]; config.enableSingleToGroupCall = YES; [[NECallEngine sharedInstance] setup:config]; [[NECallEngine sharedInstance] addCallDelegate:self]; } @end -
发起 1v1 呼叫。
objective-cNECallParam *param = [[NECallParam alloc] initWithAccId:@"callee_account_id" withCallType:NECallTypeVideo]; param.extraInfo = @"business attachment"; param.globalExtraCopy = @"business global extra"; [[NECallEngine sharedInstance] call:param completion:^(NSError *_Nullable error, NECallInfo *_Nullable callInfo) { if (error) { NSLog(@"call failed: %@", error); return; } NSLog(@"call sent, callId: %@", callInfo.callId); }]; -
处理普通来电和多人邀请来电。
收到邀请时,通过
NEInviteInfo.multiCallInvite区分普通 1v1 来电和多人邀请。objective-c- (void)onReceiveInvited:(NEInviteInfo *)info { if (info.multiCallInvite) { [self showMultiInvitePageWithInviter:info.callerAccId callType:info.callType attachment:info.extraInfo]; } else { [self showOneToOneIncomingPageWithCaller:info.callerAccId callType:info.callType attachment:info.extraInfo]; } }接听objective-c[[NECallEngine sharedInstance] accept:^(NSError *_Nullable error, NECallInfo *_Nullable callInfo) { if (error) { NSLog(@"accept failed: %@", error); return; } NSLog(@"accept success, callId: %@", callInfo.callId); }];拒绝、取消或挂断当前呼叫objective-cNEHangupParam *param = [[NEHangupParam alloc] init]; param.extraString = @"business hangup extra"; [[NECallEngine sharedInstance] hangup:param completion:^(NSError *_Nullable error) { if (error) { NSLog(@"hangup failed: %@", error); } }]; -
展示邀请入口。
业务侧应在 1v1 接通后调用
canInviteMembers判断是否允许展示邀请入口。objective-c- (void)refreshInviteButton { BOOL canInvite = [[NECallEngine sharedInstance] canInviteMembers]; self.inviteButton.hidden = !canInvite; } - (void)onCallConnected:(NECallInfo *)info { // 这里只代表 1v1 通话已建立,不用于判断单呼转群呼是否应切多人 UI。 [self refreshInviteButton]; }canInviteMembers会综合当前通话状态、是否开启单呼转群呼、对端能力、当前通话信息等条件。业务侧不要只根据本地开关决定是否展示入口。 -
发起通话中邀请。
当用户在业务 UI 中选择成员后,调用
inviteMembers:completion:。objective-c- (void)inviteUsers:(NSArray<NSString *> *)userIDs { if (![[NECallEngine sharedInstance] canInviteMembers]) { [self showToast:@"当前通话不支持邀请成员"]; return; } NECallInviteParam *param = [[NECallInviteParam alloc] init]; param.userIDs = userIDs; param.attachment = @"invite attachment"; param.globalExtra = @"invite global extra"; param.maxMembers = 10; NECallPushConfig *pushConfig = [[NECallPushConfig alloc] init]; pushConfig.pushTitle = @"多人通话邀请"; pushConfig.pushContent = @"邀请你加入多人通话"; pushConfig.needPush = YES; pushConfig.needBadge = YES; param.pushConfig = pushConfig; [[NECallEngine sharedInstance] inviteMembers:param completion:^(NSError *_Nullable error, NECallInviteResult *_Nullable result) { if (error) { NSLog(@"invite failed: %@", error); [self showToast:error.localizedDescription ?: @"邀请失败"]; return; } NSLog(@"invite sent, callId: %@, inviteBatchId: %@", result.callId, result.inviteBatchId); NSInteger successCount = 0; NSInteger failedCount = 0; for (NECallInviteItemResult *item in result.results) { if (item.isSuccess) { successCount += 1; } else { failedCount += 1; NSLog(@"invite item failed, user: %@, code: %ld, message: %@", item.inviteeUserID, (long)item.code, item.message); } } if (successCount > 0 && failedCount == 0) { [self showToast:@"邀请已发送"]; } else if (successCount > 0 && failedCount > 0) { [self showToast:@"部分邀请已发送,部分失败"]; } else { [self showToast:@"邀请失败"]; } }]; }NECallInviteParam字段说明:字段 说明 userIDs被邀请账号列表。SDK 会自动忽略无效账号、本端账号、已在通话成员和仍处于待接听的成员。 attachment业务透传扩展,会透传到被邀请端 NEInviteInfo.extraInfo。globalExtra全局抄送扩展。 pushConfig多人邀请通知和离线推送配置。 maxMembers本次通话人数上限。不设置或小于等于 0 时使用默认值 10。 -
切换多人 UI。
不同角色切换多人 UI 的时机不同:
- 原 1v1 通话方:收到
onCallModeChanged:且newMode == NECallModeMulti时,切换多人布局。 - 第三方被邀请人:
onReceiveInvited:中info.multiCallInvite == YES表示这是多人邀请;用户点击接听后,accept成功即可切换多人布局。 - 兜底刷新:如果先收到
onCallMembersChanged:,且isInMultiCall == YES或成员快照中出现NECallMemberStateWaiting,也可以先切换多人布局再刷新成员。
切换多人 UI 的逻辑建议做成幂等,避免多个回调连续触发时重复创建页面。
onReceiveInvited(info): if info.multiCallInvite == YES: 展示多人邀请来电页 记录当前来电为多人邀请 accept completion(error, callInfo): if error == nil 且当前来电是多人邀请: 切换到多人 UI 先用 currentMembers 渲染已有成员 等待 onCallMembersChanged 补齐成员列表和媒体状态 onCallModeChanged(info): if info.newMode == NECallModeMulti: 切换到多人布局 隐藏或禁用音视频类型切换入口NECallModeChangeInfo字段说明:字段 说明 oldMode变化前通话模式。 newMode变化后通话模式。 memberCount当前有效成员数量,只统计已加入成员。 hasEverMulti本次通话是否已经进入过多人模式;成功发起多人邀请并出现待接听成员后即为 YES。 - 原 1v1 通话方:收到
-
监听成员变化并刷新 UI。
邀请发出后,业务侧应根据
onCallMembersChanged:刷新完整成员列表。onCallMembersChanged:不作为切换多人 UI 的唯一入口。第三方被邀请人刚接听成功时,成员快照可能先只有自己,随后才逐步补齐原 1v1 双方;因此应先切换多人 UI,再用该回调刷新宫格内容。onCallMembersChanged(info): members = info.members if 当前还未进入多人 UI 且 (isInMultiCall == YES 或 members 中存在 Waiting 成员): 切换到多人 UI 按 members 重建或刷新多人宫格: - Waiting 成员:展示头像 / 昵称 / 等待接听占位 - Joined 且 videoAvailable == YES 且 videoMuted == NO:展示视频画面 - Joined 但未开视频:展示头像或音频占位 - Leaving 成员:从宫格中移除,或展示离开态后移除成员状态说明:
状态 说明 UI 建议 NECallMemberStateWaiting待接听,还未加入 RTC。 展示头像/昵称占位和“等待接听”。 NECallMemberStateJoined已加入 RTC。 展示音视频画面或音频头像。 NECallMemberStateLeaving正在离开或已离开。 从列表移除或展示离开态后移除。 业务侧可以随时调用
currentMembers获取当前完整成员快照:objective-cNSArray<NECallMemberInfo *> *members = [[NECallEngine sharedInstance] currentMembers]; -
监听邀请生命周期。
onCallInviteStateChanged:只通知本端发出的邀请,不会通知被邀请端收到邀请或接听动作。objective-c- (void)onCallInviteStateChanged:(NSArray<NECallInviteStateInfo *> *)infos { for (NECallInviteStateInfo *info in infos) { switch (info.state) { case NECallInviteStateSent: [self showInviteWaitingForUser:info.inviteeUserID]; break; case NECallInviteStateJoined: [self showToast:@"对方已加入通话"]; break; case NECallInviteStateRejected: [self showToast:@"对方已拒绝"]; break; case NECallInviteStateTimeout: [self showToast:@"对方未接听"]; break; case NECallInviteStateBusy: [self showToast:@"对方正在通话中"]; break; case NECallInviteStateUnsupported: [self showToast:@"对方客户端不支持多人通话"]; break; case NECallInviteStateFailed: case NECallInviteStateCanceled: [self showToast:@"邀请已结束"]; break; default: break; } } }NECallInviteStateInfo字段说明:字段 说明 callId当前通话 ID。 channelId当前信令房间 ID。 inviteBatchId本次批量邀请 ID,可关联 inviteMembers的返回结果。requestId当前账号本次邀请请求 ID。 inviterUserID邀请人账号。 inviteeUserID被邀请人账号。 state邀请生命周期状态。 reasonCode状态原因码,可用于区分拒绝、忙线、超时、加入失败等。 message兜底描述。UI 展示建议优先使用业务侧本地化文案。 邀请生命周期状态:
状态 说明 NECallInviteStateSent邀请已发送,进入待接听。 NECallInviteStateJoined被邀请方已加入通话。 NECallInviteStateRejected被邀请方拒绝。 NECallInviteStateTimeout邀请超时或接听后加入 RTC 超时。 NECallInviteStateBusy被邀请方忙线。 NECallInviteStateFailed邀请发送或加入通话失败。 NECallInviteStateCanceled邀请被取消。 NECallInviteStateUnsupported被邀请端不支持多人通话。 -
渲染多人音视频画面。
无 UI 场景下,多人宫格建议以
NECallMemberInfo为数据源。对已加入成员:- 本端用户:使用 NERTC 本地画布接口绑定本地视图。
- 远端用户:使用
member.uid调用 NERTC 远端画布接口。 - 待接听成员:不要绑定 RTC 画布,只展示占位。
objective-c- (void)bindVideoForMember:(NECallMemberInfo *)member view:(UIView *)view { if (member.state != NECallMemberStateJoined) { return; } NERtcVideoCanvas *canvas = [[NERtcVideoCanvas alloc] init]; canvas.container = view; canvas.renderMode = kNERtcVideoRenderScaleCropFill; NSString *currentUser = [[NIMSDK sharedSDK].v2LoginService getLoginUser]; if ([member.userID isEqualToString:currentUser]) { [[NERtcEngine sharedEngine] setupLocalVideoCanvas:canvas]; [[NERtcEngine sharedEngine] startPreview]; } else { [[NERtcEngine sharedEngine] setupRemoteVideoCanvas:canvas forUserID:member.uid]; } }NECallEngine setupRemoteView:主要面向 1v1 远端画面。多人宫格中需要按成员uid分别绑定远端画布,建议直接使用 NERTC SDK 的setupRemoteVideoCanvas:forUserID:。 -
处理成员媒体状态变化。
Demo / CallKit-UI 的单呼转群呼页面主监听
onCallMembersChanged::成员加入、离开、待接听占位,以及成员快照里的当前音视频状态都从NECallMemberChangeInfo.members获取。单呼转群呼场景下,Core 在远端视频开始、停止、mute 状态变化时也会更新成员媒体状态,并通过onCallMembersChanged:下发新的成员快照。同时,Demo 也保留了
onVideoMuted:userID:和onVideoAvailable:userID:,用于对单个成员格子做视频状态的增量刷新。因此无 UI 接入建议以onCallMembersChanged:为主入口,再按需补充视频和音频回调。推荐无 UI 接入按相同方式处理:
onCallMembersChanged::主监听。刷新完整多人成员列表,并读取NECallMemberInfo.audioMuted、videoMuted、videoAvailable作为当前快照状态。onVideoMuted:userID::补充监听。远端或本端视频 mute 状态变化时,更新对应成员的视频开关状态。onVideoAvailable:userID::补充监听。远端视频流可用性变化时,更新对应成员的视频画面显示。onAudioMuted:userID:/onLocalAudioMuted::如果业务 UI 需要展示麦克风图标,再监听这两个音频 mute 回调。
处理逻辑可参考以下伪代码:
onCallMembersChanged(info): members = info.members,如果为空则读取 currentMembers 对每个 member 刷新宫格数据: - 记录 member.userID / uid / state - 读取 member.videoMuted 和 member.videoAvailable,决定展示视频画面还是头像占位 - 读取 member.audioMuted,决定是否展示麦克风关闭图标 onVideoMuted(muted, userId): 找到 userId 对应成员 更新该成员 videoMuted = muted 只刷新该成员格子的视频开关状态 onVideoAvailable(available, userId): 找到 userId 对应成员 更新该成员 videoAvailable = available available == NO 时隐藏视频画面,available == YES 时恢复视频画面 onAudioMuted(muted, userId): 如果业务展示麦克风状态,更新 userId 对应成员的音频 mute 图标 onLocalAudioMuted(muted): 如果业务展示本端麦克风状态,更新当前登录用户的音频 mute 图标如果业务不展示成员麦克风状态,只处理
onCallMembersChanged:、onVideoMuted:userID:和onVideoAvailable:userID:即可满足 Demo 同款多人视频宫格刷新。
API 参考
| API | 说明 |
|---|---|
NESetupConfig.enableSingleToGroupCall |
是否开启单呼转群呼能力,默认 NO。 |
canInviteMembers |
当前通话是否允许继续邀请成员。 |
isInMultiCall |
当前通话是否已经进入过多人模式。 |
currentMembers |
当前完整成员快照。 |
accept: |
接听来电。第三方被邀请人接听 multiCallInvite == YES 的多人邀请成功后,即可切换多人 UI。 |
inviteMembers:completion: |
通话中邀请成员加入当前通话。 |
onCallConnected: |
当前端 1v1 通话建立回调,不用于判断单呼转群呼是否应切多人 UI。 |
onCallModeChanged: |
通话模式变化,原 1v1 通话方首次进入多人模式时触发,适合切换多人布局。 |
onCallMembersChanged: |
通话成员或成员媒体状态变化,返回完整成员快照;用于刷新多人宫格,不应等成员数达到 3 才切换多人 UI。 |
onVideoMuted:userID: |
视频 mute 状态变化,用于单成员视频状态增量刷新。 |
onVideoAvailable:userID: |
远端视频流可用性变化,用于单成员视频画面增量刷新。 |
onAudioMuted:userID: / onLocalAudioMuted: |
音频 mute 状态变化,业务展示麦克风状态时监听。 |
onCallInviteStateChanged: |
本端发出的邀请生命周期变化。 |
onReceiveInvited: |
收到普通 1v1 邀请或多人邀请。多人邀请时 multiCallInvite == YES。 |
常见问题
为什么 1v1 接通后没有展示邀请入口?
请确认是否满足以下条件:
- 本端初始化时是否设置
enableSingleToGroupCall = YES。 - 是否已建立 1v1 通话,且当前状态为通话中。
- 对端是否为支持单呼转群呼的新版本,并同样开启能力。
- 当前是否已达到人数上限。
业务侧建议直接以 [[NECallEngine sharedInstance] canInviteMembers] 的返回值作为入口展示依据。
inviteMembers 返回成功后,为什么成员还没出现在通话中?
inviteMembers 的 completion 只表示邀请信令发送结果。成员真正加入以 onCallMembersChanged: 中 NECallMemberStateJoined 为准。邀请发送后可以先展示 NECallMemberStateWaiting 占位。
被邀请方如何区分普通 1v1 来电和多人邀请?
在 onReceiveInvited: 中判断 NEInviteInfo.multiCallInvite。
objective-cif (info.multiCallInvite) {
// 多人邀请
} else {
// 普通 1v1 呼叫
}
多人通话退回 2 人后,可以恢复 1v1 UI 吗?
不建议恢复。目前在进入多人模式后,本次通话保持多人模式;退回 2 人时仍展示多人双人宫格,并继续禁用音视频切换。
可以直接用 NEGroupCall 发起多人通话吗?
单呼转群呼不使用旧版 NEGroupCallKit 群呼链路。该能力是在已有 1v1 通话内邀请成员加入当前房间,请使用 NECallEngine inviteMembers:completion:。
无 UI 场景必须处理哪些回调?
至少需要处理以下回调:
onReceiveInvited::展示普通来电页或多人邀请页;multiCallInvite == YES时记录为多人邀请。accept::第三方被邀请人接听多人邀请成功后切换多人 UI。onCallConnected::1v1 通话建立后展示通话中页面,并刷新邀请入口。onCallModeChanged::原 1v1 通话方进入多人模式后切换多人布局。onCallMembersChanged::刷新成员列表、待接听占位和音视频画面。onCallInviteStateChanged::展示邀请拒绝、超时、忙线、不支持等提示。onCallEnd::收口页面和释放资源。




