快速实现 PK 直播
更新时间: 2024/08/23 14:52:55
在 PK 直播时,通过跨频道转发,无须切换 NERTC SDK,也不需要退出或重新进入房间,直接将媒体流转发到主播房间和挑战者房间,实现主播跨房间与其他主播实时互动。直播间内的观众可以同时观看两个主播 PK 互动,场景无缝切换。
下文介绍在单人直播的过程中,主播 A 邀请主播 B 进行 PK 直播的实现流程。
功能原理
PK 直播的架构原理如下图所示。
PK 直播的业务流程说明如下:
- 主播 A 发出 PK 邀请,主播 B 同意。
- 通过跨频道转发,主播 A 和主播 B 不需要退出原房间,直接将媒体流转发到房间 A 和房间 B 中,实现主播跨房间与其他主播实时互动。
- 互动直播服务器将主播 A 和主播 B 的音视频进行混屏转码后,推到 CDN 分发。
- 观众端使用 RTMP/HLS/FLV 协议进行拉流观看。
注意事项
- 只支持网易云信播放器 NELivePlayer 进行拉流,其他播放器暂不兼容。
- 单人直播切换到 PK 直播时,音频采样率必须保持一致。
前提条件
实现 PK 直播前,请确保您已经实现了 单人直播。
实现 PK 直播
通过跨房间媒体流转发,主播无须退出/加入原房间,即可将媒体流同时转发到多个房间中,实现 PK 直播。
下文介绍在单人直播的过程中,主播 A 邀请主播 B 进行 PK 直播的实现流程。
API 时序图
sequenceDiagram
title: 实现 PK 直播的 API 时序图
actor 主播 A
participant 业务服务器
actor 主播 B
participant NERtcSDK as 网易云信 RTC SDK
%% 开始 PK
主播 A->>业务服务器: 邀请 PK
Note right of 主播 A: 邀请主播 B 进行 PK<br>(带上自己的 uid、cname、<br>Token 等信息)
Note left of 主播 B: 请自行实现麦位<br>管理相关业务逻辑
业务服务器->>主播 B: 邀请 PK(带上主播 A 的 <br>uid、cname、<br>Token 等信息)
主播 B-->>业务服务器: 同意 PK
业务服务器-->>主播 A: 对端同意 PK(带上主播<br> B 的 uid、cname、Token 等信息)
rect rgb(191, 223, 255)
主播 A ->> NERtcSDK: startChannelMediaRelay 开启媒体转发
主播 A ->> NERtcSDK: addLiveStream 开始旁路推流
NERtcSDK -->> 主播 A: onNERTCEngineLiveStreamState 监听旁路推流状态
主播 A ->> NERtcSDK: stopPushStreaming 旁路推流成功后,停止单人直播推流
Note right of 主播 A: 当主播 B 开始 MediaRelay <br>后,更新旁路推流,具体请<br>根据实际业务进行调整
主播 A ->> NERtcSDK: updateLiveStreamTask 更新旁路推流
Note right of 主播 A: 根据回调确认 mediaRelay <br>是否成功,如果失败,则进行失败回退,<br>例如调用 stopChannelMediaRelay 和<br> removeLiveStreamTask 方法结束 PK
NERtcSDK -->> 主播 A: onNERtcEngineChannelMediaRelayStateDidChange:channelName: <br>和 onNERtcEngineDidReceiveChannelMediaRelayEvent: channelName: error:
end
NERtcSDK -->> 主播 A: 监听房间中人员进入和音视频打开的回调
NERtcSDK -->> 主播 B: 监听房间中人员进入<br>和音视频打开的回调
主播 A ->> 业务服务器: 结束 PK
业务服务器 ->> 主播 B: 结束 PK
主播 A ->> NERtcSDK: startPushStreaming 重新开始单人直播推流
NERtcSDK -->> 主播 A: onNERtcEngineStartPushStreaming
主播 A ->> NERtcSDK: stopChannelMediaRelay 停止媒体转发
主播 A ->> NERtcSDK: removeLiveStreamTask 移除旁路推流任务
主播 B ->> NERtcSDK: stopChannelMediaRelay
主播 B ->> NERtcSDK: removeLiveStreamTask <br>移除旁路推流任务
开始 PK
-
开始 mediaRelay。
主播 A 调用
startChannelMediaRelay
方法开启媒体转发功能,将主播 A 的视频流推送到主播 B 房间。Objective-C
NERtcChannelMediaRelayConfiguration *mediaRelayConfig = [[NERtcChannelMediaRelayConfiguration alloc]init]; NERtcChannelMediaRelayInfo *info = [[NERtcChannelMediaRelayInfo alloc]init]; info.channelName = channelName; //PK 目标房间的房间号 info.token = token;//用于加入 PK 目标房间的 token, 与加入房间获取的 token 方式一致,注意 Token 需要及时使用,防止过期导致异常。 info.uid = uid;//本人在 PK 目标房间的 uid [mediaRelayConfig setDestinationInfo:info forChannelName:info.channelName]; [[NERtcEngine sharedEngine] startChannelMediaRelay:mediaRelayConfig]
-
开启旁路推流任务。
主播 A 调用
addLiveStreamTask
方法添加旁路推流任务,将主播 A 和主播 B 房间的音视频流推送到 CDN 上进行合流。Objective-C
//设置旁路推流参数 NERtcLiveStreamTaskInfo *info = [[NERtcLiveStreamTaskInfo alloc] init]; NERtcLiveConfig *config = [[NERtcLiveConfig alloc] init]; info.streamURL = self.pushStreamingUrl; info.taskID = PKTaskId; info.lsMode = kNERtcLsModeVideo; config.channels = 2; config.sampleRate = kNERtcLiveStreamAudioSampleRate44100; config.audioBitrate = 128; config.audioCodecProfile = kNERtcLiveStreamAudioCodecProfileLCAAC; //如果需要服务器录制 info.serverRecordEnabled = YES; info.config = config; //设置整体布局 NERtcLiveStreamLayout *layout = [[NERtcLiveStreamLayout alloc] init]; layout.width = 720;//整体布局宽度 建议与单推一致 layout.height = 1280;//整体布局高度 建议与单推一致 info.layout = layout; //自己 NERtcLiveStreamUserTranscoding *user1 = [[NERtcLiveStreamUserTranscoding alloc]init]; user1.uid = myUid; user1.audioPush = YES; // 推流是否发布 user1 的音频 user1.videoPush = YES; // 推流是否发布 user1 的视频 user1.x = 0; // user1 的视频布局 x 偏移,相对整体布局的左上角 user1.y = 320; // user1 的视频布局 y 偏移,相对整体布局的左上角 user1.width = 360; // user1 的视频布局宽度 user1.height = 640; //user1 的视频布局高度 user1.adaption = kNERtcLsModeVideoScaleCropFill; //会填满画面,超出部分会被裁减 //如果此时知道对方 Uid 可以在这里预先填上,如果不知道,可以先不设置 User2,然后参考第 6 步 update NERtcLiveStreamUserTranscoding *user2 = [[NERtcLiveStreamUserTranscoding alloc]init]; user2.uid = pkUid; user2.audioPush = YES; // 推流是否发布 user2 的音频 user2.videoPush = YES; // 推流是否发布 user2 的视频 user2.x = 360; // user2 的视频布局 x 偏移,相对整体布局的左上角 user2.y = 320; // user2 的视频布局 y 偏移,相对整体布局的左上角 user2.width = 360; // user2 的视频布局宽度 user2.height = 640; //user2 的视频布局高度 user2.adaption = kNERtcLsModeVideoScaleCropFill; // NSMutableArray* layoutUsers = [NSMutableArray array]; [layoutUsers addObject:user1]; [layoutUsers addObject:user2]; info.layout.users = layoutUsers; //结束之前的旁路推流,防止异常 [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; //开启旁路推流 [[NERtcEngine sharedEngine] addLiveStreamTask:info compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { if (0 != errorCode) { //表示开启开启 PK 失败,此时仍然处于单人直播状态,此时业务可以根据自身定制。如果想要继续 PK 需要重新按照实现 PK 直播逻辑继续开始 PK。 } }]
-
等待旁路推流结果。
通过
onNERTCEngineLiveStreamState
回调监听旁路推流状态,如果状态为kNERtcLsStatePushing
,表示旁路推流成功。Objective-C
//旁路直播状态回调 - (void)onNERTCEngineLiveStreamState:(NERtcLiveStreamStateCode)state taskID:(NSString *)taskID url:(NSString *)url { //表示 PK 成功 if (state == kNERtcLsStatePushing) { //停止单人直播 [NERtcEngine sharedEngine] stopPushStreaming]; }else{ //此时需要按照以下操作回退到单推 [[NERtcEngine sharedEngine] stopChannelMediaRelay]; [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; [[NERtcEngine sharedEngine] startPushStreaming:config]; } }
如果旁路推流失败,则根据业务实际情况进行失败回退,例如调用
stopChannelMediaRelay
方法停止媒体转发功能。 -
停止单人直播推流。
主播 A 调用
stopPushStreaming
方法停止单人直播推流。Objective-C
//在第 4 步骤收到 onNERTCEngineLiveStreamState 回调 kNERtcLsStatePushing 后 //调用 stopPushStreaming 停止单人直播 [NERtcEngine sharedEngine] stopPushStreaming];
-
等待主播 B 加入房间后更新旁路推流任务。
主播 A 调用
updateLiveStreamTask
如果无法预先知道 PK 对方的 Uid,或者需要保证主播房间和挑战者房间的音视频流能够同步播放明,可以参考这一步。Objective-C
//对方加入房间回调 - (void)onNERtcEngineUserDidJoinWithUserID:(uint64_t)userID userName:(NSString *)userName { //参考第 4 步的旁路推流设置 //调用 update 接口更新 [[NERtcEngine sharedEngine] updateLiveStreamTask:info compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }] }
-
确认 mediaRelay 是否成功。
通过
onNERtcEngineChannelMediaRelayStateDidChange
和onNERtcEngineDidReceiveChannelMediaRelayEvent: channelName: error:
回调监听媒体转发状态,确认媒体转发是否成功。如果媒体转发失败,则根据业务实际情况进行失败回退,例如调用
stopChannelMediaRelay
和removeLiveStreamTask
方法结束 PK。Objective-C
- (void)onNERtcEngineDidReceiveChannelMediaRelayEvent:(NERtcChannelMediaRelayEvent)event channelName:(NSString *)channelName error:(NERtcError)error { //表示失败 if (event == NERtcChannelMediaRelayEventFailure) { //此时需要按照以下操作回退到单推 [[NERtcEngine sharedEngine] stopChannelMediaRelay]; [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; [[NERtcEngine sharedEngine] startPushStreaming:config]; } //表示成功 if(event == NERtcChannelMediaRelayEventConnected){ //此时表示 PK 建立成功 } }
结束 PK
-
开始单人直播推流。
主播 A 调用
startPushStreaming
方法开始单人直播推流。Objective-C
//设置推流参数,此时 streamingRoomInfo 可以不传 NERtcPushStreamingConfig *pushConfig = [[NERtcPushStreamingConfig alloc] init]; pushConfig.streamingUrl = streamingUrl; //开启推流 [[NERtcEngine sharedEngine] startPushStreaming:config];
-
等待单人直播推流结果。
主播 A 通过
onNERtcEngineStartPushStreamingWithResult:channelId:
回调监听单人直播推流状态,如果推流成功,则执行下一步操作。Objective-C
- (void)onNERtcEngineStartPushStreamingWithResult:(NERtcError)result channelId:(uint64_t)channelId { if (kNERtcNoError != result && && kNERtcErrInvalidState != result) { //开始 cdn 推流失败,此时仍然处于 PK 直播中,可以根据业务情况进行相关处理,如需要继续结束需要参照第一步,再次调用开启推流 return; } //停止 MediaRelay [[NERtcEngine sharedEngine] stopChannelMediaRelay]; //停止旁路推流 [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; }
-
停止 mediaRelay。
主播 A 和 主播 B 分别调用
stopChannelMediaRelay
方法停止媒体转发功能。根据第 2 步, 在
onNERtcEngineStartPushStreamingWithResult
回调方法停止MediaRelay
。 -
移除旁路推流任务。
主播 A 和 主播 B 分别调用
removeLiveStreamTask
方法移除旁路推流任务。根据第 2 步, 在
onNERtcEngineStartPushStreamingWithResult
回调方法停止旁路推流。
示例代码
Objective-C//发起 PK
- (int)enterPK:(NSString *)channelName token:(NSString *)token uid:(uint64_t)uid {
//step1 开始 mediaRelay(startMediaRelay)
NERtcChannelMediaRelayConfiguration *mediaRelayConfig = [[NERtcChannelMediaRelayConfiguration alloc]init];
NERtcChannelMediaRelayInfo *info = [[NERtcChannelMediaRelayInfo alloc]init];
info.channelName = channelName;
info.token = token;
info.uid = uid;
[mediaRelayConfig setDestinationInfo:info forChannelName:info.channelName];
[[NERtcEngine sharedEngine] startChannelMediaRelay:mediaRelayConfig]
//step2 开启旁路推流任务(addLiveStream,旁路任务中的音频参数保持跟 cdn 下相同(采样率、声道数等等))
[[NERtcEngine sharedEngine] addLiveStreamTask:task compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) {
}];
//step3 等待旁路推流结果(onNERTCEngineLiveStreamState:taskID:url:),如果 state 为 kNERtcLsStatePushing 表示添加旁路任务成功,则停止 cdn 推流(stopPushStreaming)
[[NERtcEngine sharedEngine] stopPushStreaming];
//step4 等待 pk 对方加入房间后更新旁路推流任务 updateLiveStreamTask
[[NERtcEngine sharedEngine] updateLiveStreamTask:task compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) {
}];
//step5 确认 mediaRelay 是否成功,如果失败根据实际情况做相应处理
//等待 mediaRelay 任务状态回调 onNERtcEngineChannelMediaRelayStateDidChange:channelName:和 onNERtcEngineDidReceiveChannelMediaRelayEvent: channelName: error:确认 mediaRelay 是否成功
}
//结束 PK
- (int)leavePK {
//step1 开始 cdn 推流
[[NERtcEngine sharedEngine] startPushStreaming:config];
//step2 等待 cdn 推流结果回调(onNERtcEngineStartPushStreamingWithResult:channelId:),
//成功后停止 mediaRelay(stopMediaRelay)和移除旁路推流任务(removeLiveStreamTask:compeltion:)
[[NERtcEngine sharedEngine] stopChannelMediaRelay];
[[NERtcEngine sharedEngine] removeLiveStreamTask:task compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) {
}];
}
注意事项
removeLiveStreamTask
的调用
-
当 PK 业务直接退出房间时候,需要调用
removeLiveStreamTask
停止旁路推流,防止流没有停止,观众还能拉到流,出现业务异常,可参考以下场景。Objective-C
//主动 leaveChannel,退出房间 (不是切为单人直播) - (void)leavePK{ [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; [[NERtcEngine sharedEngine] leavechannel]; } //SDK disconnect 断开 - (void)onNERtcEngineDidDisconnectWithReason:(NERtcError)reason{ [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; }
-
推荐每次开始 PK
addLiveStreamTask
前,可以先调用removeLiveStreamTask
停止之前的旁路推流任务,防止一些异常情况,出现addLiveStreamTask
失败,导致业务异常,流程见开始 PK 第 2 步。Objective-C
[[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; [[NERtcEngine sharedEngine] addLiveStreamTask:[self mockLiveStreamInfoForPk] compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { if (0 != errorCode) { //表示开启开启 PK 失败,此时仍然处于单人直播状态,此时业务可以根据自身定制。如果想要继续 PK 需要重新按照实现 PK 直播逻辑继续开始 PK。 } }
异常情况的回调处理
-
旁路推流回调失败处理,回退至单推处理逻辑。
Objective-C
//开启旁路推流失败回调 [[NERtcEngine sharedEngine] addLiveStreamTask:info compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { if (0 != errorCode) { //停止 MediaRelay [[NERtcEngine sharedEngine] stopChannelMediaRelay]; //停止旁路推流 [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; } }] //旁路推流状态回调 - (void)onNERTCEngineLiveStreamState:(NERtcLiveStreamStateCode)state taskID:(NSString *)taskID url:(NSString *)url { //表示 PK 成功 if (state == kNERtcLsStatePushing) { //参考开始 PK 第 3 步停止单人直播 } else if (state == kNERtcLsStatePushFail){ //此时需要按照以下操作回退到单推 [[NERtcEngine sharedEngine] stopChannelMediaRelay]; [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; [[NERtcEngine sharedEngine] startPushStreaming:config]; } }
-
MediaRelay
失败,回退至单推处理逻辑。Objective-C
- (void)onNERtcEngineDidReceiveChannelMediaRelayEvent:(NERtcChannelMediaRelayEvent)event channelName:(NSString *)channelName error:(NERtcError)error { //表示失败 if (event == NERtcChannelMediaRelayEventFailure) { //此时需要按照以下操作回退到单推 [[NERtcEngine sharedEngine] stopChannelMediaRelay]; [[NERtcEngine sharedEngine] removeLiveStreamTask:PKTaskId compeltion:^(NSString * _Nonnull taskId, kNERtcLiveStreamError errorCode) { }]; [[NERtcEngine sharedEngine] startPushStreaming:config]; } //表示成功 else if (event == NERtcChannelMediaRelayEventConnected){ //此时表示 PK 建立成功 } }
以上回退时业务需要告知 PK 对方,同时回退。
主要回调
请在初始化时注册推流相关的回调,PK 场景需要关注的主要回调:
Objective-C//开始推流 startPushStreaming 结果回调
- (void)onNERtcEngineStartPushStreamingWithResult:(NERtcError)result channelId:(uint64_t)channelId{
if (kNERtcNoError != result) {
//kNERtcErrInvalidState 不需要关注
if(kNERtcErrInvalidState != result){
return;
}
//推流失败,业务按需处理,参考结束 PK 的第 2 步。
}else{
//推流成功,业务按需处理,参考结束 PK 的第 2 步。
}
}
//停止推流 stopPushStreaming 结果回调
-(void)onNERtcEngineStopPushStreaming:(NERtcError)result{
//可以不需要关注
}
//SDK 客户端和服务器断开连接
- (void)onNERtcEngineReconnectingStart{
//此时表示客户端 SDK 无法连上服务器,正在重连,业务按需处理,可以提示主播
}
//SDK 客户端重连状态结果回调
- (void)onNERtcEngineRejoinChannel:(NERtcError)result{
//此时表示重连成功,SDK 连上了服务器
}
//推流过程重连失败,最终断开回调
- (void)onNERtcEngineDidDisconnectWithReason:(NERtcError)reason{
//此时表示推流失败了,业务按需处理,如果需要继续推流,需要再次调用 startPushStreaming 接口
}
//MediaRelay 事件回调
- (void)onNERtcEngineDidReceiveChannelMediaRelayEvent:(NERtcChannelMediaRelayEvent)event channelName:(NSString *)channelName error:(NERtcError)error {
//表示失败
if (event == NERtcChannelMediaRelayEventFailure) {
//参照开始 PK 第 6 步
}
//表示成功
if(event == NERtcChannelMediaRelayEventConnected){
//参照开始 PK 第 6 步
}
}
//旁路直播状态回调
- (void)onNERTCEngineLiveStreamState:(NERtcLiveStreamStateCode)state
taskID:(NSString *)taskID
url:(NSString *)url {
//表示 PK 成功
if (state == kNERtcLsStatePushing) {
//参考开始 PK 第 3 步停止单人直播
}else{
//参考开始 PK 第 3 步操作回退到单推
}
}