logo
当前页

实现 PK


PK 对战是两个主播之间的友好竞争,展示给观众的是主播之间的互动。 本文档介绍如何在使用实时音视频产品(ZEGO Express SDK)实现基本直播功能的基础上,结合即时通讯产品(ZIM SDK)实现 PK 对战功能。

通常有两种玩法:

  1. 通过主播之间主动发起的 PK 对战:主播在自己的直播开始后,可以向他们想要连接的主播发送 PK 对战请求。一旦 PK 对战请求被接受,两个主播将会连接在一起。(我们将以次为例进行说明)
  2. 由业务服务器协调的 PK 对战:主播在申请 PK 匹配后,业务服务器将协调主播开始 PK 对战,主播将会自动连接到彼此。

本文档将介绍如何在直播场景中实现 PK 对战。

注意
  • 本文是基于 示例代码 来介绍的如何实现PK功能,因此文档中的部分方法为示例代码封装方法。
  • 示例代码把多个SDK进行了封装以方便管理,请参考:管理多个 SDK

前提条件

在开始之前,请确保您已完成以下步骤:

预览效果

您可以使用本文档提供的 示例代码 实现以下效果:

主页直播页面接收PK对战请求PK对战

技术原理

以下是本文档内容的结构和概述:

  1. 如何发送 PK 对战邀请:解释如何利用 呼叫邀请 发送 PK 邀请的过程。
  2. PK 拉流和推流的介绍:基于流的概念,介绍 PK 对战的推流和拉流解决方案的框架。
  3. 实现主播端逻辑:提供主播端 PK 的解决方案的详细信息,包括如何与其他主播互动,为观众开启混流,以及在 PK 对战开始时通知观众。
  4. 实现观众端逻辑:提供观众端 PK 的解决方案的细节,包括如何观看主播之间的互动及如何处理单流场景。
  5. 如何结束 PK 对战:描述结束 PK 会话,并恢复正常的单主播直播的步骤。
  6. 如何检测 PK 对战中的异常情况:描述如何通过 SEI 检测 PK 主机的异常情况。
  7. 其他功能介绍:包括使用 SEI 检测主机设备和流的状态,处理客户端断开连接或崩溃等异常情况。
  8. 常见问题解答:解答与 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 对战”为例):

有关这些接口的具体用法,请参考 呼叫邀请

  1. 在实现 PK 对战邀请时,需要注意在邀请接口的 extendedData 字段中,需要传递双方的信息: A 在发起 PK 对战邀请时,除了上述的 type 之外,还需要将自己的 roomIDuserName 传递给对方,以便对方知道开始 PK 对战的相关信息。 B 在接受 PK 对战邀请时,同样需要在 type 之外,传递自己的 roomIDuserName

  2. 在第一次邀请时,请调用 callInvite 并将其设置为高级模式。在此模式下,您需要使用 callEndcallQuit 来结束或退出 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 的推流与拉流

开始之前,请确保您熟悉以下概念:

  1. 什么是房间和流?

    • ZEGO Express SDK:由 ZEGO 提供的实时音视频通话 SDK。它可以帮助您提供具有便捷访问、高清流畅、跨平台通信、低延迟和高并发性的音视频服务。
    • 推流:将捕获并打包的音视频数据流推送到 ZEGO 实时音视频云的过程。
    • 拉流:从 ZEGO 实时音视频云接收并播放音视频数据流的过程。
    • 房间:ZEGO 提供的服务,用于组织用户群体,并允许同一房间内的用户相互接收和发送实时音视频和消息。
      • 用户只有在登录到房间后才能推流或拉流。
      • 用户可以接收房间内的变化通知(例如用户加入或离开房间,以及音视频流的变化)。
  2. 什么是混流服务? 通过混流服务,多路媒体流可以组合成一个单一的流,这样观众只需播放一个流,即可提高质量并降低性能成本。

    更多详细信息,请参考 混流

PK 解决方案需要使用 混流:混流指将多个流合并为一个流,这样观众只需播放该流,即可观看多个主播的画面。使用混流的必要性和重要优势如下:

  • 必要性:在观众观看多个主播时,它确保音视频的相对实时同步,避免两个主播延迟不一致,导致互动体验差。
  • 优势:客户端无需解码和拉多个流,可以节省观众端的带宽,并防止低端设备过热,可进一步提升观看体验。

在 PK 开始之前,每个主播都会推流,观众可以直接拉主播的流。然而,在 PK 开始后,拉流的方法将发生变化:

  • 除了推自己的流,每个主播还需要拉对方的流,以实现主播间的实时音视频互动。
  • 观众暂时静音主播的单流:使用静音可以节省带宽,并在 PK 结束后快速恢复主播的单流画面。
  • 开始混合两个主播的流:客户端或服务端可以管理混流,稍后将详细解释。
  • 观众播放混流:观看两位主播的互动。

以上是 PK 场景中推流和拉流的基本框架。在接下来的章节中,我们将根据主播和观众的逻辑提供详细操作流程。

开始 PK 对战- 主播逻辑

当主播准备开始 PK 对战时,需要执行以下操作:

1 播放每个主播的单一流

通常,流 ID 与房间 ID 和用户 ID 相关。例如,在本文档的附带示例代码中,流 ID 规则为 "${roomID}_${userID}_main_host"。因此,您可以使用此规则,连接每个主播的流 ID,然后调用 startPlayingStream 来拉对方的流。双方的信息可以从邀请接口的回调和双方之间传递的 extendedData 中获取。

注意

如果您的 PK 对战是由服务端安排和匹配的,并且服务端发送了开始 PK 信号,您需要在向主播发送 PK 开始通知时,包含对手主播的 userIDroomIDuserName 等信息。

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=810height=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 对战的正常进行。

自定义混流布局

如果您想设置混流的布局,可以使用ZegoMixerTasksetInputList 方法自定义布局。此处我们展示一些简单的设置规则。

例如,如果有两个人,您可以将布局设置为每个人占据屏幕的一半。您可以按如下方式进行设置:

- (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:

说明
  • 在服务端发起的混流方法中,服务端需要使用 用户退出房间回调 监控主播的客户端状态。如果服务端检测到主播异常退出,需要及时停止相应的混流任务,并向主播发送通知以结束 PK 对战。
  • 如果您的服务器没有信令通道用于向客户端发送通知,我们建议使用 ZIM 的 发送房间消息 来实现。

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 对战。由于混流已经包含了两个主播的音视频,观众不需要再渲染主播的单流。

因此,观众可以使用 mutePlayStreamAudiomutePlayStreamVideo 来暂停播放主播的单流的音频和视频。这样可以进一步降低成本,避免观众端不必要的流量消耗和性能损失。

注意

不建议此时使用 stopPlayingStream 来停止拉主播的单流。如果这样做,观众在 PK 结束后需要重新拉流,而流切换的速度比使用 mute 要慢得多,这会导致用户体验较差。

结束 PK 对战或退出 PK 对战

在示例代码中,主播可以手动点击结束按钮来终止 PK 对战。

当主播点击结束按钮时,还需要通知另一个主播 PK 已经结束,可以通过 ZEGOLivesSreamingManagerendPKBattle 方法来实现。当另一个主播收到此通知时,也需要处理结束 PK 的逻辑。

quitPKBattleendPKBattle 的区别在于,前者只允许玩家退出 PK,而后者会让所有人停止 PK。结束 PK 对战需要执行以下操作:

主播:

  1. 停止播放另一个主播的单流。
  2. 结束混流任务。
  3. 房间属性 中删除与 PK 相关的属性。
  4. 调整 UI,返回到单个主播的直播流状态。

观众:

当观众收到 onRoomAttributesUpdated 回调,表示与 PK 相关的属性已被删除时,可以开始处理以下逻辑:

  1. 调用 stopPlayingStream 停止播放混流,返回到单个主播的直播流状态。
  2. 由于观众在 PK 开始时静音了主播的单流,现在需要再次调用 mutePlayStreamAudiomutePlayStreamVideo 来取消静音。
  3. 调整 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 过程中,主播如何获取彼此的设备状态?

在这种实时流媒体互动场景中,主播可以使用 onRemoteCameraStateUpdateonRemoteMicStateUpdate 来获取对方的摄像头和麦克风状态。

需要注意的是,需要在调用 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: 关闭
}

您可以参考示例代码中的 相关代码 来具体实现此部分。

注意

在实现视频通话、直播等视频场景时,请注意视频分辨率与价格之间的关系。

在同一房间内播放多个视频流时,将基于分辨率总和进行计费,并且不同分辨率将对应不同的计费档次。

计算最终分辨率时,包括以下视频流:

  1. 直播视频画面(例如主播画面、连麦画面、PK 对战画面等)
  2. 视频通话中,每个人的视频画面
  3. 屏幕共享画面
  4. 云录制服务的分辨率
  5. 直播创作分辨率

在您的应用上线前,请确保您已检查所有配置并确认适用于您业务场景的计费档次,以避免不必要的损失。更多详情请参见 定价

常见问题解答

在大多数情况下,PK 对战期间不支持共同主持功能。因此,如果您已经实现了共同主持功能,需要考虑以下事项: 一旦房间进入 PK 对战状态,所有客户端角色都需要临时禁用与共同主持人相关的所有功能。

以下是一些建议:

  1. 主播开始 PK 后,拒绝所有共同主持请求。
  2. 如果共同主持人正在共同主持过程中,收到 PK 已开始的通知,则自动终止共同主持会话,并向观众显示弹出消息,指示房间已开始 PK,共同主持会话已结束。
  3. 在 PK 过程中,隐藏与共同主持人相关的任何入口点。

通常,可以使用 音量变化与音频频谱 来渲染音量级别。

然而,当观众端拉混流时,方法略有不同:

  1. 在开始混流任务时,将 enableSoundLevel 设置为 true,以便混流包含输入流的音量级别信息。
  2. 在开始混流任务时,为每个输入流分配一个 soundLevelID,以便拉流端可以确定其属于哪个音量级别数据。
  3. 拉流端可以监听 onMixerSoundLevelUpdate 来接收音量级别数据,可以使用 soundLevelID 来确定其属于哪个音量级别。

在 PK 期间,可能需要临时静音另一个主播的音频。静音后,房间中的主播和观众都无法听到另一个主播的音频。

  1. 主播可以使用 mutePlayStreamAudio 静音另一个主播的音频流。
  2. 为确保房间中的观众也无法听到另一个主播的音频,可以通过再次调用 startMixerTask 来更新混流任务,将另一个主播的 inputstreamcontentType 设置为 ZegoMixerInputContentTypeVideoOnly(无需调用StopMixerTask,直接调用startMixerTask)。

在客户端成功发起混流后,客户端可以向业务服务端报告房间已进入 PK 状态。

您可以使用 git diff 查看这些更改,并按照我们的文档逐项检查更改并将其添加到您的项目中。

  1. 首先,您需要在 Package Dependencies 中升级 SDK 版本,请使用以下或更高版本的 SDK。

    NSString *ZIMVersion = @"2.13.1";
    NSString *ZegoExpressEngineVersion = @"3.12.4";
  2. 示例代码的 internal 部分已经完全升级以适应多人 PK 功能。这部分发生了重大变化。我们建议您直接删除旧的 internal 文件夹,并将新的 internal 复制到您的项目中。

    注意
    • 我们不建议对 internal 进行任何修改。
    • 如果由于某种原因必须修改 internal 中的代码,在将新的 internal 复制到您的项目后,您需要将您对旧的 internal 进行的更改应用到新的 internal 中,这是一个必要的步骤。
  3. 示例代码中的 componentsViewControllers,以及 res 中的布局文件,也已经升级以适应多人 PK,这部分的更改相对较小:

    • 新文件可以直接复制到您的项目中。
    • 旧文件中有一些更改,您可以参考 git diff 逐步升级。

完成升级后,请对新旧功能进行全面测试。如果在测试过程中遇到任何疑似的错误,请以相同的方式直接测试我们的示例代码,以确定是示例代码的问题还是其他原因引入的问题。

Previous

实现连麦

Next

使用 Token 鉴权