一、常规跨房 PK 连麦方案
适用场景:两个或多个房间 PK,房间主播数量较少的简单跨房连麦场景。
二、方案原理
默认情况下,只有同一个房间内的用户之间音视频可以互通,不同的房间之间的音视频流是相互隔离的。通过跨房连麦,可以将另一个房间中某个主播音视频流发布到自己所在的房间中,与此同时也会将自己的音视频流发布到目标主播的房间中。让身处两个不同房间中的主播进行跨房间的音视频流分享,从而让每个房间中的观众都能观看到这两个主播的音视频。
三、实现流程
当房间“101”中的主播 A 通过 ConnectOtherRoom 跟房间“102”中的主播 B 建立跨房通话后:
·房间“101”中的用户都会收到主播 B 的 onRemoteUserEnterRoom(B) 和 onUserVideoAvailable(B,true) 这两个事件回调,即房间“101”中的用户都可以订阅主播 B 的音视频。
·房间“102”中的用户都会收到主播 A 的 onRemoteUserEnterRoom(A) 和 onUserVideoAvailable(A,true) 这两个事件回调,即房间“102”中的用户都可以订阅主播 A 的音视频。
四、实时互动跨房连麦
RTC 场景下的跨房连麦 PK 流程整体简单,主播和跨房连麦主播互相拉取 RTC 单流,观众同时拉取主播和跨房连麦主播的 RTC 单流,观众可独立控制主播和跨房连麦主播媒体流订阅逻辑。实时互动场景跨房连麦流程如下所示。
注意:实时互动跨房连麦场景下,房间内观众可独立控制跨房连麦主播媒体流的订阅逻辑,也可由主播 更改跨房主播在本房间的上行能力。
五、旁路直播跨房连麦
旁路直播场景下的跨房连麦 PK 流程相对复杂,主播和跨房连麦主播互相拉取 RTC 单流,CDN 观众拉取主播和跨房连麦主播的旁路混流,观众无法独立控制主播和跨房连麦主播媒体流订阅逻辑。旁路直播场景跨房连麦流程如下所示。
注意:旁路直播跨房连麦场景下,CDN 观众无法独立控制跨房连麦主播媒体流的订阅逻辑,需由主播通过 更新发布媒体流 来统一控制。
六、示例代码
1. 任意一方发起跨房 PK 连麦。
AndroidiOSpublic void connectOtherRoom(String roomId, String userId) { try { JSONObject jsonObj = new JSONObject(); // 以字符串房间号为例,数字房间号 key:roomId jsonObj.put("strRoomId", roomId); jsonObj.put("userId", userId); mTRTCCloud.ConnectOtherRoom(jsonObj.toString()); } catch (JSONException e) { e.printStackTrace(); }}// 请求跨房连麦的结果回调@Overridepublic void onConnectOtherRoom(String userId, int errCode, String errMsg) { // userId: 要跨房连麦的另一个房间中的主播的用户 ID // errCode: 错误码,ERR_NULL 代表请求成功 // errMsg: 错误信息}- (void)connectOtherRoom:(NSString *)roomId { NSMutableDictionary *jsonDict = [[NSMutableDictionary alloc] init]; // 以字符串房间号为例,数字房间号 key:roomId [jsonDict setObject:roomId forKey:@"strRoomId"]; [jsonDict setObject:self.userId forKey:@"userId"]; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:NSJSONWritingPrettyPrinted error:nil]; NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; [self.trtcCloud connectOtherRoom:jsonString];}// 请求跨房连麦的结果回调- (void)onConnectOtherRoom:(NSString *)userId errCode:(TXLiteAVError)errCode errMsg:(NSString *)errMsg { // userId: 要跨房连麦的另一个房间中的主播的用户 ID // errCode: 错误码,ERR_NULL 代表请求成功 // errMsg: 错误信息}
2. 两个房间中的所有用户都会收到来自另一个房间中的 PK 主播的音视频流可用回调。
AndroidiOS@Overridepublic void onUserAudioAvailable(String userId, boolean available) { // 某远端用户发布/取消了自己的音频 // 在自动订阅模式下,你无需做任何操作,SDK 会自动播放远端用户音频}@Overridepublic void onUserVideoAvailable(String userId, boolean available) { // 某远端用户发布/取消了主路视频画面 if (available) { // 订阅远端用户的视频流,并绑定视频渲染控件 mTRTCCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, view); } else { // 停止订阅远端用户的视频流,并释放渲染控件 mTRTCCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG); }}- (void)onUserAudioAvailable:(NSString *)userId available:(BOOL)available { // 某远端用户发布/取消了自己的音频 // 在自动订阅模式下,你无需做任何操作,SDK 会自动播放远端用户音频}- (void)onUserVideoAvailable:(NSString *)userId available:(BOOL)available { // 某远端用户发布/取消了主路视频画面 if (available) { // 订阅远端用户的视频流,并绑定视频渲染控件 [self.trtcCloud startRemoteView:userId streamType:TRTCVideoStreamTypeBig view:self.remoteView]; } else { // 停止订阅远端用户的视频流,并释放渲染控件 [self.trtcCloud stopRemoteView:userId streamType:TRTCVideoStreamTypeBig]; }}
3. 任意一方退出跨房 PK 连麦。
AndroidiOS// 退出跨房连麦mTRTCCloud.DisconnectOtherRoom();// 退出跨房连麦的结果回调@Overridepublic void onDisConnectOtherRoom(int errCode, String errMsg) { super.onDisConnectOtherRoom(errCode, errMsg);}// 退出跨房连麦[self.trtcCloud disconnectOtherRoom];// 退出跨房连麦的结果回调- (void)onDisconnectOtherRoom:(TXLiteAVError)errCode errMsg:(NSString *)errMsg {}
注意:跨房 PK 连麦的发起端和接收端任意一端均可调用 DisconnectOtherRoom 结束跨房 PK 连麦。
七、服务端跨房 PK 连麦方案
适用场景:多个房间 PK,每个房间有多个主播的纯服务端跨房连麦场景。
八、方案原理
服务端启动多个混流转推任务,每个转推任务都会拉起一个 Agent 机器人用户进入己方 TRTC 房间进行拉流,同时会拉起一个或多个 Feed 机器人用户将混合的音视频流回推到其他参与跨房 PK 连麦的 TRTC 房间。这样不同房间里的用户就可以通过订阅其他房间混流回推的音视频流,从而实现跨房 PK 连麦。
九、实时互动跨房连麦
1. 房间 A 主播向房间 B 主播和房间 N 主播发起跨房 PK 请求(业务信令)。
2. 房间 B 主播和房间 N 主播同意跨房 PK 请求(业务信令)。
3. 业务后台同时启动 N 个混流回推房间任务 StartPublishCdnStream。
·任务一:A_Agent 机器人接收 A 房间主播媒体流,经 TRTC 后台混流后由 A_Feed 机器人回推到 B 房间和 N 房间。
·任务二:B_Agent 机器人接收 B 房间主播媒体流,经 TRTC 后台混流后由 B_Feed 机器人回推到 A 房间和 N 房间。
·任务 N:N_Agent 机器人接收 N 房间主播媒体流,经 TRTC 后台混流后由 N_Feed 机器人回推到 A 房间和 B 房间。
4. 房间 A、房间 B、房间 N 的用户互相拉取房间中混流回推的音视频流,开始进行跨房 PK。
5. 跨房 PK 结束,业务后台通过 TaskId 停止 N 个混流回推房间任务 StopPublishCdnStream。
十、旁路直播跨房连麦
1. 业务后台在每个旁路直播间启动一个旁路转推 CDN 任务 StartPublishCdnStream。
2. 房间 A 主播向房间 B 主播和房间 N 主播发起跨房 PK 请求(业务信令)。
3. 房间 B 主播和房间 N 主播同意跨房 PK 请求(业务信令)。
4. 业务后台同时启动 N 个混流回推房间任务 StartPublishCdnStream。
·任务一:A_Agent 机器人接收 A 房间主播媒体流,经 TRTC 后台混流后由 A_Feed 机器人回推到 B 房间和 N 房间。
·任务二:B_Agent 机器人接收 B 房间主播媒体流,经 TRTC 后台混流后由 B_Feed 机器人回推到 A 房间和 N 房间。
·任务 N:N_Agent 机器人接收 N 房间主播媒体流,经 TRTC 后台混流后由 N_Feed 机器人回推到 A 房间和 B 房间。
5. 房间 A、房间 B、房间 N 的用户互相拉取房间中混流回推的音视频流,开始进行跨房 PK。
6. 业务后台更新参与跨房 PK 的房间中原有的旁路转推 CDN 任务 UpdatePublishCdnStream,混合其他房间回推的音视频流。
7. 跨房 PK 结束,业务后台通过 TaskId 停止 N 个混流回推房间任务 StopPublishCdnStream。
8. 业务后台更新参与跨房 PK 的房间中原有的旁路转推 CDN 任务 UpdatePublishCdnStream,剔除其他房间回推的音视频流。
十一、示例代码
下面以纯音频场景为例,展示跨房 PK 混流回推房间任务的参数体示例。
任务一任务二任务 N{ "SdkAppId": 1400000000, "RoomId": "A", "RoomIdType": 1, "AgentParams": { "UserId": "A_Agent", "UserSig": "eJwtjMEKgkAUAP9lz2Hv6b40oU…", "MaxIdleTime": 50 }, "WithTranscoding": 1, "AudioParams": { "AudioEncode": { "Codec": 0, "SampleRate": 48000, "Channel": 2, "BitRate": 64 } }, "FeedBackRoomParams": [ { "RoomId": "B", "RoomIdType": 1, "UserId": "A_Feed", "UserSig": "eJwtzEELgkAUBOD-sldD3745…" }, { "RoomId": "N", "RoomIdType": 1, "UserId": "A_Feed", "UserSig": "eJwtzEELgkAUBOD-sldD3745…" } ]}{ "SdkAppId": 1400000000, "RoomId": "B", "RoomIdType": 1, "AgentParams": { "UserId": "B_Agent", "UserSig": "eJwtjMEKgkAUAP9lz2Hv6b40oU…", "MaxIdleTime": 50 }, "WithTranscoding": 1, "AudioParams": { "AudioEncode": { "Codec": 0, "SampleRate": 48000, "Channel": 2, "BitRate": 64 } }, "FeedBackRoomParams": [ { "RoomId": "A", "RoomIdType": 1, "UserId": "B_Feed", "UserSig": "eJwtzEELgkAUBOD-sldD3745…" }, { "RoomId": "N", "RoomIdType": 1, "UserId": "B_Feed", "UserSig": "eJwtzEELgkAUBOD-sldD3745…" } ]}{ "SdkAppId": 1400000000, "RoomId": "N", "RoomIdType": 1, "AgentParams": { "UserId": "N_Agent", "UserSig": "eJwtjMEKgkAUAP9lz2Hv6b40oU…", "MaxIdleTime": 50 }, "WithTranscoding": 1, "AudioParams": { "AudioEncode": { "Codec": 0, "SampleRate": 48000, "Channel": 2, "BitRate": 64 } }, "FeedBackRoomParams": [ { "RoomId": "A", "RoomIdType": 1, "UserId": "N_Feed", "UserSig": "eJwtzEELgkAUBOD-sldD3745…" }, { "RoomId": "B", "RoomIdType": 1, "UserId": "N_Feed", "UserSig": "eJwtzEELgkAUBOD-sldD3745…" } ]}
注意:纯音频场景下,TRTC 后台会默认混合房间内所有主播音频流,亦可通过音频参数 McuAudioParams 指定音频混流黑白名单。
十二、子实例跨房 PK 连麦方案
适用场景:多个房间 PK,每个房间有一个或多个主播的复杂跨房连麦场景。
十三、方案原理
TRTC SDK 可以通过创建子实例的方式实现跨房 PK 连麦,此方案不限参与 PK 的房间数量,便于后期多个房间 PK 的业务拓展。开始跨房 PK 后,每个参与 PK 的主播均在本地创建一个子实例进入一个新的 PK 房间,这样不同房间的主播即可通过子实例推拉流实现音视频互通。
十四、实时互动跨房连麦
1. 房间 A 主播向房间 B 主播和房间 N 主播发起跨房 PK 请求(业务信令)。
2. 房间 B 主播和房间 N 主播同意跨房 PK 请求(业务信令)。
3. 主播 A、主播 B、主播 N 分别创建一个子实例,进入一个新的 PK 房间进行推拉流。
4. 主播 A、主播 B、主播 N 的主实例分别启动一个混流回推房间任务 startPublishMediaStream,混合 PK 房间内所有子实例的音视频流并回推到自己房间,同时停止主实例推流。
5. 各房间观众用户拉取房间中混流回推的音视频流,各房间主播用户通过子实例拉取其他房间主播的音视频流,开始进行跨房 PK。
6. 跨房 PK 结束,各房间主播的主实例重启本地推流,并停止混流回推房间任务 stopPublishMediaStream,子实例退房并销毁。
十五、旁路直播跨房连麦
1. 主播 A、主播 B、主播 N 的主实例分别启动一个旁路转推 CDN 任务 startPublishMediaStream。
2. 房间 A 主播向房间 B 主播和房间 N 主播发起跨房 PK 请求(业务信令)。
3. 房间 B 主播和房间 N 主播同意跨房 PK 请求(业务信令)。
4. 主播 A、主播 B、主播 N 分别创建一个子实例,进入一个新的 PK 房间进行推拉流。
5. 主播 A、主播 B、主播 N 的主实例分别更新原有的旁路转推任务 updatePublishMediaStream,混合 PK 房间内所有子实例的音视频流并转推到直播 CDN,同时停止主实例推流。
6. 各房间观众用户拉取 CDN 流观看,各房间主播用户通过子实例拉取其他房间主播的音视频流,开始进行跨房 PK。
7. 跨房 PK 结束,各房间主播的主实例重启本地推流,并更新原混流转推任务 updatePublishMediaStream,恢复主实例单流旁路转推,子实例退房并销毁。
十六、示例代码
1. 创建子实例,进入一个新的 PK 房间进行推拉流。
AndroidiOS// 初始化 TRTC 主实例TRTCCloud mainCloud = TRTCCloud.sharedInstance(context);// 创建 TRTC 子实例TRTCCloud subCloud = mainCloud.createSubCloud();// 添加子实例事件监听subCloud.addListener(trtcSdkListener);// 子实例进入 PK 房间TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();params.sdkAppId = SDKAppId;params.userId = "Sub_" + UserId;params.userSig = UserSig;params.role = TRTCCloudDef.TRTCRoleAnchor;params.strRoomId = PK_RoomId;subCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE);// 子实例开启本地音频采集和发布subCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);// 子实例开启本地视频预览和发布subCloud.startLocalPreview(mIsFrontCamera, mTxcvvAnchorPreviewView);// 来自 TRTC SDK 的各类事件通知private TRTCCloudListener trtcSdkListener = new TRTCCloudListener() { @Override public void onEnterRoom(long result) { if (result > 0) { // result 代表加入房间所消耗的时间(毫秒) Log.d(TAG, "Enter room succeed!"); } else { // result 代表进房失败的错误码 Log.d(TAG, "Enter room failed!"); } } @Override public void onUserAudioAvailable(String userId, boolean available) { // 某远端用户发布/取消了自己的音频 // 在自动订阅模式下,你无需做任何操作,SDK 会自动播放远端用户音频 } @Override public void onUserVideoAvailable(String userId, boolean available) { // 某远端用户发布/取消了主路视频画面 if (available) { // 订阅远端用户的视频流,并绑定视频渲染控件 subCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, remoteView); } else { // 停止订阅远端用户的视频流,并释放渲染控件 subCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG); } }};// 初始化 TRTC 主实例TRTCCloud *mainCloud = [TRTCCloud sharedInstance];// 创建 TRTC 子实例TRTCCloud *subCloud = [mainCloud createSubCloud];// 添加子实例事件监听subCloud.delegate = self;// 子实例进入 PK 房间TRTCParams *params = [[TRTCParams alloc] init];params.sdkAppId = self.SDKAppId;params.userId = [NSString stringWithFormat:@"Sub_%@", self.UserId];params.userSig = self.UserSig;params.role = TRTCRoleAnchor;params.strRoomId = self.PK_RoomId;[subCloud enterRoom:params appScene:TRTCAppSceneLIVE];// 子实例开启本地音频采集和发布[subCloud startLocalAudio:TRTCAudioQualityDefault];// 子实例开启本地视频预览和发布[subCloud startLocalPreview:self.isFrontCamera view:self.anchorPreviewView];// 来自 TRTC SDK 的各类事件通知- (void)onEnterRoom:(NSInteger)result { if (result > 0) { // result 代表加入房间所消耗的时间(毫秒) [self toastTip:@"Enter room succeed!"]; } else { // result 代表进房失败的错误码 [self toastTip:@"Enter room failed!"]; }}- (void)onUserAudioAvailable:(NSString *)userId available:(BOOL)available { // 某远端用户发布/取消了自己的音频 // 在自动订阅模式下,你无需做任何操作,SDK 会自动播放远端用户音频}- (void)onUserVideoAvailable:(NSString *)userId available:(BOOL)available { // 某远端用户发布/取消了主路视频画面 if (available) { // 订阅远端用户的视频流,并绑定视频渲染控件 [subCloud startRemoteView:userId streamType:TRTCVideoStreamTypeBig view:self.remoteView]; } else { // 停止订阅远端用户的视频流,并释放渲染控件 [subCloud stopRemoteView:userId streamType:TRTCVideoStreamTypeBig]; }}
2. (实时互动跨房连麦)启动混流回推房间任务,同时停止主实例推流。
AndroidiOS// 开始发布混合媒体流到 TRTC 房间public void startPublishMediaToRoom(List<String> mixUserList) { // 媒体流发布的目标地址 TRTCCloudDef.TRTCPublishTarget target = new TRTCCloudDef.TRTCPublishTarget(); // 目标地址设定为混流回推到房间 target.mode = TRTCCloudDef.TRTC_PublishMixStream_ToRoom; target.mixStreamIdentity.strRoomId = RoomId; // 混流机器人用户名不能与房间内其他用户重复 target.mixStreamIdentity.userId = UserId + "_robot"; // 设置媒体流编码输出参数 TRTCCloudDef.TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCCloudDef.TRTCStreamEncoderParam(); trtcStreamEncoderParam.audioEncodedChannelNum = 1; trtcStreamEncoderParam.audioEncodedKbps = 50; trtcStreamEncoderParam.audioEncodedCodecType = 0; trtcStreamEncoderParam.audioEncodedSampleRate = 48000; trtcStreamEncoderParam.videoEncodedFPS = 15; trtcStreamEncoderParam.videoEncodedGOP = 2; trtcStreamEncoderParam.videoEncodedKbps = 1300; trtcStreamEncoderParam.videoEncodedWidth = 540; trtcStreamEncoderParam.videoEncodedHeight = 960; // 媒体流转码配置参数 TRTCCloudDef.TRTCStreamMixingConfig trtcStreamMixingConfig = new TRTCCloudDef.TRTCStreamMixingConfig(); if (mixUserList != null) { ArrayList<TRTCCloudDef.TRTCUser> audioMixUserList = new ArrayList<>(); ArrayList<TRTCCloudDef.TRTCVideoLayout> videoLayoutList = new ArrayList<>(); for (int i = 0; i < mixUserList.size() && i < 16; i++) { TRTCCloudDef.TRTCUser user = new TRTCCloudDef.TRTCUser(); user.strRoomId = PK_RoomId; user.userId = mixUserList.get(i); audioMixUserList.add(user); TRTCCloudDef.TRTCVideoLayout videoLayout = new TRTCCloudDef.TRTCVideoLayout(); if (mixUserList.get(i).equals("Sub_" + UserId)) { // 本地主播画面布局 videoLayout.x = 0; videoLayout.y = 0; videoLayout.width = 540; videoLayout.height = 960; videoLayout.zOrder = 0; } else { // PK 主播画面布局 videoLayout.x = 400; videoLayout.y = 5 + i * 245; videoLayout.width = 135; videoLayout.height = 240; videoLayout.zOrder = 1; } videoLayout.fixedVideoUser = user; videoLayout.fixedVideoStreamType = TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG; videoLayoutList.add(videoLayout); } // 指定转码流中的每一路输入音频的信息 trtcStreamMixingConfig.audioMixUserList = audioMixUserList; // 指定混合画面的中每一路视频画面的位置、大小、图层以及流类型等信息 trtcStreamMixingConfig.videoLayoutList = videoLayoutList; } // 开始发布媒体流 mainCloud.startPublishMediaStream(target, trtcStreamEncoderParam, trtcStreamMixingConfig);}// 开始发布媒体流的事件回调@Overridepublic void onStartPublishMediaStream(String taskId, int code, String message, Bundle extraInfo) { // taskId: 当请求成功时,TRTC 后台会在回调中提供给你这项任务的 taskId,后续你可以通过该 taskId 结合 updatePublishMediaStream 和 stopPublishMediaStream 进行更新和停止 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 主实例停止推流 mainCloud.stopLocalAudio(); mainCloud.stopLocalPreview(); }}// 开始发布混合媒体流到 TRTC 房间- (void)startPublishMediaToRoom { // 媒体流发布的目标地址 TRTCPublishTarget* target = [[TRTCPublishTarget alloc] init]; // 目标地址设定为混流回推到房间 target.mode = TRTCPublishMixStreamToRoom; TRTCUser *mixStreamIdentity = [[TRTCUser alloc] init]; mixStreamIdentity.strRoomId = self.RoomId; // 混流机器人用户名不能与房间内其他用户重复 mixStreamIdentity.userId = [self.UserId stringByAppendingString:@"_robot"]; target.mixStreamIdentity = mixStreamIdentity; // 设置媒体流编码输出参数 TRTCStreamEncoderParam* encoderParam = [[TRTCStreamEncoderParam alloc] init]; encoderParam.audioEncodedSampleRate = 48000; encoderParam.audioEncodedChannelNum = 1; encoderParam.audioEncodedKbps = 50; encoderParam.audioEncodedCodecType = 0; encoderParam.videoEncodedWidth = 540; encoderParam.videoEncodedHeight = 960; encoderParam.videoEncodedFPS = 15; encoderParam.videoEncodedGOP = 2; encoderParam.videoEncodedKbps = 1300; TRTCStreamMixingConfig *config = [[TRTCStreamMixingConfig alloc] init]; if (self.mixUserList.count) { NSMutableArray<TRTCUser *> *userList = [NSMutableArray array]; NSMutableArray<TRTCVideoLayout *> *layoutList = [NSMutableArray array]; for (int i = 1; i < MIN(self.mixUserList.count, 16); i++) { TRTCUser *user = [[TRTCUser alloc] init]; user.strRoomId = self.PK_RoomId; user.userId = self.mixUserList[i]; [userList addObject:user]; TRTCVideoLayout *layout = [[TRTCVideoLayout alloc] init]; if ([self.mixUserList[i] isEqualToString:[NSString stringWithFormat:@"Sub_%@", self.UserId]]) { // 本地主播画面布局 layout.rect = CGRectMake(0, 0, 540, 960); layout.zOrder = 0; } else { // PK 主播画面布局 layout.rect = CGRectMake(400, 5 + i * 245, 135, 240); layout.zOrder = 1; } layout.fixedVideoUser = user; layout.fixedVideoStreamType = TRTCVideoStreamTypeBig; [layoutList addObject:layout]; } // 指定转码流中的每一路输入音频的信息 config.audioMixUserList = [userList copy]; // 指定混合画面的中每一路视频画面的位置、大小、图层以及流类型等信息 config.videoLayoutList = [layoutList copy]; } // 开始发布媒体流 [self.mainCloud startPublishMediaStream:target encoderParam:encoderParam mixingConfig:config];}// 开始发布媒体流的事件回调- (void)onStartPublishMediaStream:(NSString *)taskId code:(int)code message:(NSString *)message extraInfo:(NSDictionary *)extraInfo { // taskId: 当请求成功时,TRTC 后台会在回调中提供给你这项任务的 taskId,后续你可以通过该 taskId 结合 updatePublishMediaStream 和 stopPublishMediaStream 进行更新和停止 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 主实例停止推流 [self.mainCloud stopLocalAudio]; [self.mainCloud stopLocalPreview]; }}
3. (旁路直播跨房连麦)更新原有旁路转推任务,同时停止主实例推流。
AndroidiOS// 更新发布混合媒体流到直播 CDNpublic void updatePublishMediaToCDN(String streamName, List<String> mixUserList, String taskId) { // 设定推流地址过期时间 long txTime = (System.currentTimeMillis() / 1000) + (24 * 60 * 60); // 生成鉴权信息,getSafeUrl 方法可在云直播控制台-域名管理-推流配置-推流地址示例代码获取 String secretParam = UrlHelper.getSafeUrl(LIVE_URL_KEY, streamName, txTime); // 媒体流发布的目标地址 TRTCCloudDef.TRTCPublishTarget target = new TRTCCloudDef.TRTCPublishTarget(); // 目标地址设定为混流转推到 CDN target.mode = TRTCCloudDef.TRTC_PublishMixStream_ToCdn; TRTCCloudDef.TRTCPublishCdnUrl cdnUrl = new TRTCCloudDef.TRTCPublishCdnUrl(); // 拼接发布到直播服务商的推流地址(RTMP 格式) cdnUrl.rtmpUrl = "rtmp://" + PUSH_DOMAIN + "/live/" + streamName + "?" + secretParam; // 腾讯云直播服务为 true,第三方直播服务为 false cdnUrl.isInternalLine = true; // 可以添加多个 CDN 推流地址 target.cdnUrlList.add(cdnUrl); // 设置媒体流编码输出参数 TRTCCloudDef.TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCCloudDef.TRTCStreamEncoderParam(); trtcStreamEncoderParam.audioEncodedChannelNum = 1; trtcStreamEncoderParam.audioEncodedKbps = 50; trtcStreamEncoderParam.audioEncodedCodecType = 0; trtcStreamEncoderParam.audioEncodedSampleRate = 48000; trtcStreamEncoderParam.videoEncodedFPS = 15; trtcStreamEncoderParam.videoEncodedGOP = 2; trtcStreamEncoderParam.videoEncodedKbps = 1300; trtcStreamEncoderParam.videoEncodedWidth = 540; trtcStreamEncoderParam.videoEncodedHeight = 960; // 媒体流转码配置参数 TRTCCloudDef.TRTCStreamMixingConfig trtcStreamMixingConfig = new TRTCCloudDef.TRTCStreamMixingConfig(); if (mixUserList != null) { ArrayList<TRTCCloudDef.TRTCUser> audioMixUserList = new ArrayList<>(); ArrayList<TRTCCloudDef.TRTCVideoLayout> videoLayoutList = new ArrayList<>(); for (int i = 0; i < mixUserList.size() && i < 16; i++) { TRTCCloudDef.TRTCUser user = new TRTCCloudDef.TRTCUser(); user.strRoomId = PK_RoomId; user.userId = mixUserList.get(i); audioMixUserList.add(user); TRTCCloudDef.TRTCVideoLayout videoLayout = new TRTCCloudDef.TRTCVideoLayout(); if (mixUserList.get(i).equals("Sub_" + UserId)) { // 本地主播画面布局 videoLayout.x = 0; videoLayout.y = 0; videoLayout.width = 540; videoLayout.height = 960; videoLayout.zOrder = 0; } else { // PK 主播画面布局 videoLayout.x = 400; videoLayout.y = 5 + i * 245; videoLayout.width = 135; videoLayout.height = 240; videoLayout.zOrder = 1; } videoLayout.fixedVideoUser = user; videoLayout.fixedVideoStreamType = TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG; videoLayoutList.add(videoLayout); } // 指定转码流中的每一路输入音频的信息 trtcStreamMixingConfig.audioMixUserList = audioMixUserList; // 指定混合画面的中每一路视频画面的位置、大小、图层以及流类型等信息 trtcStreamMixingConfig.videoLayoutList = videoLayoutList; } // 更新发布媒体流 mainCloud.updatePublishMediaStream(taskId, target, trtcStreamEncoderParam, trtcStreamMixingConfig);}// 更新媒体流的事件回调@Overridepublic void onUpdatePublishMediaStream(String taskId, int code, String message, Bundle extraInfo) { // 你调用媒体流发布接口 (updatePublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次更新请求 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 主实例停止推流 mainCloud.stopLocalAudio(); mainCloud.stopLocalPreview(); }}// 更新发布混合媒体流到直播 CDN- (void)updatePublishMediaToCDN { NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0]; // 设定推流地址过期时间 NSTimeInterval time = [date timeIntervalSince1970] + (24 * 60 * 60); // 生成鉴权信息,getSafeUrl 方法可在云直播控制台-域名管理-推流配置-推流地址示例代码获取 NSString *secretParam = [self getSafeUrl:LIVE_URL_KEY streamName:self.streamName time:time]; // 媒体流发布的目标地址 TRTCPublishTarget* target = [[TRTCPublishTarget alloc] init]; // 目标地址设定为混流转推到 CDN target.mode = TRTCPublishMixStreamToCdn; TRTCPublishCdnUrl* cdnUrl = [[TRTCPublishCdnUrl alloc] init]; // 拼接发布到直播服务商的推流地址(RTMP 格式) cdnUrl.rtmpUrl = [NSString stringWithFormat:@"rtmp://%@/live/%@?%@", PUSH_DOMAIN, self.streamName, secretParam]; // 腾讯云直播推流地址为 true,第三方为 false cdnUrl.isInternalLine = YES; NSMutableArray* cdnUrlList = [NSMutableArray array]; // 可以添加多个 CDN 推流地址 [cdnUrlList addObject:cdnUrl]; target.cdnUrlList = cdnUrlList; // 设置媒体流编码输出参数 TRTCStreamEncoderParam* encoderParam = [[TRTCStreamEncoderParam alloc] init]; encoderParam.audioEncodedSampleRate = 48000; encoderParam.audioEncodedChannelNum = 1; encoderParam.audioEncodedKbps = 50; encoderParam.audioEncodedCodecType = 0; encoderParam.videoEncodedWidth = 540; encoderParam.videoEncodedHeight = 960; encoderParam.videoEncodedFPS = 15; encoderParam.videoEncodedGOP = 2; encoderParam.videoEncodedKbps = 1300; TRTCStreamMixingConfig *config = [[TRTCStreamMixingConfig alloc] init]; if (self.mixUserList.count) { NSMutableArray<TRTCUser *> *userList = [NSMutableArray array]; NSMutableArray<TRTCVideoLayout *> *layoutList = [NSMutableArray array]; for (int i = 1; i < MIN(self.mixUserList.count, 16); i++) { TRTCUser *user = [[TRTCUser alloc] init]; user.strRoomId = self.PK_RoomId; user.userId = self.mixUserList[i]; [userList addObject:user]; TRTCVideoLayout *layout = [[TRTCVideoLayout alloc] init]; if ([self.mixUserList[i] isEqualToString:[NSString stringWithFormat:@"Sub_%@", self.UserId]]) { // 本地主播画面布局 layout.rect = CGRectMake(0, 0, 540, 960); layout.zOrder = 0; } else { // PK 主播画面布局 layout.rect = CGRectMake(400, 5 + i * 245, 135, 240); layout.zOrder = 1; } layout.fixedVideoUser = user; layout.fixedVideoStreamType = TRTCVideoStreamTypeBig; [layoutList addObject:layout]; } // 指定转码流中的每一路输入音频的信息 config.audioMixUserList = [userList copy]; // 指定混合画面的中每一路视频画面的位置、大小、图层以及流类型等信息 config.videoLayoutList = [layoutList copy]; } // 更新发布媒体流 [self.mainCloud updatePublishMediaStream:self.taskId publishTarget:target encoderParam:encoderParam mixingConfig:config];}// 更新媒体流的事件回调- (void)onUpdatePublishMediaStream:(NSString *)taskId code:(int)code message:(NSString *)message extraInfo:(NSDictionary *)extraInfo { // 你调用媒体流发布接口 (updatePublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次更新请求 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 主实例停止推流 [self.mainCloud stopLocalAudio]; [self.mainCloud stopLocalPreview]; }}
4. 各房间主播子实例互相拉取音视频流,开始跨房 PK。
AndroidiOS@Overridepublic void onUserAudioAvailable(String userId, boolean available) { // 某远端用户发布/取消了自己的音频 // 在自动订阅模式下,你无需做任何操作,SDK 会自动播放远端用户音频}@Overridepublic void onUserVideoAvailable(String userId, boolean available) { // 某远端用户发布/取消了主路视频画面 if (available) { // 订阅远端用户的视频流,并绑定视频渲染控件 subCloud.startRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG, remoteView); } else { // 停止订阅远端用户的视频流,并释放渲染控件 subCloud.stopRemoteView(userId, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_BIG); }}- (void)onUserAudioAvailable:(NSString *)userId available:(BOOL)available { // 某远端用户发布/取消了自己的音频 // 在自动订阅模式下,你无需做任何操作,SDK 会自动播放远端用户音频}- (void)onUserVideoAvailable:(NSString *)userId available:(BOOL)available { // 某远端用户发布/取消了主路视频画面 if (available) { // 订阅远端用户的视频流,并绑定视频渲染控件 [self.subCloud startRemoteView:userId streamType:TRTCVideoStreamTypeBig view:self.remoteView]; } else { // 停止订阅远端用户的视频流,并释放渲染控件 [self.subCloud stopRemoteView:userId streamType:TRTCVideoStreamTypeBig]; }}
5. (实时互动跨房连麦)跨房 PK 结束,主实例重新推流,停止混流回推房间,子实例退房并销毁。
AndroidiOS// 主实例开启本地音频采集和发布mainCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);// 主实例开启本地视频预览和发布mainCloud.startLocalPreview(mIsFrontCamera, mTxcvvAnchorPreviewView);// 主实例停止混流回推房间任务mainCloud.stopPublishMediaStream(taskId);// 停止媒体流的事件回调@Overridepublic void onStopPublishMediaStream(String taskId, int code, String message, Bundle extraInfo) { // 你调用停止发布媒体流 (stopPublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次停止请求 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 子实例退房并销毁 subCloud.stopLocalAudio(); subCloud.stopLocalPreview(); subCloud.exitRoom(); mainCloud.destroySubCloud(subCloud); }}// 主实例开启本地音频采集和发布[self.mainCloud startLocalAudio:TRTCAudioQualityDefault];// 主实例开启本地视频预览和发布[self.mainCloud startLocalPreview:self.isFrontCamera view:self.anchorPreviewView];// 主实例停止混流回推房间任务[self.mainCloud stopPublishMediaStream:self.taskId];// 停止发布媒体流的事件回调- (void)onStopPublishMediaStream:(NSString *)taskId code:(int)code message:(NSString *)message extraInfo:(NSDictionary *)extraInfo { // 你调用停止发布媒体流 (stopPublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次停止请求 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 子实例退房并销毁 [self.subCloud stopLocalAudio]; [self.subCloud stopLocalPreview]; [self.subCloud exitRoom]; [self.mainCloud destroySubCloud:self.subCloud]; }}
6. (旁路直播跨房连麦)跨房 PK 结束,主实例重新推流,更新旁路转推任务,子实例退房并销毁。
AndroidiOS// 主实例开启本地音频采集和发布mainCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);// 主实例开启本地视频预览和发布mainCloud.startLocalPreview(mIsFrontCamera, mTxcvvAnchorPreviewView);// 主实例更新旁路转推任务public void updatePublishMediaToCDN(String streamName, String taskId) { // 设定推流地址过期时间 long txTime = (System.currentTimeMillis() / 1000) + (24 * 60 * 60); // 生成鉴权信息,getSafeUrl 方法可在云直播控制台-域名管理-推流配置-推流地址示例代码获取 String secretParam = UrlHelper.getSafeUrl(LIVE_URL_KEY, streamName, txTime); // 媒体流发布的目标地址 TRTCCloudDef.TRTCPublishTarget target = new TRTCCloudDef.TRTCPublishTarget(); // 目标地址设定为旁路转推到 CDN target.mode = TRTCCloudDef.TRTC_PublishBigStream_ToCdn; TRTCCloudDef.TRTCPublishCdnUrl cdnUrl = new TRTCCloudDef.TRTCPublishCdnUrl(); // 拼接发布到直播服务商的推流地址(RTMP 格式) cdnUrl.rtmpUrl = "rtmp://" + PUSH_DOMAIN + "/live/" + streamName + "?" + secretParam; // 腾讯云直播服务为 true,第三方直播服务为 false cdnUrl.isInternalLine = true; // 可以添加多个 CDN 推流地址 target.cdnUrlList.add(cdnUrl); // 设置媒体流编码输出参数 TRTCCloudDef.TRTCStreamEncoderParam trtcStreamEncoderParam = new TRTCCloudDef.TRTCStreamEncoderParam(); trtcStreamEncoderParam.audioEncodedChannelNum = 1; trtcStreamEncoderParam.audioEncodedKbps = 50; trtcStreamEncoderParam.audioEncodedCodecType = 0; trtcStreamEncoderParam.audioEncodedSampleRate = 48000; trtcStreamEncoderParam.videoEncodedFPS = 15; trtcStreamEncoderParam.videoEncodedGOP = 2; trtcStreamEncoderParam.videoEncodedKbps = 1300; trtcStreamEncoderParam.videoEncodedWidth = 540; trtcStreamEncoderParam.videoEncodedHeight = 960; // 更新发布媒体流 mainCloud.updatePublishMediaStream(taskId, target, trtcStreamEncoderParam, null);}// 更新媒体流的事件回调@Overridepublic void onUpdatePublishMediaStream(String taskId, int code, String message, Bundle extraInfo) { // 你调用媒体流发布接口 (updatePublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次更新请求 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 子实例退房并销毁 subCloud.stopLocalAudio(); subCloud.stopLocalPreview(); subCloud.exitRoom(); mainCloud.destroySubCloud(subCloud); }}// 主实例开启本地音频采集和发布[self.mainCloud startLocalAudio:TRTCAudioQualityDefault];// 主实例开启本地视频预览和发布[self.mainCloud startLocalPreview:self.isFrontCamera view:self.anchorPreviewView];// 主实例更新旁路转推任务- (void)updatePublishMediaToCDN { NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0]; // 设定推流地址过期时间 NSTimeInterval time = [date timeIntervalSince1970] + (24 * 60 * 60); // 生成鉴权信息,getSafeUrl 方法可在云直播控制台-域名管理-推流配置-推流地址示例代码获取 NSString *secretParam = [self getSafeUrl:LIVE_URL_KEY streamName:self.streamName time:time]; // 媒体流发布的目标地址 TRTCPublishTarget* target = [[TRTCPublishTarget alloc] init]; // 目标地址设定为旁路转推到 CDN target.mode = TRTCPublishBigStreamToCdn; TRTCPublishCdnUrl* cdnUrl = [[TRTCPublishCdnUrl alloc] init]; // 拼接发布到直播服务商的推流地址(RTMP 格式) cdnUrl.rtmpUrl = [NSString stringWithFormat:@"rtmp://%@/live/%@?%@", PUSH_DOMAIN, self.streamName, secretParam]; // 腾讯云直播推流地址为 true,第三方为 false cdnUrl.isInternalLine = YES; NSMutableArray* cdnUrlList = [NSMutableArray array]; // 可以添加多个 CDN 推流地址 [cdnUrlList addObject:cdnUrl]; target.cdnUrlList = cdnUrlList; // 设置媒体流编码输出参数 TRTCStreamEncoderParam* encoderParam = [[TRTCStreamEncoderParam alloc] init]; encoderParam.audioEncodedSampleRate = 48000; encoderParam.audioEncodedChannelNum = 1; encoderParam.audioEncodedKbps = 50; encoderParam.audioEncodedCodecType = 0; encoderParam.videoEncodedWidth = 540; encoderParam.videoEncodedHeight = 960; encoderParam.videoEncodedFPS = 15; encoderParam.videoEncodedGOP = 2; encoderParam.videoEncodedKbps = 1300; // 更新发布媒体流 [self.mainCloud updatePublishMediaStream:self.taskId publishTarget:target encoderParam:encoderParam mixingConfig:nil];}// 更新媒体流的事件回调- (void)onUpdatePublishMediaStream:(NSString *)taskId code:(int)code message:(NSString *)message extraInfo:(NSDictionary *)extraInfo { // 你调用媒体流发布接口 (updatePublishMediaStream) 时传入的 taskId,会通过此回调再带回给你,用于标识该回调属于哪一次更新请求 // code: 回调结果,0 表示成功,其余值表示失败 if (code == 0) { // 子实例退房并销毁 [self.subCloud stopLocalAudio]; [self.subCloud stopLocalPreview]; [self.subCloud exitRoom]; [self.mainCloud destroySubCloud:self.subCloud]; }}
十七、跨房 PK 连麦方案对比分析
上面介绍了三种不同的跨房 PK 连麦实现方案,它们各自有不同的适用场景。下面分四个维度对不同的跨房连麦方案进行对比分析。
| 方案类型 | 方案优势 | 方案劣势 | 房间及人数限制 | 推荐的使用场景 |
| 常规跨房 PK 连麦方案 | 两人 PK 调用逻辑简单 | 多人 PK 调用逻辑复杂 | 单个主播最多和其他房间的 9 个主播跨房 PK | 两个房间,单主播(双人)跨房 PK |
| 服务端跨房 PK 连麦方案 | 纯服务端方案,客户端无需额外处理 | 存在额外的机器人推拉流及混流费用 | 最多支持 11 个房间同时进行跨房 PK,每个房间最多支持 16 个主播同时参与跨房 PK | 多个房间,多主播(多人)跨房 PK,纯服务端管理 |
| 子实例跨房 PK 连麦方案 | 不限制跨房数量,便于后期业务扩展 | 实现逻辑复杂,多实例的管理易出错 | 无限制 | 多个房间,单主播跨房 PK,后期业务可能扩展更多房间 |