同一个用户可以加入多个房间,分别订阅和接收这些房间中的音视频流,并在其中一个房间中发布音视频。也可以发送和接收实时消息。

适用场景
前提条件

你已经集成 RTC SDK v3.43 或更新的版本,实现了基本的音视频通话。
支持多房间功能的 SDK 详见API 及回调。

功能实现

在集成 RTC SDK,并完成业务逻辑代码时,你已发现,音视频引擎类和房间类两个常用的主调类有明显的功能区分:

要让一个用户加入多个房间,可以通过一个音视频引擎实例,创建多个房间实例,并为这些实例设置不同的房间 ID。同一个用户在多个房间中,可以订阅在这些房间中发布的音视频流。
本文以加入一个房间为例,你可以按照相同流程,让用户加入更多房间。

以下时序图以 Android SDK 中的 API 名称为例。不同端的 SDK 中 API 或回调名称可能略有不同,以 API 及回调为准。

alt

1. 创建引擎类

创建和初始化一个音视频引擎类。

参考 构建 RTC 应用 获取详细步骤。

// APP_ID: 已经在控制台申请的app_id
// videoEventHandler: 用于接收 RTCVideo 回调的接口实例
rtcVideo = RTCVideo.createRTCVideo(this, Constants.APP_ID, videoEventHandler, null, null);
java
// kAppID: 已经在控制台申请的app_id
// delegate: 用于接收 RTCVideo 回调的接口实例
// parameters: 私有参数,可以填空
self.rtcVideo = ByteRTCVideo.createRTCVideo(kAppID, delegate: self, parameters: [:])
swift
// app_id: 已经在控制台申请的app_id
// handler: 用于接收 RTCVideo 回调的接口实例
// parameters: 私有参数,可以填 nullptr
bytertc::IRTCVideo *video = bytertc::createRTCVideo("app_id", handler, nullptr);
cpp
2. 启动音视频采集

创建音视频引擎类后,你可以启动音视频采集,并设置渲染视图。

// 开启音视频采集
rtcVideo.startAudioCapture();
rtcVideo.startVideoCapture();

// 设置本地渲染视图,支持 TextureView 和 SurfaceView
private void setLocalRenderView() {
    VideoCanvas videoCanvas = new VideoCanvas();
    videoCanvas.renderView = localView;
    videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN;
    rtcVideo.setLocalVideoCanvas(StreamIndex.STREAM_INDEX_MAIN, videoCanvas);
}
java
// 开启本地音视频采集
self.rtcVideo?.startVideoCapture()
self.rtcVideo?.startAudioCapture()

// 设置本地渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = self.localView.videoView
canvas.renderMode = .hidden
self.rtcVideo?.setLocalVideoCanvas(.main, withCanvas: canvas);
swift
//开始音视频采集
video->startAudioCapture();
video->startVideoCapture();

//设置本地渲染窗口
bytertc::VideoCanvas cas;
cas.view = view;
video->setLocalVideoCanvas(bytertc::kStreamIndexMain, cas);
cpp
3. 创建房间实例

创建房间实例后,你可以加入房间发布和订阅音视频流。建议设置房间回调接口,以便监听房间和音视频流的状态回调。
继续调用 createRTCRoom 并传入不同房间 ID,以创建多个房间实例,并分别加入这些房间。

// 创建房间1
rtcRoom1 = rtcVideo.createRTCRoom(roomID);
rtcRoom1.setRTCRoomEventHandler(firstRoomEventHandler);

// 创建房间2
rtcRoom2 = rtcVideo.createRTCRoom(roomID);
rtcRoom2.setRTCRoomEventHandler(secondRoomEventHandler);
java
//创建房间1
self.rtcRoom1 = self.rtcVideo?.createRTCRoom(roomId1)
self.rtcRoom1?.delegate = self

//创建房间2
self.rtcRoom2 = self.rtcVideo?.createRTCRoom(roomId2)
self.rtcRoom2?.delegate = self
swift
//创建房间1
bytertc::IRTCRoom *room1 = video->createRTCRoom("roomid1");
room1->setRTCRoomEventHandler(room_handler); 

//创建房间2
bytertc::IRTCRoom *room2 = video->createRTCRoom("roomid2");
room2->setRTCRoomEventHandler(room_handler);
cpp
4. 加入 RTC 房间

alt

  1. 加入每个房间时,默认自动订阅房间中的音视频流。
// 用户信息
UserInfo userInfo = new UserInfo(localUid, "");
// 房间配置
boolean isAutoPublish = true;
boolean isAutoSubscribeAudio = true;
boolean isAutoSubscribeVideo = true;
RTCRoomConfig roomConfig = new RTCRoomConfig(ChannelProfile.CHANNEL_PROFILE_CHAT_ROOM, isAutoPublish, isAutoSubscribeAudio, isAutoSubscribeVideo);
// 加入房间
rtcRoom1.joinRoom(token, userInfo, roomConfig);

// 用户信息
UserInfo userInfo = new UserInfo(localUid, "");
// room2不发流
boolean isAutoPublish = false;
boolean isAutoSubscribeAudio = true;
boolean isAutoSubscribeVideo = true;
RTCRoomConfig roomConfig = new RTCRoomConfig(ChannelProfile.CHANNEL_PROFILE_CHAT_ROOM, isAutoPublish, isAutoSubscribeAudio, isAutoSubscribeVideo);
// 加入房间
rtcRoom2.joinRoom(token, userInfo, roomConfig);
java
//room1 进房,默认可以发布音视频流
// 获取 token,建议从服务端获取
let token1 = generatorToken(roomId: roomId1, userId: userId1)

let userInfo1 = ByteRTCUserInfo.init()
userInfo1.userId = userId1

let roomCfg1 = ByteRTCRoomConfig.init()
self.rtcRoom1?.joinRoom(token1, userInfo: userInfo1, roomConfig: roomCfg1)


//room2 进房,不能发布音视频流
// 获取 token,建议从服务端获取
let token2 = generatorToken(roomId: roomId2, userId: userId2)

let userInfo2 = ByteRTCUserInfo.init()
userInfo2.userId = userId2

let roomCfg2 = ByteRTCRoomConfig.init()
roomCfg2.isAutoPublish = false
self.rtcRoom2?.joinRoom(token2, userInfo: userInfo2, roomConfig: roomCfg2)
swift
//room1 进房,默认可以发布音视频流
bytertc::RTCRoomConfig config1, config2;
bytertc::UserInfo userinfo1, userinfo2;
userinfo1.uid = "name1";
int ret = room1->joinRoom("token1", userinfo1, config1); 

//room2 进房,不能发布音视频流
config2.is_auto_publish = false;
userinfo2.uid= "name2";
ret = room2->joinRoom("token2", userinfo2, config2);
cpp
  1. 监听进房状态,处理进房失败。
public void onRoomStateChanged(String roomId, String uid, int state, String extraInfo) {
    if (state == 0) { //进房成功
    } else if (state == -1000) { //token错误
    } else {// .... 其他错误
    }
}
java
func rtcRoom(_ rtcRoom: ByteRTCRoom, onRoomStateChanged roomId: String, withUid uid: String, state: Int, extraInfo: String) {
    if (state == 0) { //进房成功
    } else if (state == -1000) { //token错误
    } else {// .... 其他错误
    }
}
swift
void RoomHandler::onRoomStateChanged(const char* room_id, const char* uid, int state, const char* extra_info) {
    if (state == 0) { //进房成功
    } else if (state == -1000) { //token错误
    } else {// .... 其他错误
    }
}
cpp
  1. 订阅端在接收到其他用户发布/取消发布音视频流回调时,设置渲染视图。
@Override
public void onUserPublishStream(String uid, MediaStreamType type) {
    super.onUserPublishStream(uid, type);
    runOnUiThread(() -> {
        // 设置远端视频渲染视图
        RemoteStreamKey remoteStreamKey = new RemoteStreamKey(roomID1, uid, StreamIndex.STREAM_INDEX_MAIN);
        VideoCanvas videoCanvas = new VideoCanvas();
        videoCanvas.renderView = firstRoomRemoteView;
        videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN;
        rtcVideo.setRemoteVideoCanvas(remoteStreamKey, videoCanvas);
    });
}

@Override
public void onUserUnpublishStream(String uid, MediaStreamType type, StreamRemoveReason reason) {
    super.onUserUnpublishStream(uid, type, reason);
    runOnUiThread(() -> {
        // 解除远端视频渲染视图绑定
        RemoteStreamKey remoteStreamKey = new RemoteStreamKey(roomID1, uid, StreamIndex.STREAM_INDEX_MAIN);
        rtcVideo.setRemoteVideoCanvas(remoteStreamKey, null);
    });
}
java
// 远端用户发布流
func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserPublishStream userId: String, type: ByteRTCMediaStreamType) {
    // 渲染远端用户
    DispatchQueue.main.async {
        // 设置远端用户视频渲染视图
        let canvas = ByteRTCVideoCanvas.init()
        canvas.view = view.videoView
        canvas.renderMode = .hidden
        
        let streamKey = ByteRTCRemoteStreamKey.init()
        streamKey.userId = userId;
        streamKey.roomId = roomId;
        streamKey.streamIndex = .main;
        
        self.rtcVideo?.setRemoteVideoCanvas(streamKey, withCanvas: canvas)
    }
}

// 远端用户取消发布流
func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserUnpublishStream userId: String, type: ByteRTCMediaStreamType, reason: ByteRTCStreamRemoveReason) {
    
    // 移除 UI 显示
    DispatchQueue.main.async {
        // 设置远端用户视频渲染视图
        let canvas = ByteRTCVideoCanvas.init()
        canvas.view = nil // 置为空
        canvas.renderMode = .hidden
        
        let streamKey = ByteRTCRemoteStreamKey.init()
        streamKey.userId = userId;
        streamKey.roomId = roomId;
        streamKey.streamIndex = .main;
        
        self.rtcVideo?.setRemoteVideoCanvas(streamKey, withCanvas: canvas)
    }
}
swift
//远端用户发布流
void RoomHandler::onUserPublishStream(const char* uid, bytertc::MediaStreamType type){
    bytertc::RemoteStreamKey key;
    bytertc::VideoCanvas cas;
    key.stream_index = bytertc::kStreamIndexMain;
    key.room_id = roomid.c_str();
    key.user_id = uid;
    cas.view = (void*)ui->widget->winId();
    video->setRemoteVideoCanvas(key, cas);
}
//远端用户取消发布流
void RoomHandler::onUserUnPublishStream(const char* uid, MediaStreamType type,StreamRemoveReason reason) {
    bytertc::RemoteStreamKey key;
    bytertc::VideoCanvas cas;
    key.stream_index = bytertc::kStreamIndexMain;
    key.room_id = roomid.c_str();
    key.user_id = uid;
    cas.view = nullptr;
    video->setRemoteVideoCanvas(key, cas);
}
cpp
5. 离开 RTC 房间

用户结束通话时,退出各个房间,并销毁本地房间实例。

rtcRoom.leaveRoom();
rtcRoom.destroy();
rtcRoom = null;
java
self?.rtcRoom?.leaveRoom()
self?.rtcRoom?.destroy()
self?.rtcRoom = nil
swift
room->leaveRoom();
room->destroy();
cpp
6. 销毁引擎类

在销毁所有房间实例后可销毁音视频引擎。

RTCVideo.destroyRTCVideo();
java

ByteRTCVideo.destroyRTCVideo()
self.rtcVideo = nil
swift
bytertc::destroyRTCVideo();
cpp
示例项目

参考以下项目获取完整代码。在示例项目中,本端用户加入了两个不同的 RTC 房间。你可以按照相同流程,让用户加入更多房间。

API 及回调

你可以根据上文的描述和示例,使用以下客户端 SDK,在不同的端上实现同一用户加入多房间功能。

说明:表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。

平台AndroidiOSmacOSWindowsLinuxElectronFlutterUnity
音视频引擎类RTCVideoByteRTCVideoByteRTCVideoIRTCVideoIRTCVideoRTCVideoRTCVideoIRTCVideo
开启视频采集startVideoCapturestartVideoCapturestartVideoCapturestartVideoCapturestartVideoCapturestartVideoCapturestartVideoCaptureStartVideoCapture
开始音频采集startAudioCapturestartAudioCapturestartAudioCapturestartAudioCapturestartAudioCapturestartAudioCapturestartAudioCaptureStartAudioCapture
设置本地视图setLocalVideoCanvassetLocalVideoCanvas:withCanvas:setLocalVideoCanvas:withCanvas:setLocalVideoCanvassetLocalVideoCanvassetupLocalVideoRTCViewContextSetLocalVideoSink
创建房间createRTCRoomcreateRTCRoom:createRTCRoom:createRTCRoomcreateRTCRoomcreateRTCRoomcreateRTCRoomCreateRTCRoom
音视频房间类RTCRoomByteRTCRoomByteRTCRoomIRTCRoomIRTCRoomRTCRoomRTCRoomIRTCVideoRoom
设置房间回调setRTCRoomEventHandlersetRTCRoomDelegate:setRTCRoomDelegate:setRTCRoomEventHandlersetRTCRoomEventHandler无setRTCRoomEventHandler无
加入房间joinRoomjoinRoom:userInfo:roomConfig:joinRoom:userInfo:roomConfig:joinRoomjoinRoomjoinRoomjoinRoomJoinRoom
房间状态回调onRoomStateChangedrtcRoom:onRoomStateChanged:withUid:state:extraInfo:rtcRoom:onRoomStateChanged:withUid:state:extraInfo:onRoomStateChangedonRoomStateChangedonRoomStateChangedonRoomStateChangedonRoomStateChanged
发布回调onUserPublishStreamrtcRoom:onUserPublishStream:type:rtcRoom:onUserPublishStream:type:onUserPublishStreamonUserPublishStreamonUserPublishStreamonUserPublishStreamOnUserPublishStreamEventHandler
取消发布回调onUserUnpublishStreamrtcRoom:onUserUnpublishStream:type:reason:rtcRoom:onUserUnpublishStream:type:reason:onUserUnpublishStreamonUserUnpublishStreamonUserUnpublishStreamonUserUnpublishStreamOnUserUnPublishStreamEventHandler
离开房间leaveRoomleaveRoomleaveRoomleaveRoomleaveRoomleaveRoomleaveRoomLeaveRoom
销毁房间destroydestroydestroydestroydestroydestroydestroyDestroy
销毁音视频引擎destroyRTCVideodestroyRTCVideodestroyRTCVideodestroyRTCVideodestroyRTCVideodestroyRTCVideodestroyRelease
常见问题
Q1: 同一用户如何在多个房间内同时发送音视频流?

A:对于一个音视频引擎实例,即使加入了多个房间,也仅能同时在其中的一个房间中发布音视频流。如果需要在多个房间中同时发布音视频流,参看 跨房间转发媒体流 。

Q2: 如何处理 Token 相关错误?

A:确保每个房间的 token 使用了对应的 roomId 生成。更多 Token 相关问题,参见 Token 使用常见问题