实现单呼转群聊

更新时间: 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_idaccount_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。

准备工作

根据本文操作前,请确保您已经完成了以下设置:

  • 网易云信控制台 创建应用,并获取了对应的 App Key。

  • 开通 IM 即时通讯、音视频通话 2.0、话单功能以及信令产品。

  • 已实现 单聊呼叫

    使用单呼转群呼功能前,请先完成 1v1 单聊呼叫接入,并确认可以正常发起、接听和挂断 1v1 音视频通话。

实现单呼转群呼

  1. 初始化呼叫组件。

    在初始化时需要在 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
    
  2. 发起 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);
    }];
    
  3. 处理普通来电和多人邀请来电。

    收到邀请时,通过 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);
        }
    }];
    
  4. 展示邀请入口。

    业务侧应在 1v1 接通后调用 canInviteMembers 判断是否允许展示邀请入口。

    objective-c- (void)refreshInviteButton {
        BOOL canInvite = [[NECallEngine sharedInstance] canInviteMembers];
        self.inviteButton.hidden = !canInvite;
    }
    
    - (void)onCallConnected:(NECallInfo *)info {
        // 这里只代表 1v1 通话已建立,不用于判断单呼转群呼是否应切多人 UI。
        [self refreshInviteButton];
    }
    

    canInviteMembers 会综合当前通话状态、是否开启单呼转群呼、对端能力、当前通话信息等条件。业务侧不要只根据本地开关决定是否展示入口。

  5. 发起通话中邀请。

    当用户在业务 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。
  6. 切换多人 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
  7. 监听成员变化并刷新 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];
    
  8. 监听邀请生命周期。

    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 被邀请端不支持多人通话。
  9. 渲染多人音视频画面。

    无 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:

  10. 处理成员媒体状态变化。

    Demo / CallKit-UI 的单呼转群呼页面主监听 onCallMembersChanged::成员加入、离开、待接听占位,以及成员快照里的当前音视频状态都从 NECallMemberChangeInfo.members 获取。单呼转群呼场景下,Core 在远端视频开始、停止、mute 状态变化时也会更新成员媒体状态,并通过 onCallMembersChanged: 下发新的成员快照。

    同时,Demo 也保留了 onVideoMuted:userID:onVideoAvailable:userID:,用于对单个成员格子做视频状态的增量刷新。因此无 UI 接入建议以 onCallMembersChanged: 为主入口,再按需补充视频和音频回调。

    推荐无 UI 接入按相同方式处理:

    • onCallMembersChanged::主监听。刷新完整多人成员列表,并读取 NECallMemberInfo.audioMutedvideoMutedvideoAvailable 作为当前快照状态。
    • 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 接通后没有展示邀请入口?

请确认是否满足以下条件:

  1. 本端初始化时是否设置 enableSingleToGroupCall = YES
  2. 是否已建立 1v1 通话,且当前状态为通话中。
  3. 对端是否为支持单呼转群呼的新版本,并同样开启能力。
  4. 当前是否已达到人数上限。

业务侧建议直接以 [[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::收口页面和释放资源。
此文档是否对你有帮助?
有帮助
去反馈
  • 注意事项
  • 基本概念
  • 开发环境
  • 准备工作
  • 实现单呼转群呼
  • API 参考
  • 常见问题