同一个用户可以加入多个房间,分别订阅和接收这些房间中的音视频流,并在其中一个房间中发布音视频。也可以发送和接收实时消息。
适用场景
大班小组课:主讲老师在大班房间内讲课,学生在该房间内听讲,同时可以在小组房间交流提问,并由助教老师答疑。
游戏场景:玩家可以在已加入的小队房间内和其他小队成员进行语音互动,同时能收听到世界房间内的广播音频。
会议场景:云端会议过程中,主持人可以发起分组讨论。参会者无需离开原先的会议,即可在各自小组中进行音视频互动,不同讨论组互不干扰。
前提条件
你已经集成 RTC SDK v3.43 或更新的版本,实现了基本的音视频通话。
支持多房间功能的 SDK 详见API 及回调。
功能实现
在集成 RTC SDK,并完成业务逻辑代码时,你已发现,音视频引擎类和房间类两个常用的主调类有明显的功能区分:
要让一个用户加入多个房间,可以通过一个音视频引擎实例,创建多个房间实例,并为这些实例设置不同的房间 ID。同一个用户在多个房间中,可以订阅在这些房间中发布的音视频流。
本文以加入两个房间为例,你可以按照相同流程,让用户加入更多房间。
以下时序图以 Android SDK 中的 API 名称为例。不同端的 SDK 中 API 或回调名称可能略有不同,以 API 及回调为准。
1. 创建引擎类
创建和初始化一个音视频引擎类。
参考 构建 RTC 应用 获取详细步骤。
// APP_ID: 已经在控制台申请的app_id
// videoEventHandler: 用于接收 RTCVideo 回调的接口实例
rtcVideo = RTCVideo.createRTCVideo(this, Constants.APP_ID, videoEventHandler, null, null);
// kAppID: 已经在控制台申请的app_id
// delegate: 用于接收 RTCVideo 回调的接口实例
// parameters: 私有参数,可以填空
self.rtcVideo = ByteRTCVideo.createRTCVideo(kAppID, delegate: self, parameters: [:])
// app_id: 已经在控制台申请的app_id
// handler: 用于接收 RTCVideo 回调的接口实例
// parameters: 私有参数,可以填 nullptr
bytertc::IRTCVideo *video = bytertc::createRTCVideo("app_id", handler, nullptr);
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);
}
// 开启本地音视频采集
self.rtcVideo?.startVideoCapture()
self.rtcVideo?.startAudioCapture()
// 设置本地渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = self.localView.videoView
canvas.renderMode = .hidden
self.rtcVideo?.setLocalVideoCanvas(.main, withCanvas: canvas);
//开始音视频采集
video->startAudioCapture();
video->startVideoCapture();
//设置本地渲染窗口
bytertc::VideoCanvas cas;
cas.view = view;
video->setLocalVideoCanvas(bytertc::kStreamIndexMain, cas);
3. 创建房间实例
创建房间实例后,你可以加入房间发布和订阅音视频流。建议设置房间回调接口,以便监听房间和音视频流的状态回调。
继续调用 createRTCRoom 并传入不同房间 ID,以创建多个房间实例,并分别加入这些房间。
// 创建房间1
rtcRoom1 = rtcVideo.createRTCRoom(roomID);
rtcRoom1.setRTCRoomEventHandler(firstRoomEventHandler);
// 创建房间2
rtcRoom2 = rtcVideo.createRTCRoom(roomID);
rtcRoom2.setRTCRoomEventHandler(secondRoomEventHandler);
//创建房间1
self.rtcRoom1 = self.rtcVideo?.createRTCRoom(roomId1)
self.rtcRoom1?.delegate = self
//创建房间2
self.rtcRoom2 = self.rtcVideo?.createRTCRoom(roomId2)
self.rtcRoom2?.delegate = self
//创建房间1
bytertc::IRTCRoom *room1 = video->createRTCRoom("roomid1");
room1->setRTCRoomEventHandler(room_handler);
//创建房间2
bytertc::IRTCRoom *room2 = video->createRTCRoom("roomid2");
room2->setRTCRoomEventHandler(room_handler);
4. 加入 RTC 房间
- 加入每个房间时,默认自动订阅房间中的音视频流。
// 用户信息
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, "");
// 一个用户只能在一个房间中发布视频流。
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);
//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)
// 一个用户只能在一个房间中发布视频流。
// 获取 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)
//room1 进房,默认可以发布音视频流
bytertc::RTCRoomConfig config1, config2;
bytertc::UserInfo userinfo1, userinfo2;
userinfo1.uid = "name1";
int ret = room1->joinRoom("token1", userinfo1, config1);
// 一个用户只能在一个房间中发布视频流。
config2.is_auto_publish = false;
userinfo2.uid= "name2";
ret = room2->joinRoom("token2", userinfo2, config2);
- 监听进房状态,处理进房失败。
public void onRoomStateChanged(String roomId, String uid, int state, String extraInfo) {
if (state == 0) { //进房成功
} else if (state == -1000) { //token错误
} else {// .... 其他错误
}
}
func rtcRoom(_ rtcRoom: ByteRTCRoom, onRoomStateChanged roomId: String, withUid uid: String, state: Int, extraInfo: String) {
if (state == 0) { //进房成功
} else if (state == -1000) { //token错误
} else {// .... 其他错误
}
}
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 {// .... 其他错误
}
}
- 订阅端在接收到其他用户发布/取消发布音视频流回调时,设置渲染视图。
@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);
});
}
// 远端用户发布流
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)
}
}
//远端用户发布流
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);
}
5. 离开 RTC 房间
用户结束通话时,退出各个房间,并销毁本地房间实例。
rtcRoom.leaveRoom();
rtcRoom.destroy();
rtcRoom = null;
self?.rtcRoom?.leaveRoom()
self?.rtcRoom?.destroy()
self?.rtcRoom = nil
room->leaveRoom();
room->destroy();
6. 销毁引擎类
在销毁所有房间实例后可销毁音视频引擎。
RTCVideo.destroyRTCVideo();
ByteRTCVideo.destroyRTCVideo()
self.rtcVideo = nil
bytertc::destroyRTCVideo();
示例项目
参考以下项目获取完整代码。在示例项目中,本端用户加入了两个不同的 RTC 房间。你可以按照相同流程,让用户加入更多房间。
API 及回调
你可以根据上文的描述和示例,使用以下客户端 SDK,在不同的端上实现同一用户加入多房间功能。
说明:表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。
常见问题
Q1: 同一用户如何在多个房间内同时发送音视频流?
A:对于一个音视频引擎实例,即使加入了多个房间,也仅能同时在其中的一个房间中发布音视频流。如果需要在多个房间中同时发布音视频流,参看 跨房间转发媒体流 。
Q2: 如何处理 Token 相关错误?
A:确保每个房间的 token 使用了对应的 roomId 生成。更多 Token 相关问题,参见 Token 使用常见问题