有奖:语音产品征文挑战赛火热进行中> HOT
为了保障在线考试的流畅进行,建议您在控制台提交工单,提前报备考试人数、人员地域分布、使用的分辨率、码率等信息。

业务流程

本节汇总了一些常见的业务流程,帮助您更好地理解整个场景的实现流程。
登录与登出
创建销毁房间
音视频推流
音视频拉流
?
?
?
?
?
?
?
?
?
?
?
?

接入准备

步骤一:开通服务

在线考试场景通常需要依赖腾讯云 即时通信 IM实时音视频 TRTC 两项付费 PaaS 服务构建。
1. 首先,您需要登录 实时音视频 TRTC 控制台 创建应用,完成后在 即时通信 IM 控制台 即会自动创建一个对应的体验版 IM 应用。您可根据需要选择升级 TRTC 及 IM 应用版本,例如旗舰版可解锁更多增值功能服务。
?
?
?
说明:
建议创建两个应用分别用于测试环境和生产环境,首次开通 TRTC 服务可前往 试用中心 免费领取 10000 分钟试用时长包。
TRTC 包月套餐(入门版、基础版、尊享版、旗舰版)可以解锁不同的增值功能服务,详情可见 包月套餐说明
2. 创建应用完毕之后,您可以在应用管理-应用概览栏目看到该应用的基本信息,其中需要您保管好 SDKAppID、SDKSecretKey 便于后续的使用,同时应避免密钥泄露造成流量盗刷。
?
?
?

步骤二:导入SDK

分别把 TRTC SDKIM SDK 的 Windows 端 zip 压缩包下载到本地并解压,可在项目根目录下创建一个目录 thirdparty 用于存放所有 SDK,把 TRTC SDK 和 IM SDK 移动到 thirdparty 目录下备用。
1. 使用 QTCreator 集成
// *.pro
INCLUDEPATH += $$PWD/thirdparty/TRTC_SDK/CPlusPlus/Win64/include \\
$$PWD/thirdparty/TRTC_SDK/CPlusPlus/Win64/include/TRTC \\
$$PWD/thirdparty/IM_SDK/include
?
LIBS += -L'$$PWD/thirdparty/TRTC_SDK/CPlusPlus/Win64/lib' -lliteav \\
-L'$$PWD/thirdparty/IM_SDK/lib/Win64' -lImSDK
2. 使用 Visual Studio 集成
添加头文件目录,配置 - C/C++ - 常规 - 附加头文件目录。
$(SolutionDir)thirdparty/TRTC_SDK/CPlusPlus/Win64/include
$(SolutionDir)thirdparty/TRTC_SDK/CPlusPlus/Win64/include/TRTC
$(SolutionDir)thirdparty/IM_SDK/include
添加库文件目录,配置 - 链接 - 常规 - 附加库目录。
$(SolutionDir)thirdparty/TRTC_SDK/CPlusPlus/Win64/lib$(SolutionDir)thirdparty/IM_SDK/lib/Win64
引用库文件。
#pragma comment(lib,"liteav.lib")
#pragma comment(lib,"ImSDK.lib")
注意:
根据具体自己业务情况,如果需要集成 x86,则使用 Win32 目录下的头文件和库文件。
dll 动态库需要拷贝到 exe 所在目录。
x86 和 x64 的 lib 文件、dll 文件不能混用,需要保持一致。

接入过程

步骤一:生成鉴权凭证

UserSig 是腾讯云设计的一种安全保护签名,目的是为了阻止恶意攻击者盗用您的云服务使用权。腾讯云实时音视频(TRTC)、即时通信(IM)等服务都采用了该套安全保护机制,TRTC 在进房时鉴权,IM 在登录时鉴权。
调试跑通阶段:可以通过 客户端示例代码控制台 两种方法计算生成 UserSig,仅用于调试测试。
正式运行阶段:推荐安全等级更高的服务端计算 UserSig 方案,防止客户端被逆向破解泄露密钥。
具体实现流程如下:
1. 您的 App 在调用 SDK 的初始化函数之前,首先要向您的服务器请求 UserSig。
2. 您的服务器根据 SDKAppID 和 UserID 计算 UserSig。
3. 服务器将计算好的 UserSig 返回给您的 App。
4. 您的 App 将获得的 UserSig 通过特定 API 传递给 SDK。
5. SDK 将 SDKAppID + UserID + UserSig 提交给腾讯云服务器进行校验。
6. 腾讯云校验 UserSig,确认合法性。
7. 校验通过后,会向 IM SDK 提供即时通信服务、TRTC SDK 提供实时音视频服务。
?
?
?
说明:
调试跑通阶段的本地 UserSig 计算方式不推荐应用到线上环境,容易被逆向破解导致密钥泄露。
我们提供了多个语言版本(Java/GO/PHP/Nodejs/Python/C#/C++)的 UserSig 服务端计算源代码,详见 UserSig 计算源码

步骤二:初始化与监听

API 时序图

?
?
?
1. IM SDK 初始化与添加事件监听器
IMSDK 是函数式的回调方式,把它封装一层,封装成回调类,方便使用。
// IMWrapperCallback.h
class IMWrapperCallback
{
public:
virtual void OnLogin(int errCode, const char* errMsg) = 0;
virtual void OnLogout(int errCode, const char* errMsg) = 0;
virtual void OnError(int code, const char* errMsg) = 0;
virtual void OnCreateGroup(int errCode, const char* errMsg) = 0;
virtual void OnJoinGroup(int errCode, const char* errMsg) = 0;
virtual void OnRecvNewMsg(const char* msg) = 0;
// ……
};
?
// IMWrapper.h
class IMWrapper
{
public:
IMWrapper();
bool InitIM(const char* path);
bool UnInitIM();
void SetCallback(IMWrapperCallback* callback);
?
bool LoginIM(const char* id, const char* sig);
bool LogoutIM();
?
bool CreateGroup(const char* group_name, const char* group_type, const char* group_id);
bool JoinGroup(const char* group_id);
?
bool SendGroupTextMsg(const char* group_id, const char* text);
?
private:
IMWrapperCallback* m_callback;
std::string m_userId;
};
?
// IMWrapper.cpp
bool IMWrapper::InitIM(const char* path)
{
Json::Value json_value_init;
json_value_init[kTIMSdkConfigLogFilePath] = path;
int nRet = TIMInit(SDKAppID_IM, json_value_init.toStyledString().c_str());
if(nRet != TIM_SUCC){
return false;
}
?
TIMAddRecvNewMsgCallback([](const char* json_param, const void* user_data) {
if(user_data != nullptr){
IMWrapper* wrapper = (IMWrapper*)user_data;
if(wrapper->m_callback != nullptr){
wrapper->m_callback->OnRecvNewMsg(json_param);
}
}
}, this);
?
return true;
}
说明:
如果您的应用生命周期跟 SDK 生命周期一致,退出应用前可以不进行反初始化。若您只在进入特定界面后才初始化 SDK,退出界面后不再使用,可以对 SDK 进行反初始化。
2. TRTC SDK 创建实例与设置事件监听器
//OnlineExam.h
#include "ITRTCCloud.h"
class OnlineExam: public ITRTCCloudCallback
{
public:
OnlineExam();
~OnlineExam();
virtual void onWarning(TXLiteAVWarning warningCode, const char* warningMsg, void* extraInfo) override;
virtual void onError(TXLiteAVError errCode, const char *errMsg, void *extraInfo) override;
virtual void onEnterRoom(int result) override;
virtual void onExitRoom(int reason) override;
//……
}
?
OnlineExam::OnlineExam(){
getTRTCShareInstance()->addCallback(this);//创建单例模式,设置事件监听
}
?
OnlineExam::~OnlineExam(){
getTRTCShareInstance()->removeCallback(this);//取消事件监听
destroyTRTCShareInstance();//销毁实例
}
说明:
建议监听 SDK 事件通知,对一些常见错误进行日志打印和处理,详见 错误码表

步骤三:登录与登出

初始化 IM SDK 后,您需要调用 SDK 登录接口验证账号身份,获得的功能使用权限。因此在使用其他功能之前,请务必确保登录成功,否则可能导致功能异常或不可用。如您仅需使用 TRTC 音视频服务,可忽略此步骤。

API 时序图

?
?
?
// 登录:userID 可自定义,userSig 即步骤一生成获取
bool IMWrapper::LoginIM(const char* id, const char* sig){
m_userId = id;
int nRet = TIMLogin(id, sig, [](int32_t code, const char* desc, const char* json_param, const void* user_data) {
if(user_data != nullptr){
IMWrapper* wrapper = (IMWrapper*)user_data;
if(wrapper->m_callback != nullptr){
wrapper->m_callback->OnLogin(code, desc);
}
}
}, this);
?
if(nRet != TIM_SUCC){
return false;
}
return true;
}
?
// 登出
bool IMWrapper::LogoutIM(){
int nRet = TIMLogout([](int32_t code, const char* desc, const char* json_param, const void* user_data) {
if(user_data != nullptr){
IMWrapper* wrapper = (IMWrapper*)user_data;
if(wrapper->m_callback != nullptr){
wrapper->m_callback->OnLogout(code, desc);
}
}
}, this);
?
if(nRet != TIM_SUCC){
return false;
}
return true;
}
说明:
如果您的应用生命周期跟 IM SDK 生命周期一致,退出应用前可以不登出。若您只在进入特定界面后才使用 IM SDK,退出界面后不再使用,可以进行登出操作和对 IM SDK 进行反初始化。

步骤四:房间管理

API 时序图

?
?
?
1. 创建房间
主播(房主)开播时需要创建房间,这里的“房间”概念对应 IM 中的“群组”。本例展示客户端创建 IM 群组的方式,实际也可在服务端创建。
bool IMWrapper::CreateGroup(const char* name, const char* type, const char* id)
{
Json::Value param;
//群id
param[kTIMCreateGroupParamGroupId] = id;
//群类型
if (strcmp(type, "Public") == 0) {
param[kTIMCreateGroupParamGroupType] = kTIMGroup_Public;
}
else if(strcmp(type, "Work") == 0) {
param[kTIMCreateGroupParamGroupType] = kTIMGroup_Private;
}
else if(strcmp(type, "Meeting") == 0) {
param[kTIMCreateGroupParamGroupType] = kTIMGroup_ChatRoom;
}
else if(strcmp(type, "AVChatRoom") == 0) {
param[kTIMCreateGroupParamGroupType] = kTIMGroup_AVChatRoom;
//邀请进群方式
param[kTIMCreateGroupParamApproveOption] = kTIMGroupAddOpt_Forbid;//AnyJoin
}
//群名称
param[kTIMCreateGroupParamGroupName] = name;
std::string createParams = param.toStyledString();
int nRet = TIMGroupCreate(createParams.c_str(), [](int32_t code, const char* desc, const char* json_params, const void* user_data) {
if(user_data != nullptr){
IMWrapper* wrapper = (IMWrapper*)user_data;
if(wrapper->m_callback != nullptr){
wrapper->m_callback->OnCreateGroup(code, desc);
}
}
}, this);
?
if(nRet != TIM_SUCC){
return false;
}
return true;
}
说明:
在线考试场景创建 IM 群组建议选用会议群类型:kTIMGroup_ChatRoom。
TRTC 没有单独创建房间的步骤,进入一个不存在的房间,该房间即被自动创建出来。
2. 进入房间
加入 IM 群组
bool IMWrapper::JoinGroup(const char* id)
{
int nRet = TIMGroupJoin(id, "Want Join Group", [](int32_t code, const char* desc, const char* json_param, const void* user_data) {
if(user_data != nullptr){
IMWrapper* wrapper = (IMWrapper*)user_data;
if(wrapper->m_callback != nullptr){
wrapper->m_callback->OnJoinGroup(code, desc);
}
}
}, this);
?
if(nRet != TIM_SUCC){
return false;
}
return true;
}
进入 TRTC 房间
注意:
TRTC 房间号分为整型 roomId 和字符串类型 strRoomId,两种类型的房间不互通,选择其一即可,建议统一房间号类型。
UserSig 和 SdkAppId 建议在初始化 SDK 时即从业务后台生成并获取,其中 UserSig 只会在进房时校验,进房后过期不影响体验。
TRTC 进房场景可分为实时通话(AudioCall、VideoCall)和互动直播(Live、VoiceChatRoom)两大类。在线考试场景选用 VideoCall 模式。
TRTC 用户角色,只有在互动直播模式下才区分主播和观众,其中只有主播才有推流权限,观众如需推流要先切换至主播角色。在线考试选择的是VideoCall 模式,没有主播和观众的区分,所以 role 参数不需要填。
进房结果事件回调中,result > 0 代表加入房间所消耗的时间(单位毫秒);result < 0 其数值为进房失败的错误码,参照 错误码表
// 进入房间
void OnlineExam::EnterExamRoom(String roomId, String userId, String userName, int roleType, String userSig)
{
TRTCParams param;
param.sdkAppId = SDKAppID_TRTC;
param.strRoomId = roomID.c_str();
param.userId = userID.c_str();
param.userSig = userSig.c_str();
getTRTCShareInstance()->enterRoom(param, TRTCAppSceneVideoCall);
}
?
// 进房结果事件回调
void OnlineExam::onEnterRoom(int result)
{
if (result > 0) {
// 进房成功
} else {
// 进房失败
}
}
3. 退出房间
// 退出房间
void OnlineExam::ExitExamRoom()
{
getTRTCShareInstance()->exitRoom();
}
?
// 退房结果事件回调
void OnlineExam::onExitRoom(int reason)
{
// 0:主动调用 exitRoom 退出房间;1:被服务器踢出当前房间;2:当前房间整个被解散
}

步骤五:音频流管理

TRTC SDK 默认为自动订阅音频流逻辑,用户进房会自动开始播放远端用户的声音。如有手动订阅音频流的需求,请参考 设置订阅模式
1. 学生端推流
// 在进房成功后,学生开始推音频和视频流
void OnlineExam::onEnterRoom(int result){
if (result > 0) {
// 进房成功
getTRTCShareInstance()->startLocalAudio(TRTCAudioQualitySpeech);
getTRTCShareInstance()->startLocalPreview(hwndView);
} else {
// 进房失败
}
}
?
// 考试结束,停止推流
void OnlineExam::ExitExamRoom(){
getTRTCShareInstance()->stopLocalAudio();
getTRTCShareInstance()->stopLocalPreview();
getTRTCShareInstance()->exitRoom();
}
2. 监控端拉流
因为学生是提前进房推流了,所以监控老师可以在进房成功后立刻拉流;
在收到远端流事件回调后,则可以判断是否是当前分页的学生,如果是可以再次调用开始拉流不会有影响。
void OnlineExam::onUserVideoAvailable(const char* userId, bool available){
if(available){
if(userId == 当前页面的学生){
getTRTCShareInstance()->startRemoteView(userId, TRTCVideoStreamTypeBig, hwndView);
}
}
}

高级功能

考场划分

在在线考试场景中,一场考试可能有上千人,甚至上万人。而 TRTC 房间限制同一个房间同时推流人数为50人。所以需要对学生按人数进行虚拟考场分配。如果不是很严格的考试,一个学生使用一路视频流即可,预留一路视频给监控老师,一个虚拟考场,建议分配40-49个学生,给这些学生分配相同的 roomid,进房后只推流、不拉流。
相同考试、相同时间的学生,可以按小于50人/考场的方式,在业务后台提前分配好虚拟考场,为每个虚拟考场映射好 TRTC 的房间号 roomId。考试前,需要引导学生提前30分钟进入“考场”等待。进入考场后,引导学生完成摄像头、麦克风、扬声器的检测。可以提前5分钟开始推流,提前检测学生推流是否正常。
// 学生端(以 Web 为例)
?
/// 摄像头测试
TRTC.startLocalVideo({ view: 'camera-video', publish: false });
/// 麦克风测试
TRTC.startLocalAudio({ publish: false });
// 扬声器测试,准备一个可以播放的 mp3 url, <audio id="audio-player" src="mp3 url" controls></audio>
const audioPlayer = document.getElementById('audio-player');
if (!audioPlayer.paused) {
audioPlayer.pause();
}
audioPlayer.currentTime = 0;
?
/// 只推流,不拉流
const localStream = TRTC.createStream({ userId, audio: true, video: true });
try {
await localStream.initialize();
console.log('初始化本地流成功');
} catch (error) {
console.error('初始化本地流失败 ' + error);
}
try {
await client.publish(localStream);
console.log('本地流发布成功');
} catch (error) {
console.error('本地流发布失败 ' + error);
}
TRTC 提供了 设备检测 的 React 组件,推荐使用组件快速集成设备检测功能。

视频分页管理

监考老师端,不建议一次拉取所有学生的视频,这样需要很高的带宽。例如一路 360P 视频需要 400-600 kbps 带宽,40个学生,需要 16-24 mbps 带宽。如果同一个办公室有10个监控老师拉流,则需要160-240 mbps 带宽。所以,监控考试端可以做分页展示,例如一页展示9路视频,点击下一页,则停止拉流当前的9路视频,再拉流下一页的9路视频。
// 监考端(以Windows为例)
void OnlineExam::onClickNext(){
getTRTCShareInstance()->stopAllRemoteView();
getTRTCShareInstance()->muteAllRemoteAudio(true);
for(int i = 0; i < m_curUserList.size()){
getTRTCShareInstance()->startRemoteView(m_curUserList[i].userId, TRTCVideoStreamTypeBig, m_hwndList[i]);
}
}
注意:
因为拉流底层是异步操作,会有短暂延时,所以分页点击按钮的点击,要做个点击频率限制,限制在 1-2s 左右。如果太频繁点击,则容易引起底层响应不及时,导致出现未知的异常。

双路画面

普通的在线考试中,摄像头一般使用笔记本自带的摄像头,只能拍摄到学生的正面。对于比较严格的考试,这种满足不了要求,为了防止考试作弊,需要把学生的侧后面也需要拍摄到。
与单路流监考不一样的地方是,增加一路侧后台的视频监控,一般侧后方的监控使用手机端,方便移动和调整位置。需要注意的是,在位置摆放时,需要引导学生进行摆放,比如提供摆放的效果图。摆放完后,正面视频和侧面视频会采集一帧图片,发送到自己的业务后台做判断,自动判断学生摆放的位置是否合格。
例如,用户 id 为 1234,侧方位的视频 userId 可以加个自定义的后缀 1234_side。
// 学生端-正面视频(以Web端为例)
const trtc = TRTC.create();
await trtc.enterRoom({ roomId, sdkAppId, "1234", userSig });
?
const localStream = TRTC.createStream({ userId, audio: true, video: true });
try {
await localStream.initialize();
console.log('初始化本地流成功');
} catch (error) {
console.error('初始化本地流失败 ' + error);
}
try {
await client.publish(localStream);
console.log('本地流发布成功');
} catch (error) {
console.error('本地流发布失败 ' + error);
// 学生端-侧面视频(以Android端为例)
TRTCCloudDef.TRTCParams params = new TRTCCloudDef.TRTCParams();
params.sdkAppId = SDKAPPID;
params.userId = "1234_side";
params.roomId = roomId;
params.userSig = usersig;
?
mCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_VIDEOCALL);
?
mTRTCCloud.startLocalPreview(true, mTXCloudPreviewView);
mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);

音量检测

在更严格的在线考试场景中,不仅需要监控考生的正面和侧面视频,防止考生在考试过程中查阅资料,有时候也需要检测考生周围环境的声音。因为有可能考生会通过声音来作弊,例如播放提前预录制的资料、与他人交流、打电话等。
TRTC SDK 提供了音量检测回调功能。这个功能可以实时监测每个用户的声音大小,可以通过这个功能,帮助我们发现可疑的作弊行为。例如,一个考生的声音突然变大,可能是他正在播放预录制的答案,或者是跟其他人在交流。这时候,监控老师发现后可以介入,打开音频听一下这个考生的声音。

API 时序图

?
?
?
// 设置不自动拉流
void OnlineExam::enterExamRoom(){
getTRTCShareInstance()->setDefaultStreamRecvMode(false, false);
getTRTCShareInstance()->enterRoom(params);
getTRTCShareInstance()->enableAudioVolumeEvaluation();
}
?
// 拉当前分页用户的流
void OnlineExam::onEnterRoom(){
for(int i = 0; i < m_curUserList.size()){
getTRTCShareInstance()->muteRemoteAudio(m_curUserList[i].userId, false);
getTRTCShareInstance()->startRemoteView(m_curUserList[i].userId, TRTCVideoStreamTypeBig, m_hwndList[i]);
}
}
?
// 检测音量大小
void OnlineExam::onUserVoiceVolume(TRTCVolumeInfo* userVolumes, uint32_t userVolumesCount, uint32_t totalVolume){
int count = userVolumesCount;
TRTCVolumeInfo* remoteUserVolumes = userVolumes;
std::string userId;
int userVolume = 0;
for(int i = 0; i < count; i++){
userId = remoteUserVolumes->userId;
if(!userId.empty()){
userVolume = remoteUserVolumes->volume;
if(userVolume > m_maxVolume){
// 在界面上标识该学生
}
}
remoteUserVolumes++;
}
}
?
?
?
?
?


http://www.vxiaotou.com