直播答题
  • 概述
  • SDK 集成指引
  • 实现流程

观众端功能实现流程

更新时间:2021-04-25 20:25

1 接收题目

用户端在 MediaSideInfo(媒体次要信息) 中获取到主播端发送的题目信息。题目信息会按照预先约定的 JSON 格式组织。

如果开发者使用即构提供的主播端推流,请务必按照特定的 JSON 格式解析数据
如果开发者自行实现主播端推流,内部约定主播端与观众端的通信格式即可。

获取 MediaSideInfo 的开发步骤如下所示。

1.1 设置回调,接收媒体次要信息

ZEGO SDK 提供了相关接口,用于设置回调,接收媒体次要信息。

ZegoLiveRoomApi-Player.h

/**
 设置回调, 接收媒体次要信息

 @param onMediaSideCallback 回调函数指针, pszStreamID:流ID,标记当前回调的信息属于哪条流, buf:接收到的信息数据(具体内容参考官网对应文档中的格式说明), dataLen:buf 总长度
 @discussion 开始拉流前调用。观众端在此 API 设置的回调中获取主播端发送的次要信息(要求主播端开启发送媒体次要信息开关,并调用 [ZegoLiveRoomApi (Publisher) -sendMediaSideInfo:dataLen:packet:] 发送次要信息)。当不需要接收信息时,需将 onMediaSideCallback 置空,避免内存泄漏
 */
- (void)setMediaSideCallback:(void(*)(const char *pszStreamID, const unsigned char* buf, int dataLen))onMediaSideCallback;

代码示例如下:

ZegoPlayViewController.m

[[ZegoSDKManager api] setMediaSideCallback:onReceivedMediaSideInfo];

其中,onReceivedMediaSideInfo 是满足特定格式要求的 C 语言函数。当用户端收到媒体次要信息时,SDK 会调用之。因此,对题目的处理,均在 onReceivedMediaSideInfo 中进行。

1.2 实现回调函数,处理媒体次要信息

媒体次要信息会作为 onReceivedMediaSideInfo 的入参传递进去,开发者需要在该函数内,根据业务要求,处理媒体次要信息。

主播向观众发送答案的 JSON 数据格式为:

{
    "seq": 1,
    "type": "question",
    "data": {
        "id": "0ca175b9c0f726a831d895e269332461",
        "index": 1,
        "activity_id": "123456",
        "title": "下面哪个省的面积最大? ",
        "options": [
            {
                "answer": "A",
                "option": "河北"
            },
            {
                "answer": "B",
                "option": "山东"
            },
            {
                "answer": "C",
                "option": "湖南"
            }
        ]
    }
}

JSON 字段含义为:

参数名 类型 说明
seq Int 协议序列号,一轮活动内递增
type String 消息类型;可用类型为:question(题目),answer(答案),sum(用户列表汇总)
id String 题目 id;题目的唯一标识,由客户保证唯一性;最大 64 bytes(包括字符串结束符),
不能包含字符 ','
index Int 题目序号;第 n 题序号为 n
activity_id String 活动 id,答题场次(活动)的唯一标识;由客户保证唯一性;一个答题场次有多个题目;
最大 64 bytes(包括字符串结束符),不能包含字符 ','
title String 题目标题;可与 index 配合展示为题目描述
options Array 题目选项列表
answer String 某一选项的序号
option String 某一选项的描述

演示 Demo 中处理媒体次要信息的示例代码如下:

ZegoPlayViewController.m

void onReceivedMediaSideInfo(const char *pszStreamID, const unsigned char* buf, int dataLen) {
    if (dataLen == 0) {
        NSLog(@"%s, data is empty", __func__);
        return;
    }

    NSData *mediaInfo = [NSData dataWithBytes:buf + 4 length:dataLen - 4];
    NSError *error = nil;
    NSDictionary *info = [NSJSONSerialization JSONObjectWithData:mediaInfo options:0 error:&error];

    if (error == nil) {
        int seq = [info[@"seq"] intValue];
        if (seq <= mediaSeq) {
            NSLog(@"%s, repeat seq: %d, discard", __func__, seq);
            return;
        }

        NSLog(@"%s, type: %@, activityId: %@, questionId: %@", __func__, info[@"type"], info[@"data"][@"id"], info[@"data"][@"activity_id"]);

        mediaSeq = seq;

        if ([info[@"type"] isEqualToString:questionKey]) {
            [selfObject handleQuestionInfo:info];
        } else if ([info[@"type"] isEqualToString:answerKey]) {
            [selfObject handleAnswerInfo:info];
        } else if ([info[@"type"] isEqualToString:sumKey]) {
            [selfObject handleFinalResult:info];
        } else {
            NSLog(@"onReceivedMediaSideInfo unknown type, don't handle");
        }
    }
}

关于媒体次要信息功能的详细说明,可参考文档:视频进阶-媒体次要信息

2 回答题目

开发者从媒体次要信息中,解析出题目,并正确展示给用户后,用户可以开始作答。SDK 提供了统一的接口,用于用户答题。

ZegoLiveRoomApi-Relay.h

/**
 转发接口

 @param data 需要转发的数据
 @param type 转发类型
 @param completionBlock 转发发送结果,回调 server 下发的转发结果
 @return true 成功,false 失败
 @discussion 这个接口用来实现大并发的调用,实现观众答题的功能就需要调用这个接口
 */
- (bool)relayData:(NSString *)data type:(ZegoRelayType)type completion:(ZegoRelayCompletionBlock)completionBlock;

调用该接口答题,需要注意以下事项:

  1. data 为指定格式(见下文)的 JSON 数据转换后的字符串。
  2. 如果是答题业务,type 需要指定为 ZEGO_RELAY_TYPE_DATI

请注意,1 中指定的 JSON 格式如下。开发者必须按照此格式传递字符串,格式不对会导致后台解析错误,答题出错。

{
    "activity_id": "",
    "question_id": "",
    "answer": "",
    "user_data": ""
}

参数说明为:

参数名 类型 说明
activity_id String 活动 id,答题场次(活动)的唯一标识;由客户保证唯一性;一个答题场次有多个题目;
最大 64 bytes(包括字符串结束符),不能包含字符 ',';观众端从主播端获取活动 id
question_id String 题目 id;题目的唯一标识,由客户保证唯一性;最大 64 bytes(包括字符串结束符),
不能包含字符 ',';观众端从主播端获取题目 id
answer String 观众答题,要与主播下发题目中的 answer 字段值(例如 "A")保持一致
user_data String 用户自定义数据。开发者上传 user_data 后可调用拉取流水信息接口获取。该数据可以
用于进行消息一致性、完整性校验。

Demo 中示例代码如下:

ZegoPlayViewController.m

- (void)relayDataWithAnswer:(NSString *)answer {
    self.currentQuiz.userAnswer = answer;
    NSString *relayData = [ZegoQuizParser assembleRelayData:self.activityInfo.activityID questionID:self.currentQuiz.quizID answer:answer userData:nil];

    if (relayData == nil) {
        NSLog(@"relay data is nil");
        return;
    }

    BOOL invokeSuccess = [[ZegoSDKManager api] relayData:relayData type:ZEGO_RELAY_TYPE_DATI completion:^(int errorCode, NSString *roomId, NSString *relayResult) {
        if (errorCode == 0) {
            NSLog(@"relay data send succeed");
        } else {
            NSLog(@"relay data send failed, errorCode: %d", errorCode);
        }
    }];

    if (!invokeSuccess) {
        NSLog(@"relay data invoke failed");
    }
}

3 接收答案及统计信息

观众端仍然在媒体次要信息中接收每道题答案、统计结果。
对媒体次要信息的获取同第一小节一致,处理逻辑视业务逻辑而定。

主播向观众发送答案的 JSON 数据格式为:

{
    "seq": 2,
    "type": "answer",
    "data": {
        "id": "0ca175b9c0f726a831d895e269332461",
        "correct_answer": "B",
        "activity_id": "123456",
        "answer_stat": [
            {
                "answer": "A",
                "user_count": 2000
            },
            {
                "answer": "B",
                "user_count": 1300
            },
            {
                "answer": "C",
                "user_count": 420
            }
        ]
    }
}

参数说明为:

参数名 类型 说明
seq Int 协议序列号,一轮活动内递增
type String 消息类型;可用类型为:question(题目),answer(答案),sum(用户列表汇总)
id String 题目 id;题目的唯一标识,由客户保证唯一性;最大 64 bytes(包括字符串结束符),
不能包含字符 ','
correct_answer String 某道题目的正确答案
activity_id String 活动 id,答题场次(活动)的唯一标识;由客户保证唯一性;一个答题场次有多个题目;
最大 64 bytes(包括字符串结束符),不能包含字符 ','
answer_stat Array 结果统计列表
answer String 某一选项的序号
user_count String 选择了该选项的用户总人数

4 接收用户列表

每轮活动结束后,观众端或需要拉取最终胜利的用户列表,该信息仍然在媒体次要信息中获取。
对媒体次要信息的获取同第一小节一致,处理逻辑视业务逻辑而定。

主播向观众发送用户列表的 JSON 数据格式为:

{
    "seq": 3,
    "type": "sum",
    "data": {
        "room_id": "roomid123",
        "activity_id": "123456",
        "user_list": [
            {
                "id_name": "555",
                "nick_name": "lzp"
            },
            {
                "id_name": "666",
                "nick_name": "hhh"
            }
        ]
    }
}

参数说明为:

参数名 类型 说明
seq Int 协议序列号,一轮活动内递增
type String 消息类型;可用类型为:question(题目),answer(答案),sum(用户列表汇总)
room_id String 房间 id;房间的唯一标识
activity_id String 活动 id,答题场次(活动)的唯一标识;由客户保证唯一性;一个答题场次有多个题目;
最大 64 bytes(包括字符串结束符),不能包含字符 ','
user_list Array 最终胜利的用户列表
id_name String 用户 id,用户的唯一标识
nick_name String 用户昵称

5 大房间消息的发送与接收

直播答题房间内人数数量可能非常多,房间消息会出现高并发使用场景。

开发者需要调用 ZEGO SDK 提供的、用于发送/接受大房间消息的接口,来实现房间内消息处理。
如果开发者仍然使用普通房间消息收发接口(sendRoomMessage onRecvRoomMessage),Zego 后台服务可能会丢弃较多消息,影响业务方的正常功能。

5.1 发送消息

开发者可调用如下 API 发送大房间消息。

ZegoLiveRoomApi-IM.h

/**
 房间发送不可靠信道的消息

 @param content 消息内容
 @param type 消息类型,可以自定义
 @param category 消息分类,可以自定义
 @param completionBlock 消息发送结果,回调 server 下发的 messageId
 @return true 成功,false 失败
 @discussion 用于高并发的场景,消息可能被丢弃,当高并发达到极限时会根据策略丢弃部分消息
 */
- (bool)sendBigRoomMessage:(NSString *)content type:(ZegoMessageType)type category:(ZegoMessageCategory)category completion:(ZegoBigRoomMessageCompletion)completionBlock;

Demo 代码示例如下:

ZegoPlayViewController.m

- (void)sendRoomMessage {
    [self.customCommentView.commentInput resignFirstResponder];
    if (self.customCommentView.commentInput.text.length == 0) {
        NSLog(@"%s,评论为空,不发送任何信息", __func__);
        return;
    }

    NSString *comment = self.customCommentView.commentInput.text;
    bool ret = [[ZegoSDKManager api] sendBigRoomMessage:comment
                                                   type:ZEGO_TEXT
                                               category:ZEGO_CHAT
                                             completion:nil];

    if (ret) {
        ZegoBigRoomMessage *roomMessage = [ZegoBigRoomMessage new];
        roomMessage.fromUserId = [ZegoSetting sharedInstance].userID;
        roomMessage.fromUserName = [ZegoSetting sharedInstance].userName;
        roomMessage.content = comment;
        roomMessage.type = ZEGO_TEXT;
        roomMessage.category = ZEGO_CHAT;
        roomMessage.priority = ZEGO_DEFAULT;

        [self.mockedMessageList addObject:roomMessage];
        self.messageViewController.messageList = self.mockedMessageList;
        self.customCommentView.commentInput.text = @"";
    }
}

5.2 接收消息

正常接收消息前,需要先设置代理对象,调用如下 API 进行:

/**
 设置 IM 代理对象

 @param imDelegate 遵循 ZegoIMDelegate 协议的代理对象
 @return true 成功,false 失败
 @discussion 使用 IM 功能,初始化相关视图控制器时需要设置代理对象。未设置代理对象,或对象设置错误,可能导致无法正常收到相关回调
 */
- (bool)setIMDelegate:(id<ZegoIMDelegate>)imDelegate;

代理对象设置成功后,当同一房间内的其他用户发送大房间消息时,可在如下回调中接收消息:

ZegoLiveRoomApi-IM.h

/**
 收到房间的不可靠消息广播

 @param roomId 房间 Id
 @param messageList 消息列表,包括消息内容,消息分类,消息类型,发送者等信息
 @discussion 调用 [ZegoLiveRoomApi (IM) -setIMDelegate:] 设置代理对象成功后,调用 [ZegoLiveRoomApi (IM) -sendBigRoomMessage:type:category:completion:] 发送消息,会触发此通知
 */
- (void)onRecvBigRoomMessage:(NSString *)roomId messageList:(NSArray<ZegoBigRoomMessage*> *)messageList;

Demo 代码示例如下:

ZegoPlayViewController.m

- (void)onRecvBigRoomMessage:(NSString *)roomId messageList:(NSArray<ZegoBigRoomMessage*> *)messageList {
    if (![roomId isEqualToString:self.roomInfo.roomID]) {
        NSLog(@"%s, receive big room message, but roomId mismatch, abandon", __func__);
        return;
    }

    if (messageList.count == 0) {
        NSLog(@"%s, receive big room message, but messageList is nil", __func__);
        return;
    }

    [self.mockedMessageList addObjectsFromArray:messageList];
    self.messageViewController.messageList = self.mockedMessageList;
}

6 获取实时在线人数

ZEGO SDK 提供如下 API,向开发者提供房间内实时在线用户数。该回调目前默认为 30s 回调一次,开发者可联系即构技术支持,自定义回调频率。

ZegoLiveRoomApi-IM.h

/**
 收到在线人数更新

 @param onlineCount 在线人数。默认为 30s 回调一次,开发者可联系即构技术支持,自定义回调频率。
 @param roomId 房间 Id
 */
- (void)onUpdateOnlineCount:(int)onlineCount room:(NSString *)roomId;

请注意,通过该回调接收在线人数更新前,需要调用 setIMDelegate: 设置 IM 代理对象,具体设置步骤同 5.2 节中 setIMDelegate: 一致。

Demo 代码示例如下:

ZegoPlayViewController.m

// 更新在线人数 label
- (void)onUpdateOnlineCount:(int)onlineCount room:(NSString *)roomId {
    if ([roomId isEqualToString:self.roomInfo.roomID]) {
        self.onlineCountLabel.text = [NSString stringWithFormat:@"%d", onlineCount];
    }
}