实现 PK
PK 对战是两个主播之间的友好竞争,展示给观众的是主播之间的互动。 本文档介绍如何在使用实时音视频产品(ZEGO Express SDK)实现基本直播功能的基础上,结合即时通讯产品(ZIM SDK)实现 PK 对战功能。
通常有两种玩法:
- 通过主播之间主动发起的 PK 对战:主播在自己的直播开始后,可以向他们想要连接的主播发送 PK 对战请求。一旦 PK 对战请求被接受,两个主播将会连接在一起。(我们将以次为例进行说明)
- 由业务服务器协调的 PK 对战:主播在申请 PK 匹配后,业务服务器将协调主播开始 PK 对战,主播将会自动连接到彼此。
本文档将介绍如何在直播场景中实现 PK 对战。
前提条件
在开始之前,请确保您已完成以下步骤:
- 已集成 SDK,详情请参考 CDN 直播快速开始 或 超低延迟直播快速开始 文档。
- 下载与本文档配套的 示例代码。
- 请联系 ZEGO 技术支持开通混流服务。
- 在 管理控制台 中注册并创建项目,同时开通 “即时通讯” 服务。
预览效果
您可以使用本文档提供的 示例代码 实现以下效果:
主页 | 直播页面 | 接收PK对战请求 | PK对战 |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
技术原理
以下是本文档内容的结构和概述:
- 如何发送 PK 对战邀请:解释如何利用 呼叫邀请 发送 PK 邀请的过程。
- PK 拉流和推流的介绍:基于流的概念,介绍 PK 对战的推流和拉流解决方案的框架。
- 实现主播端逻辑:提供主播端 PK 的解决方案的详细信息,包括如何与其他主播互动,为观众开启混流,以及在 PK 对战开始时通知观众。
- 实现观众端逻辑:提供观众端 PK 的解决方案的细节,包括如何观看主播之间的互动及如何处理单流场景。
- 如何结束 PK 对战:描述结束 PK 会话,并恢复正常的单主播直播的步骤。
- 如何检测 PK 对战中的异常情况:描述如何通过 SEI 检测 PK 主机的异常情况。
- 其他功能介绍:包括使用 SEI 检测主机设备和流的状态,处理客户端断开连接或崩溃等异常情况。
- 常见问题解答:解答与 PK 对战相关的常见问题,如渲染主播声音或暂时静音其他主播。
如何发送 PK 对战邀请
如果您计划实现“由业务服务器协调的 PK 对战”,请忽略本节。
此外,如果您的服务器没有信令通道向客户端发送通知,我们建议使用 ZIM 服务端 API 的Command message (signaling message)
实现,详情请参考 ZIM 服务端 API-发送房间消息。
此处使用类似呼叫邀请的方法,实现 PK 对战邀请:
基于 ZIM SDK 提供的 呼叫邀请 功能。该功能提供了呼叫邀请的能力,允许您发送、取消、接受和拒绝邀请,您可以实现 PK 对战邀请、房间邀请。您可以使用 ZIMCallInviteConfig 提供的extendedData
字段,该字段允许您自定义此邀请的类型,从而实现不同的功能。
例如,您可以将业务协议编码为 JSON 并附加到extendedData
中:
{
"room_id": "Room10001",
"user_name": "Alice",
"type": "start_pkbattles" // 或者 "video_call" , "voice_call"
}
这样,接收方在收到邀请后,可以根据type
字段判断并执行不同的业务逻辑。
基于此实现呼叫邀请的过程如下(以“Alice 邀请 Bob 进行 PK 对战,Bob 接受并连接 PK 对战”为例):
有关这些接口的具体用法,请参考 呼叫邀请。
-
在实现 PK 对战邀请时,需要注意在邀请接口的
extendedData
字段中,需要传递双方的信息: A 在发起 PK 对战邀请时,除了上述的type
之外,还需要将自己的roomID
和userName
传递给对方,以便对方知道开始 PK 对战的相关信息。 B 在接受 PK 对战邀请时,同样需要在type
之外,传递自己的roomID
和userName
。 -
在第一次邀请时,请调用
callInvite
并将其设置为高级模式。在此模式下,您需要使用callEnd
或callQuit
来结束或退出 PK 对战,并且您可以使用callingInvite
继续邀请其他人加入 PK 对战。NSMutableArray *invitees = [NSMutableArray array]; // 邀请对象列表 [invitees addObject:@"421234"]; // 邀请人的 ID ZIMCallInviteConfig *config = [[ZIMCallInviteConfig alloc] init]; config.timeout = 200; // 邀请超时时间,单位为秒,范围为 1-600 // mode 表示呼叫邀请模式,ADVANCED 表示设置为高级模式。 config.mode = ZIMCallInviteModeAdvanced; [zim callInviteWith:invitees config:config callback:^(NSString *requestID, NSDictionary *sentInfo, NSDictionary *errorInfo) { // 这里的 callID 是由 SDK 内部生成的,用于在用户发起呼叫后,唯一标识呼叫邀请。 // 后续,当发起者取消呼叫或被邀请者接受或拒绝呼叫时,将使用该 callID。 }];
PK 的推流与拉流
开始之前,请确保您熟悉以下概念:
-
什么是房间和流?
- ZEGO Express SDK:由 ZEGO 提供的实时音视频通话 SDK。它可以帮助您提供具有便捷访问、高清流畅、跨平台通信、低延迟和高并发性的音视频服务。
- 推流:将捕获并打包的音视频数据流推送到 ZEGO 实时音视频云的过程。
- 拉流:从 ZEGO 实时音视频云接收并播放音视频数据流的过程。
- 房间:ZEGO 提供的服务,用于组织用户群体,并允许同一房间内的用户相互接收和发送实时音视频和消息。
- 用户只有在登录到房间后才能推流或拉流。
- 用户可以接收房间内的变化通知(例如用户加入或离开房间,以及音视频流的变化)。
-
什么是混流服务? 通过混流服务,多路媒体流可以组合成一个单一的流,这样观众只需播放一个流,即可提高质量并降低性能成本。
更多详细信息,请参考 混流。
PK 解决方案需要使用 混流:混流指将多个流合并为一个流,这样观众只需播放该流,即可观看多个主播的画面。使用混流的必要性和重要优势如下:
- 必要性:在观众观看多个主播时,它确保音视频的相对实时同步,避免两个主播延迟不一致,导致互动体验差。
- 优势:客户端无需解码和拉多个流,可以节省观众端的带宽,并防止低端设备过热,可进一步提升观看体验。
在 PK 开始之前,每个主播都会推流,观众可以直接拉主播的流。然而,在 PK 开始后,拉流的方法将发生变化:
- 除了推自己的流,每个主播还需要拉对方的流,以实现主播间的实时音视频互动。
- 观众暂时静音主播的单流:使用静音可以节省带宽,并在 PK 结束后快速恢复主播的单流画面。
- 开始混合两个主播的流:客户端或服务端可以管理混流,稍后将详细解释。
- 观众播放混流:观看两位主播的互动。
以上是 PK 场景中推流和拉流的基本框架。在接下来的章节中,我们将根据主播和观众的逻辑提供详细操作流程。
开始 PK 对战- 主播逻辑
当主播准备开始 PK 对战时,需要执行以下操作:
1 播放每个主播的单一流
通常,流 ID 与房间 ID 和用户 ID 相关。例如,在本文档的附带示例代码中,流 ID 规则为 "${roomID}_${userID}_main_host"
。因此,您可以使用此规则,连接每个主播的流 ID,然后调用 startPlayingStream 来拉对方的流。双方的信息可以从邀请接口的回调和双方之间传递的 extendedData
中获取。
如果您的 PK 对战是由服务端安排和匹配的,并且服务端发送了开始 PK 信号,您需要在向主播发送 PK 开始通知时,包含对手主播的 userID
、roomID
、userName
等信息。
2 开始混流任务
混流可以由客户端或服务端发起,您可以根据需要选择:
- 如果从客户端发起混流,架构更简单,但需要处理客户端的复杂网络环境和可能的异常应用退出等问题(下文将详细解释)。
- 如果从服务端发起混流,则受客户端异常情况的影响较小。但是,客户端和服务端之间的交互更复杂,需要一定水平的服务器端开发能力。
本文档的附带示例代码使用了 “由主播客户端手动发起混流” 的方法。
从客户端发起混流
混流参数相关注意事项:
- 混流布局:通常,在 PK 对战中,每个观众在界面左侧看到自己房间的画面。因此,在 PK 开始后,需要发起混流任务,即每个房间中的每个主播都需要发起一个混流任务。在混合流时,每个主播需要将自己的视频放在流混流布局的左侧。
layout
参数可以参考下方代码,或者您可以参考 混流 文档,了解更多关于混流布局的详细信息。 - 混流分辨率:以主播的默认 540p 分辨率为例,每个单一流的分辨率为
width=540, height=960
。将两个流并排组合后,混合流的分辨率应为width=540*2, height=960
。如果要降低混合流的分辨率,可以保持此宽高比,并减小混流分辨率,例如,使用540*480
分辨率和width=540*2/2, height=960/2
。请注意,如果需要降低流混流分辨率,还需要相应调整layout
参数。我们的示例代码使用了混合流分辨率为width=810
和height=720
。 - 混流任务 ID 和流 ID:通常,每个流混流任务只有一个输出流,PK 场景也是如此。因此,可以将混流任务 ID 和流 ID 使用相同的 ID,例如
'${roomID}__mix'
,这样可以更容易地管理未来的流混流任务。
以下是一个完整的混流参数示例代码片段:
- (void)updatePKMixTask:(ZegoMixerStartCallback)callback {
if (!self.pkInfo) {
return;
}
NSMutableArray *pkStreamList = [NSMutableArray array];
for (ZegoPKUser *pkUser in self.pkInfo.pkUserList) {
if (pkUser.hasAccepted) {
[pkStreamList addObject:pkUser.pkUserStream];
}
}
ZegoMixerVideoConfig *videoConfig = [[ZegoMixerVideoConfig alloc] init];
videoConfig.resolution = MixVideoSize;
videoConfig.bitrate = 1500;
videoConfig.fps = 15;
NSMutableArray<ZegoMixerInput *> *mixInputList = [NSMutableArray array];
ZegoMixLayoutConfig *layOutConfig = [self.liveManager getMixLayoutConfigWithStreamList:pkStreamList videoConfig:videoConfig];
if (layOutConfig) {
mixInputList = layOutConfig;
} else {
mixInputList = [self getMixVideoInputs:pkStreamList videoConfig:videoConfig];
}
self.currentInputList = mixInputList;
if (self.currentMixerTask) {
[self.currentMixerTask setInputList:mixInputList];
} else {
NSString *mixStreamID = [NSString stringWithFormat:@"%@_mix", ZegoSDKManager.shared.expressService.currentRoomID ?: @""];
self.currentMixerTask = [[ZegoMixerTask alloc] initWithTaskID:mixStreamID];
[self.currentMixerTask setVideoConfig:videoConfig];
[self.currentMixerTask setInputList:mixInputList];
ZegoMixerOutput *mixerOutput = [[ZegoMixerOutput alloc] initWithTarget:mixStreamID];
NSMutableArray<ZegoMixerOutput *> *mixerOutputList = [NSMutableArray array];
[mixerOutputList addObject:mixerOutput];
[self.currentMixerTask setOutputList:mixerOutputList];
[self.currentMixerTask enableSoundLevel:YES];
[self.currentMixerTask setAudioConfig:[ZegoMixerAudioConfig defaultConfig]];
}
//startMixerTask 为示例代码封装方法 https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/44169b0962cf1a08fbb6e798eb452de6c66554f6/best_practice/ZegoCloudSDKDemo/Internal/SDK/Express/ExpressService%2BMixer.swift#L13
[ZegoSDKManager.shared.expressService startMixerTask:self.currentMixerTask completion:^(int errorCode, NSString *info) {
if (errorCode == 0) {
[self updatePKRoomAttributes];
}
if (callback) {
callback(errorCode, info);
}
}];
}
在客户端发起的混流方法中,重要的是要检查调用流混流接口时返回的错误码。如果错误码不为 0,则表示混流失败。在这种情况下,客户端应采取适当的操作,例如重试流混流任务,以确保 PK 对战的正常进行。
自定义混流布局
如果您想设置混流的布局,可以使用ZegoMixerTask
的 setInputList 方法自定义布局。此处我们展示一些简单的设置规则。
例如,如果有两个人,您可以将布局设置为每个人占据屏幕的一半。您可以按如下方式进行设置:
- (NSArray<ZegoMixerInput *> *)getMixVideoInputs:(NSArray<NSString *> *)streamList videoConfig:(ZegoMixerVideoConfig *)videoConfig {
NSMutableArray<ZegoMixerInput *> *inputList = [NSMutableArray array];
if (streamList.count == 2) {
for (int i = 0; i < 2; i++) {
CGFloat left = (int)(videoConfig.resolution.width / streamList.count) * i;
CGFloat top = 0;
CGFloat width = MixVideoSize.width / 2;
CGFloat height = MixVideoSize.height;
CGRect rect = CGRectMake(left, top, width, height);
ZegoMixerInput *input = [[ZegoMixerInput alloc] initWithStreamID:streamList[i] contentType:ZegoMixerInputContentTypeVideo layout:rect];
input.renderMode = ZegoMixerInputRenderModeFill;
input.soundLevelID = 0;
input.volume = 100;
[inputList addObject:input];
}
} else {
//...
}
return inputList;
}
如果有超过两个人,您可以根据需要设置布局(N 行 N 列)。
- (NSArray<ZegoMixerInput *> *)getMixVideoInputs:(NSArray<NSString *> *)streamList videoConfig:(ZegoMixerVideoConfig *)videoConfig {
NSMutableArray<ZegoMixerInput *> *inputList = [NSMutableArray array];
if (streamList.count == 3) {
for (int i = 0; i < streamList.count; i++) {
int left = (i == 0) ? 0 : (int)(MixVideoSize.width / 2);
int top = (i == 2) ? (int)(MixVideoSize.height / 2) : 0;
int width = (int)(MixVideoSize.width / 2);
int height = (i == 0) ? (int)MixVideoSize.height : (int)(MixVideoSize.height / 2);
CGRect rect = CGRectMake(left, top, width, height);
ZegoMixerInput *input = [[ZegoMixerInput alloc] initWithStreamID:streamList[i] contentType:ZegoMixerInputContentTypeVideo layout:rect];
input.renderMode = ZegoViewModeFill;
input.soundLevelID = 0;
input.volume = 100;
[inputList addObject:input];
}
} else if (streamList.count == 4) {
int row = 2;
int column = 2;
int cellWidth = (int)(MixVideoSize.width) / column;
int cellHeight = (int)(MixVideoSize.width) / row;
int left, top;
for (int i = 0; i < streamList.count; i++) {
left = cellWidth * (i % column);
top = cellHeight * (i < column ? 0 : 1);
CGRect rect = CGRectMake(left, top, cellWidth, cellHeight);
ZegoMixerInput *input = [[ZegoMixerInput alloc] initWithStreamID:streamList[i] contentType:ZegoMixerInputContentTypeVideo layout:rect];
input.renderMode = ZegoViewModeFill;
input.soundLevelID = 0;
input.volume = 100;
[inputList addObject:input];
}
} else if (streamList.count == 5) {
int lastLeft = 0;
int height = 432;
for (int i = 0; i < streamList.count; i++) {
if (i == 2) {
lastLeft = 0;
}
int width = (i < 2) ? (int)(MixVideoSize.width / 2) : (int)(MixVideoSize.width / 3);
int left = lastLeft + (width * (i < 2 ? i : (i - 2)));
int top = (i > 1) ? height : 0;
CGRect rect = CGRectMake(left, top, width, height);
ZegoMixerInput *input = [[ZegoMixerInput alloc] initWithStreamID:streamList[i] contentType:ZegoMixerInputContentTypeVideo layout:rect];
input.renderMode = ZegoViewModeFill;
input.soundLevelID = 0;
input.volume = 100;
[inputList addObject:input];
}
} else if (streamList.count > 5) {
int row = (streamList.count % 3 == 0) ? (streamList.count / 3) : (streamList.count / 3) + 1;
int column = 3;
int cellWidth = (int)(MixVideoSize.width) / column;
int cellHeight = (int)(MixVideoSize.height) / row;
int left, top;
for (int i = 0; i < streamList.count; i++) {
left = cellWidth * (i % column);
top = cellHeight * (i < column ? 0 : 1);
CGRect rect = CGRectMake(left, top, cellWidth, cellHeight);
ZegoMixerInput *input = [[ZegoMixerInput alloc] initWithStreamID:streamList[i] contentType:ZegoMixerInputContentTypeVideo layout:rect];
input.renderMode = ZegoViewModeFill;
input.soundLevelID = 0;
input.volume = 100;
[inputList addObject:input];
}
}
return inputList;
}
该示例代码已实现了混合流的默认布局:支持 2 到 9 个玩家的 PK 布局。
因此,示例代码已实现了混流的默认布局,当 PK 中有 2 个人时,是并排布局。当有超过2个人时,屏幕将被分为两行或三行。
如果您需要更复杂的自定义布局,请参考 混流布局文档,了解混流布局的方式,并在示例代码中使用ZEGOLiveStreamingManager.shared.eventDelegates.add(self)
来修改布局:
- (NSArray<ZegoMixerInput *> *)getMixLayoutConfig:(NSArray<NSString *> *)streamList videoConfig:(ZegoMixerVideoConfig *)videoConfig {
NSMutableArray<ZegoMixerInput *> *inputList = [NSMutableArray array];
// ... your logic
return inputList;
}
从服务端发起混流
如果您想要从服务端管理混流,需要在 PK 对战开始时,在服务端启动这两个混流任务。有关从客户端发起时设置混流参数的说明,请参考上述说明。
有关如何从服务端管理混流任务的详细信息,请参考服务端 API:
3 通知观众 PK 对战开始
当 PK 对战开始时,需要通知观众 PK 对战已经开始。观众在收到通知后,可以处理观看 PK 的逻辑。我们建议使用 ZIM SDK 的 房间属性 功能通知观众。
此功能允许应用客户端在房间中设置和同步自定义房间属性。房间属性以键值对的方式存储在 ZEGO 服务器上,ZEGO 服务器处理写冲突调解和其他问题,以确保数据一致性。
同时,应用客户端对房间属性的修改会通过 ZEGO 服务器实时同步到房间中的所有其他观众。
每个房间最多允许设置 20 个属性,key
的长度限制为 16 字节,value
的长度限制为 1024 字节。
当 PK 开始时,主播需要调用 setRoomAttributes 来设置房间的附加属性,表示房间已进入 PK 状态。建议在房间的附加属性中包含以下信息:
host_user_id
:该房间的主播的 userID。request_id
:该 PK 的 requestID。pk_users
:参与 PK 的主播。
在设置房间属性时,为了防止在主播异常退出房间时删除房间的附加属性,导致无法恢复 PK 对战,请确保将 isDeleteAfterOwnerLeft
参数设置为 false
。
生成房间属性的示例代码片段:
- (void)updatePKRoomAttributes {
if (!self.pkInfo) {
return;
}
NSMutableDictionary *pkDict = [NSMutableDictionary dictionary];
LiveUser *hostUser = liveManager.hostUser;
if (hostUser) {
pkDict[@"host_user_id"] = hostUser.id;
}
pkDict[@"request_id"] = self.pkInfo.requestID;
NSMutableArray<PKUser *> *pkAcceptedUserList = [NSMutableArray array];
for (PKUser *pkUser in self.pkInfo.pkUserList) {
if (pkUser.hasAccepted) {
[pkAcceptedUserList addObject:pkUser];
}
}
for (PKUser *pkUser in pkAcceptedUserList) {
for (ZegoMixerInput *zegoMixerInput in self.currentInputList) {
if ([pkUser.pkUserStream isEqualToString:zegoMixerInput.streamID]) {
pkUser.edgeInsets = [self rectToEdgeInsetsWithRect:zegoMixerInput.layout];
}
}
}
NSArray *pkUsers = [pkAcceptedUserList valueForKeyPath:@"toDict"];
pkDict[@"pk_users"] = [pkUsers toJsonString];
ZIMRoomAttributesSetConfig *config = [[ZIMRoomAttributesSetConfig alloc] init];
config.isDeleteAfterOwnerLeft = NO;
//setRoomAttributes 为示例代码封装方法 https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/44169b0962cf1a08fbb6e798eb452de6c66554f6/best_practice/ZegoCloudSDKDemo/Internal/SDK/ZIM/ZIMService%2BRoom.swift#L50
[[ZegoSDKManager sharedManager].zimService setRoomAttributes:pkDict callback:nil];
}
设置完成后,观众将收到 onRoomAttributesUpdated 回调。
开始 PK 对战 - 观众逻辑
在收到 onRoomAttributesUpdated 回调后,如果观众发现有与 PK 对战相关的新添加字段,可以开始处理观看 PK 的逻辑。
1 观众需要根据流 ID 规则拉混流
在本文档的附带示例代码中,混流的流 ID 规则为 ${currentRoomID}__mix
,建议您也设计并使用与房间 ID 相关的这种规则。
拉普通流和混流的方法是相同的。观众可以调用 startPlayingStream
来开始播放混流。
需要处理以下两个细节:
- 需要监听 onPlayerStateUpdate 回调来判断拉流是否成功。如果拉混流失败,需要向用户提示加载消息,并实现相应的重试逻辑。
- 由于生成混流需要一定的时间,观众可能无法立即拉混流。为了优化用户体验,避免黑屏,拉混流后,需要监听 onPlayerRecvVideoFirstFrame 回调。在收到这两个回调中的任意一个后,可以渲染混流。这样可以避免黑屏。
2 在 PK 过程中,观众需要静音主播的单流
一旦观众成功拉混流,就可以开始观看 PK 对战。由于混流已经包含了两个主播的音视频,观众不需要再渲染主播的单流。
因此,观众可以使用 mutePlayStreamAudio 和 mutePlayStreamVideo 来暂停播放主播的单流的音频和视频。这样可以进一步降低成本,避免观众端不必要的流量消耗和性能损失。
不建议此时使用 stopPlayingStream 来停止拉主播的单流。如果这样做,观众在 PK 结束后需要重新拉流,而流切换的速度比使用 mute
要慢得多,这会导致用户体验较差。
结束 PK 对战或退出 PK 对战
在示例代码中,主播可以手动点击结束按钮来终止 PK 对战。
当主播点击结束按钮时,还需要通知另一个主播 PK 已经结束,可以通过 ZEGOLivesSreamingManager
的 endPKBattle 方法来实现。当另一个主播收到此通知时,也需要处理结束 PK 的逻辑。
quitPKBattle 和 endPKBattle 的区别在于,前者只允许玩家退出 PK,而后者会让所有人停止 PK。结束 PK 对战需要执行以下操作:
主播:
- 停止播放另一个主播的单流。
- 结束混流任务。
- 从 房间属性 中删除与 PK 相关的属性。
- 调整 UI,返回到单个主播的直播流状态。
观众:
当观众收到 onRoomAttributesUpdated 回调,表示与 PK 相关的属性已被删除时,可以开始处理以下逻辑:
- 调用 stopPlayingStream 停止播放混流,返回到单个主播的直播流状态。
- 由于观众在 PK 开始时静音了主播的单流,现在需要再次调用 mutePlayStreamAudio 和 mutePlayStreamVideo 来取消静音。
- 调整 UI,返回到单个主播的直播流状态。
如何检测 PK 对战中的异常情况
通过利用定期发送 SEI 消息,可以将其视为客户端之间的“心跳”,并可以用于检测 PK 对战中的异常情况。当一段时间内,未收到主播的 SEI 消息时,可以认为主播的直播流出现异常。逻辑如下:
- 在开始 PK 对战后,记录 PK 的开始时间,并启动一个 2 秒的定时器(可以根据需要调整检测间隔)。
- 在接收到 SEI 消息时,记录每个主播最后发送 SEI 消息的时间。
- 每次定时器触发时,检查最后接收到 SEI 消息的时间。如果最后接收时间与当前时间的时间差大于一定的持续时间,比如 5 秒,就认为主播的直播流异常。
当检测到主播异常时,将在主播的视频画面上出现“主播重新连接”的提示,详情请参考 PKBattleView.swift。
- (void)onPKUserConnectingWithUserID:(NSString *)userID duration:(NSInteger)duration {
if ([userID isEqualToString:self.pkUser.userID]) {
//...
self.connectingTipView.hidden = duration > 5000 ? NO : YES;
}
}
此外,您还需要定义一个最大超时时间。例如,在示例代码中,如果一个 PK 主播的 SEI 消息超过 60 秒没有收到,所有参与 PK 的用户将把该异常主播从 PK 中移除,详情请参考 LiveStreamingViewController.swift。
- (void)onPKUserConnectingWithUserID:(NSString *)userID duration:(NSInteger)duration {
if (duration > 60000) {
if (![userID isEqualToString:[ZegoSDKManager sharedManager].currentUser.id]) {
[liveManager removeUserFromPKBattleWithUserID:userID];
} else {
[liveManager quitPKBattle];
}
}
}
其他功能
如何实现服务端匹配
如果您想向随机主播发起 PK 挑战,您可能需要使用服务端进行匹配。例如,客户端可以向服务端发送请求,服务端将响应目标主播的用户 ID。客户端收到该用户 ID 后,可以使用该用户 ID 调用 startPKBattle
函数,向目标主播发送自动 PK 请求。在使用此接口时,接收到 PK 请求的主播将默认自动同意开始 PK。
如何获取主播的设备状态
根据具体的流媒体场景,可以使用不同的方法来获取主播的设备状态。
场景 1:在 PK 过程中,主播如何获取彼此的设备状态?
在这种实时流媒体互动场景中,主播可以使用 onRemoteCameraStateUpdate 和 onRemoteMicStateUpdate 来获取对方的摄像头和麦克风状态。
需要注意的是,需要在调用 createEngineWithProfile 后调用 setEngineConfig 来启用此功能。以下是示例代码:
ZegoEngineConfig *config = [[ZegoEngineConfig alloc] init];
config.advancedConfig = @{@"notify_remote_device_unknown_status": @"true", @"notify_remote_device_init_status": @"true"};
[ZegoExpressEngine setEngineConfig:config];
场景 2:观众获取主播的设备状态
在播放直播
或混流
的流时,建议使用 SEI(Supplemental Enhancement Information)方案来获取主播的设备状态。这包括 PK 对战场景中的以下两种情况:
- 在 PK 模式下,观众播放混流。
- 在非 PK 模式下,观众播放主播的单路流。
在这种情况下,观众无法接收到“场景 1”中提到的回调。因此,当主播推流时,他们需要使用 sendSEI 更新自己的设备状态,而拉流端将通过回调 onPlayerRecvSEI 接收。
- 有关 SEI 功能的详细说明,请参考 媒体补充增强信息(SEI)。
- 关于 SEI 和混流的其他信息:混流服务端将重新编码所有输入流的 SEI 到输出混流中。因此,主播发送的 SEI 信息可以被房间内的观众和其他主播接收到。
如需要发送 SEI,您需要创建一个定时器来定期发送 SEI 消息。建议每 200ms 发送一次。在定时器中,定期发送以下信息以同步设备状态:
{
'type': 0, // 设备状态
'senderID': myUserID, // 由于将多个流的 SEI 混合到混流中,需要在 SEI 中添加 senderID 标识符。
'cam': false, // true: 打开,false: 关闭
'mic': true, // true: 打开,false: 关闭
}
您可以参考示例代码中的 相关代码 来具体实现此部分。
在实现视频通话、直播等视频场景时,请注意视频分辨率与价格之间的关系。
在同一房间内播放多个视频流时,将基于分辨率总和进行计费,并且不同分辨率将对应不同的计费档次。
计算最终分辨率时,包括以下视频流:
- 直播视频画面(例如主播画面、连麦画面、PK 对战画面等)
- 视频通话中,每个人的视频画面
- 屏幕共享画面
- 云录制服务的分辨率
- 直播创作分辨率
在您的应用上线前,请确保您已检查所有配置并确认适用于您业务场景的计费档次,以避免不必要的损失。更多详情请参见 定价。
常见问题解答
在大多数情况下,PK 对战期间不支持共同主持功能。因此,如果您已经实现了共同主持功能,需要考虑以下事项: 一旦房间进入 PK 对战状态,所有客户端角色都需要临时禁用与共同主持人相关的所有功能。
以下是一些建议:
- 主播开始 PK 后,拒绝所有共同主持请求。
- 如果共同主持人正在共同主持过程中,收到 PK 已开始的通知,则自动终止共同主持会话,并向观众显示弹出消息,指示房间已开始 PK,共同主持会话已结束。
- 在 PK 过程中,隐藏与共同主持人相关的任何入口点。
通常,可以使用 音量变化与音频频谱 来渲染音量级别。
然而,当观众端拉混流时,方法略有不同:
- 在开始混流任务时,将
enableSoundLevel
设置为true
,以便混流包含输入流的音量级别信息。 - 在开始混流任务时,为每个输入流分配一个
soundLevelID
,以便拉流端可以确定其属于哪个音量级别数据。 - 拉流端可以监听 onMixerSoundLevelUpdate 来接收音量级别数据,可以使用
soundLevelID
来确定其属于哪个音量级别。
在 PK 期间,可能需要临时静音另一个主播的音频。静音后,房间中的主播和观众都无法听到另一个主播的音频。
- 主播可以使用 mutePlayStreamAudio 静音另一个主播的音频流。
- 为确保房间中的观众也无法听到另一个主播的音频,可以通过再次调用 startMixerTask 来更新混流任务,将另一个主播的 inputstream 的
contentType
设置为ZegoMixerInputContentTypeVideoOnly
(无需调用StopMixerTask
,直接调用startMixerTask
)。
在客户端成功发起混流后,客户端可以向业务服务端报告房间已进入 PK 状态。
您可以使用 git diff 查看这些更改,并按照我们的文档逐项检查更改并将其添加到您的项目中。

-
首先,您需要在 Package Dependencies 中升级 SDK 版本,请使用以下或更高版本的 SDK。
NSString *ZIMVersion = @"2.13.1"; NSString *ZegoExpressEngineVersion = @"3.12.4";
-
示例代码的
internal
部分已经完全升级以适应多人 PK 功能。这部分发生了重大变化。我们建议您直接删除旧的internal
文件夹,并将新的internal
复制到您的项目中。注意- 我们不建议对
internal
进行任何修改。 - 如果由于某种原因必须修改
internal
中的代码,在将新的internal
复制到您的项目后,您需要将您对旧的internal
进行的更改应用到新的internal
中,这是一个必要的步骤。
- 我们不建议对
-
示例代码中的
components
和ViewControllers
,以及res
中的布局文件,也已经升级以适应多人 PK,这部分的更改相对较小:- 新文件可以直接复制到您的项目中。
- 旧文件中有一些更改,您可以参考 git diff 逐步升级。
完成升级后,请对新旧功能进行全面测试。如果在测试过程中遇到任何疑似的错误,请以相同的方式直接测试我们的示例代码,以确定是示例代码的问题还是其他原因引入的问题。