直播答题

观众端功能实现流程

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

1 接收题目

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

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

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

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

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

/**
* 设置回调,接收媒体次要信息
*/
mZegoLiveRoom.setZegoMediaSideCallback(new IZegoMediaSideCallback() {
    @Override
    public void onRecvMediaSideInfo(String streamID, ByteBuffer byteBuffer, int dataLen) {

    }
});

其中,当用户端收到媒体次要信息时,SDK 会调用之。因此,对题目的处理,均在 onRecvMediaSideInfo 中进行。

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

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

主播向观众发送答案的 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 中处理媒体次要信息的示例代码如下:

/**
    * 设置回调,接收媒体次要信息
   */
mZegoLiveRoom.setZegoMediaSideCallback(new IZegoMediaSideCallback() {
        @Override
        public void onRecvMediaSideInfo(String streamID, ByteBuffer byteBuffer, int dataLen) {
            try {
                if (dataLen == 0) {
                    AppLogger.getInstance().writeLog(this.getClass(), "onRecvMediaSideInfo data is empty");
                    return;
                }
                //转换成JSONObject格式
                JSONObject jsonObject = ZegoCommon.getInstance().getJsonObjectFrom(byteBuffer, dataLen);
                if (jsonObject != null && jsonObject.isNull("type")) {
                    int seq;
                    seq = jsonObject.getInt("seq");
                    //seq比较去重
                    if (seq <= mediaSeq) {
                        AppLogger.getInstance().writeLog(this.getClass(), "onRecvMediaSideInfo repeat seq %d, discard  mediaSeq  %d", seq, mediaSeq);
                        return;
                    }
                    mediaSeq = seq;
                }
                    JSONObject jsonObjectData = jsonObject.getJSONObject("data");
                /**
                * 显示答题
                */
                if (!jsonObject.isNull("type") && "question".equals(jsonObject.getString("type"))) {
                    //转换成map格式
                    Map<String, Object> map = ZegoCommon.getInstance().getMapFromJsonToMapQuestion(jsonObjectData);
                    //当前线程是子线程,需要用handler在主线程控制
                    handler.sendMessage(handler.obtainMessage(ANSWER_DIALOG, map));
                    /**
                    * 答案处理
                    */
                } else if (!jsonObject.isNull("type") && "answer".equals(jsonObject.getString("type"))) {
                    //转换成map格式
                    Map<String, Object> map = ZegoCommon.getInstance().getMapFromJsonToMapAnswer(jsonObjectData);
                    //当前线程是子线程,需要用handler在主线程控制
                    handler.sendMessage(handler.obtainMessage(ATATISTICS_ANSWER, map));
                }
            } catch (org.json.JSONException e) {
                AppLogger.getInstance().writeLog(this.getClass(), "json data is conversion exception");
                e.printStackTrace();
            }
    }
});

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

2 回答题目

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

//发送答题信息
mZegoLiveRoom.relay(ZegoRelay.RelayTypeDati, relayDate, new IZegoRelayCallback() {
    @Override
    public void onRelay(int errorCode, String roomID, String relayResult) {
    }
});

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

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

请注意,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 中示例代码如下:

@SuppressLint("HandlerLeak")
Handler retryHandler = new Handler() {
        @Override
        public void handleMessage(final Message msg) {
            final String relayDate = (String) msg.obj;
            mZegoLiveRoom.relay(ZegoRelay.RelayTypeDati, relayDate, new IZegoRelayCallback() {
                @Override
                public void onRelay(int errorCode, String roomID, String relayResult) {
                    int msgWhat = msg.what;
                    AppLogger.getInstance().writeLog(this.getClass(), "onRelay errorCode:%d  roomID:%s   relayResult:%s  relayDate:%s  msgWhat:%s",         errorCode, roomID, relayResult, relayDate, msgWhat);
                    //如果发送失败,则1秒重复发送一次
                    if (errorCode != 0 && msgWhat < 5) {
                        msgWhat = msgWhat + 1;
                        retryHandler.sendMessageDelayed(retryHandler.obtainMessage(msgWhat, relayDate), 1000);
                    }
                }
            });
        }
};

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 发送大房间消息。

/**
 房间发送不可靠信道的消息
 @param type 消息类型,可以自定义
 @param category 消息分类,可以自定义、
 @param msg 消息内容
 @param IZegoBigRoomMessageCallback 消息发送结果,回调 server 下发的 messageId
 @return true 成功,false 失败
 @discussion 用于高并发的场景,消息可能被丢弃,当高并发达到极限时会根据策略丢弃部分消息
 */
 mZegoLiveRoom.sendBigRoomMessage(ZegoIM.MessageType.Text, ZegoIM.MessageCategory.Chat, msg, new IZegoBigRoomMessageCallback() {
    @Override
    public void onSendBigRoomMessage(int errorCode, String roomID, String messageID) {

    }
 });

Demo 代码示例如下:

/**
* 发送房间不可靠消息
* @param msg 消息内容
*/
private void sendBigRoomMsg(String msg) {
    AppLogger.getInstance().writeLog(this.getClass(), "sendBigRoomMsg msg %s", msg);
    BigMessage bigMessage = new BigMessage();
    bigMessage.setContent(msg);
    bigMessage.setFromUserName(android.os.Build.MODEL);
    roomAdapter.addMsgToString(bigMessage);
    mZegoLiveRoom.sendBigRoomMessage(ZegoIM.MessageType.Text, ZegoIM.MessageCategory.Chat, msg, new IZegoBigRoomMessageCallback() {
        @Override
        public void onSendBigRoomMessage(int errorCode, String roomID, String messageID) {
            AppLogger.getInstance().writeLog(this.getClass(), "sendBigRoomMsg errorCode %d  roomID %s  messageID  %s", errorCode, roomID, messageID);
        }
    });
}

5.2 接收消息

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

mZegoLiveRoom.setZegoIMCallback(new IZegoIMCallback() {
        @Override
        public void onUserUpdate(ZegoUserState[] zegoUserStates, int i) {
        }

        @Override
        public void onRecvRoomMessage(String s, ZegoRoomMessage[] zegoRoomMessages) {

        }

        @Override
        public void onRecvConversationMessage(String s, String s1, ZegoConversationMessage zegoConversationMessage) {

        }

        @Override
        public void onUpdateOnlineCount(String roomId, int onlineCount) {

        }
        //不可靠消息回调
        @Override
        public void onRecvBigRoomMessage(String roomID, ZegoBigRoomMessage[] zegoBigRoomMessages) {

        }
});

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

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

 @param roomID 房间 Id
 @param zegoBigRoomMessages 消息列表,包括消息内容,消息分类,消息类型,发送者等信息
 @discussion 调用 setZegoIMCallback 设置代理对象成功后,调用 ZegoLiveRoom.sendBigRoomMessage 发送消息,会触发此通知
 */
 @Override
 public void onRecvBigRoomMessage(String roomID, ZegoBigRoomMessage[] zegoBigRoomMessages) {
 }

Demo 代码示例如下:

@Override
public void onRecvBigRoomMessage(String roomID, ZegoBigRoomMessage[] zegoBigRoomMessages) {
    if (roomID == null && !roomID.equals(room_id)) {
        AppLogger.getInstance().writeLog(this.getClass(), "receive big room message, but roomId mismatch, abandon roomId:%s", roomID);
        return;
    }
    if (zegoBigRoomMessages.length == 0) {
        AppLogger.getInstance().writeLog(this.getClass(), "receive big room message, but messageList is 0 zegoBigRoomMessages:%d", zegoBigRoomMessages.length);
        return;
    }
    AppLogger.getInstance().writeLog(this.getClass(), "onRecvBigRoomMessage Im receive  roomID: %s", roomID);
    for (int i = 0; i < zegoBigRoomMessages.length; i++) {
        BigMessage bigMessage = new BigMessage();
        bigMessage.setContent(zegoBigRoomMessages[i].content);
        bigMessage.setFromUserName(zegoBigRoomMessages[i].fromUserName);
        AppLogger.getInstance().writeLog(this.getClass(), "onRecvBigRoomMessage Im receive  content: %s userName: %s", zegoBigRoomMessages[i].content, zegoBigRoomMessages[i].fromUserName);
        roomAdapter.addMsgToString(bigMessage);
    }
}

6 获取实时在线人数

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

/**
 收到在线人数更新
 @param roomId 房间 Id
 @param onlineCount 在线人数。默认为 30s 回调一次,开发者可联系即构技术支持,自定义回调频率。
 */
 public void onUpdateOnlineCount(String roomId, int onlineCount) {

 }

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

Demo 代码示例如下:

public void onUpdateOnlineCount(String roomId, int onlineCount) {
    if (roomId != null && roomId.equals(room_id)) {
        binding.currentQueueCount.setText(String.valueOf(onlineCount));
    }
}