直播间里,为了增进直播气氛、快速吸粉,主播可以邀请其他直播间的主播进行连麦互动或在线 PK。连麦直播间内的观众可以同时收听或观看多个主播互动,能够增强互动直播的趣味性,激发观众刷榜送礼物的欲望。下面介绍三种不同跨房 PK 连麦方案的具体实现。


常规跨房 PK 连麦方案
适用场景:两个或多个房间 PK,房间主播数量较少的简单跨房连麦场景。
方案原理
默认情况下,只有同一个房间内的用户之间音视频可以互通,不同的房间之间的音视频流是相互隔离的。通过跨房连麦,可以将另一个房间中某个主播音视频流发布到自己所在的房间中,与此同时也会将自己的音视频流发布到目标主播的房间中。让身处两个不同房间中的主播进行跨房间的音视频流分享,从而让每个房间中的观众都能观看到这两个主播的音视频。


实现流程
房间“101”中的用户都会收到主播 B 的
onRemoteUserEnterRoom(B) 和 onUserVideoAvailable(B,true) 这两个事件回调,即房间“101”中的用户都可以订阅主播 B 的音视频。房间“102”中的用户都会收到主播 A 的
onRemoteUserEnterRoom(A) 和 onUserVideoAvailable(A,true) 这两个事件回调,即房间“102”中的用户都可以订阅主播 A 的音视频。注意:
两个房间单主播跨房 PK,只需要其中一个房间的主播调用
ConnectOtherRoom 建立跨房连麦即可,请勿双向调用。主播可通过多次调用
ConnectOtherRoom 与多个房间的主播建立跨房连麦,目前限制单个主播最多和其他房间的 9 个主播进行跨房连麦。实时互动跨房连麦
RTC 场景下的跨房连麦 PK 流程整体简单,主播和跨房连麦主播互相拉取 RTC 单流,观众同时拉取主播和跨房连麦主播的 RTC 单流,观众可独立控制主播和跨房连麦主播媒体流订阅逻辑。实时互动场景跨房连麦流程如下图所示。


注意:
旁路直播跨房连麦
旁路直播场景下的跨房连麦 PK 流程相对复杂,主播和跨房连麦主播互相拉取 RTC 单流,CDN 观众拉取主播和跨房连麦主播的旁路混流,观众无法独立控制主播和跨房连麦主播媒体流订阅逻辑。旁路直播场景跨房连麦流程如下图所示。


注意:
示例代码
1. 任意一方发起跨房 PK 连麦。
public void connectOtherRoom(String roomId, String userId) {try {JSONObject jsonObj = new JSONObject();// 以字符串房间号为例,数字房间号 key:roomIdjsonObj.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: 错误信息}
注意:
跨房 PK 连麦的本地用户和对端用户必须都为主播角色,且必须都有音频或视频上行。
两个房间单主播跨房 PK,只需要其中一个房间的主播调用
ConnectOtherRoom 建立跨房连麦即可,请勿双向调用。2. 两个房间中的所有用户都会收到来自另一个房间中的 PK 主播的音视频流可用回调。
@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 连麦。
// 退出跨房连麦mTRTCCloud.DisconnectOtherRoom();// 退出跨房连麦的结果回调@Overridepublic void onDisConnectOtherRoom(int errCode, String errMsg) {super.onDisConnectOtherRoom(errCode, errMsg);}
// 退出跨房连麦[self.trtcCloud disconnectOtherRoom];// 退出跨房连麦的结果回调- (void)onDisconnectOtherRoom:(TXLiteAVError)errCode errMsg:(NSString *)errMsg {}
注意:
服务端跨房 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。
注意:
本方案最多支持 11 个房间同时进行跨房 PK 连麦,每个房间最多支持 16 个主播同时参与连麦。
机器人 ID 不能与房间内的普通用户 ID 冲突,否则会导致转推任务由于机器人用户被踢出 TRTC 房间而异常结束。
旁路直播跨房连麦
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,剔除其他房间回推的音视频流。
注意:
根据转推目标的不同,旁路转推 CDN 对应参数 McuPublishCdnParam,回推 TRTC 房间对应参数 McuFeedBackRoomParams。
若直播间为单主播直播场景,则在启动转推任务时可选择单流旁路转推 SingleSubscribeParams,从而节省混流转码费用。
示例代码
下面以纯音频场景为例,展示跨房 PK 混流回推房间任务的参数体示例。
{"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..."}]}
注意:
子实例跨房 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,恢复主实例单流旁路转推,子实例退房并销毁。
注意:
根据转推目标的不同,您可通过 TRTCPublishTarget 自行设置媒体流发布模式,目前支持单流转推、混流转码和回推房间模式。
若涉及美颜特效的使用,建议主实例和子实例使用同一个美颜实例,设置第三方美颜的视频数据回调 时采用不同的 视频像素格式。
示例代码
1. 创建子实例,进入一个新的 PK 房间进行推拉流。
// 初始化 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() {@Overridepublic void onEnterRoom(long result) {if (result > 0) {// result 代表加入房间所消耗的时间(毫秒)Log.d(TAG, "Enter room succeed!");} else {// result 代表进房失败的错误码Log.d(TAG, "Enter room failed!");}}@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);}}};
// 初始化 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. (实时互动跨房连麦)启动混流回推房间任务,同时停止主实例推流。
// 开始发布混合媒体流到 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. (旁路直播跨房连麦)更新原有旁路转推任务,同时停止主实例推流。
// 更新发布混合媒体流到直播 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();// 目标地址设定为混流转推到 CDNtarget.mode = TRTCCloudDef.TRTC_PublishMixStream_ToCdn;TRTCCloudDef.TRTCPublishCdnUrl cdnUrl = new TRTCCloudDef.TRTCPublishCdnUrl();// 拼接发布到直播服务商的推流地址(RTMP 格式)cdnUrl.rtmpUrl = "rtmp://" + PUSH_DOMAIN + "/live/" + streamName + "?" + secretParam;// 腾讯云直播服务为 true,第三方直播服务为 falsecdnUrl.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];// 目标地址设定为混流转推到 CDNtarget.mode = TRTCPublishMixStreamToCdn;TRTCPublishCdnUrl* cdnUrl = [[TRTCPublishCdnUrl alloc] init];// 拼接发布到直播服务商的推流地址(RTMP 格式)cdnUrl.rtmpUrl = [NSString stringWithFormat:@"rtmp://%@/live/%@?%@", PUSH_DOMAIN, self.streamName, secretParam];// 腾讯云直播推流地址为 true,第三方为 falsecdnUrl.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。
@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 结束,主实例重新推流,停止混流回推房间,子实例退房并销毁。
// 主实例开启本地音频采集和发布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 结束,主实例重新推流,更新旁路转推任务,子实例退房并销毁。
// 主实例开启本地音频采集和发布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();// 目标地址设定为旁路转推到 CDNtarget.mode = TRTCCloudDef.TRTC_PublishBigStream_ToCdn;TRTCCloudDef.TRTCPublishCdnUrl cdnUrl = new TRTCCloudDef.TRTCPublishCdnUrl();// 拼接发布到直播服务商的推流地址(RTMP 格式)cdnUrl.rtmpUrl = "rtmp://" + PUSH_DOMAIN + "/live/" + streamName + "?" + secretParam;// 腾讯云直播服务为 true,第三方直播服务为 falsecdnUrl.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];// 目标地址设定为旁路转推到 CDNtarget.mode = TRTCPublishBigStreamToCdn;TRTCPublishCdnUrl* cdnUrl = [[TRTCPublishCdnUrl alloc] init];// 拼接发布到直播服务商的推流地址(RTMP 格式)cdnUrl.rtmpUrl = [NSString stringWithFormat:@"rtmp://%@/live/%@?%@", PUSH_DOMAIN, self.streamName, secretParam];// 腾讯云直播推流地址为 true,第三方为 falsecdnUrl.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 调用逻辑复杂 | 单个主播最多和其他房间的 9 个主播跨房 PK | 两个房间,单主播(双人)跨房 PK | |
纯服务端方案,客户端无需额外处理 | 存在额外的机器人推拉流及混流费用 | 最多支持 11 个房间同时进行跨房 PK,每个房间最多支持 16 个主播同时参与跨房 PK | 多个房间,多主播(多人)跨房 PK,纯服务端管理 | |
不限制跨房数量,便于后期业务扩展 | 实现逻辑复杂,多实例的管理易出错 | 无限制 | 多个房间,单主播跨房 PK,后期业务可能扩展更多房间 |