实现单呼转群聊

更新时间: 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_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。业务通常不需要直接处理,可用于日志和问题排查。

开发环境

环境要求 说明
Android Studio 版本 Android Studio 5.0 及以上版本。 Android Studio 版本编号系统的变更请参考 Android Studio 版本说明
Android API 版本 Level 为 21 及以上版本。
Android SDK 版本 Android SDK 31、Android SDK Platform-Tools 31.x.x 及以上版本。
Gradle 及所需的依赖库 Gradle Services 页面下载对应版本的 Gradle 及所需的依赖库。
  • Gradle 版本:7.4.1
  • Android Gradle 插件版本: 7.1.3
    关于 Android Gradle 插件、Gradle、SDK Tool 之间的版本依赖关系,请参考 Android Gradle 插件版本说明
kotlin 1.6.21 及以上版本。
CPU 架构 ARM 64、ARMV7。
IDE Android Studio。
其他 依赖 Androidx,不支持 support 库。
Android 系统 5.0 及以上版本的真机。由于模拟器缺少摄像头及麦克风能力,因此工程需要在真机运行。

准备工作

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

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

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

  • 已实现 单聊呼叫

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

实现单呼转群呼

  1. 初始化呼叫组件。

    在初始化时需要在 NESetupConfig 中开启 enableSingleToGroupCall,并注册 NECallEngineDelegate

    javaNESetupConfig config =
        new NESetupConfig.Builder(appKey)
            .enableSingleToGroupCall(true)
            .build();
    
    NECallEngine.sharedInstance().setup(context.getApplicationContext(), config);
    NECallEngine.sharedInstance().addCallDelegate(callDelegate);
    
  2. 发起 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);
            });
    
  3. 处理普通来电和多人邀请来电。

    收到邀请时,通过 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 : "挂断失败");
    }
    });
    
  4. 展示邀请入口。

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

  5. 发起通话中邀请。

    当用户在业务 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。
  6. 切换多人 UI。

    不同角色切换多人 UI 的时机不同:

    • 原 1v1 通话方:收到 onCallModeChangednewMode == NECallMode.MULTI 时,切换多人布局。
    • 第三方被邀请人:onReceiveInvitedinfo.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
  7. 监听成员变化并刷新 UI。

    邀请发出后,业务侧应根据 onCallMembersChanged 刷新完整成员列表。

    onCallMembersChanged 不作为切换多人 UI 的唯一入口。第三方被邀请人刚接听成功时,成员快照可能先只有自己,随后才逐步补齐原 1v1 双方;因此应先切换多人 UI,再用该回调刷新宫格内容。

    onCallMembersChanged(info):
        members = info.members
        if 当前还未进入多人 UI 且 (isInMultiCall() == truemembers 中存在 WAITING 成员):
            切换到多人 UI
    
        按 members 重建或刷新多人宫格:
            - WAITING 成员:展示头像 / 昵称 / 等待接听占位
            - JOINED 且 videoAvailable == true 且 videoMuted == false:展示视频画面
            - JOINED 但未开视频:展示头像或音频占位
            - LEAVING 成员:从宫格中移除,或展示离开态后移除
    

    成员状态说明:

    状态 说明 UI 建议
    NECallMemberState.WAITING 待接听,还未加入 RTC。 展示头像/昵称占位和“等待接听”。
    NECallMemberState.JOINED 已加入 RTC。 展示音视频画面或音频头像。
    NECallMemberState.LEAVING 正在离开或已离开。 从列表移除或展示离开态后移除。

    业务侧可以随时调用 currentMembers 获取当前完整成员快照:

    javaList<NECallMemberInfo> members = NECallEngine.sharedInstance().currentMembers();
    
  8. 监听邀请生命周期。

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

    无 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

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

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

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

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

    • onCallMembersChanged:主监听。刷新完整多人成员列表,并读取 NECallMemberInfo.audioMutedvideoMutedvideoAvailable 作为当前快照状态。
    • 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 图标
    

    如果业务不展示成员麦克风状态,只处理 onCallMembersChangedonVideoMutedonVideoAvailable 即可满足 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 接通后没有展示邀请入口?

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

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

业务侧建议直接以 NECallEngine.sharedInstance().canInviteMembers() 的返回值作为入口展示依据。

inviteMembers 返回成功后,为什么成员还没出现在通话中?

inviteMembers 的 observer 只表示邀请信令发送结果。成员真正加入以 onCallMembersChangedNECallMemberState.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:收口页面和释放资源。
此文档是否对你有帮助?
有帮助
去反馈
  • 注意事项
  • 基本概念
  • 开发环境
  • 准备工作
  • 实现单呼转群呼
  • API 参考
  • 常见问题