场景概述
在线考试是指通过计算机在网络上进行的一种考试形式,脱离纸质媒体,也可以描述为通过在线媒体进行的考试。
- 在线考试涵盖各种无纸化考试系统,如全国计算机等级考试、大学英语考试、研究生入学考试、职业资格考试、金融考试、工程考试、外贸考试、大专升本科考试、公务员考试和企业笔试。考试完成后,可以进行自动评分等操作,以获取最终成绩报告,可发送到电子邮件以供保存或在线打印。
- 传统考试涉及从出题、编写和打印试卷,到分发试卷、提交答案、收集和批改试卷、公布结果及统计分析考试结果等整个过程的手动参与。这个过程漫长、劳动密集,容易出错,并且需要足够的保密工作,使整体学习和考试成本较高。在线考试学习系统可以完全实现无纸化、网络化和自动化的计算机在线学习与考试,这对单位的信息化建设具有深远的现实意义和实践价值。
需要注意的是,“在线考试”和“离线计算机考试”并不是同一种商业模式。离线计算机考试是指在离线IDC中通过在线系统进行考试,与传统考试的区别仅在于试题是否在纸质上完成。
实施方案
在线考试场景的整体参考业务流程如图所示。在线考试系统的用户通常包括考生、监考人员、考试管理员和出题人(阅卷人)。
- 考生:考生需要登录考生账户进入考场,激活摄像头和屏幕共享,并在监控条件下完成并提交试卷。
- 监考人员:监考人员需要登录监考账户,能够查看所有参加考试的考生的摄像头画面,以判断是否有作弊行为。系统本身也具备一定的自动监控能力,例如监控考试应用切换的次数和持续时间。在某些场景中,他们还需要回答考生可能提出的问题,在允许的范围内。
- 考试管理员:他们需要创建考场,将试题上传到相应的考场,并控制考场的访问权限。通常,一名监考人员也可以担任此角色。
- 教师:他们的主要任务是在考试前出题,并在考试后手动批改试卷(如果必要的话)。
在线考试业务流程图:

业务流程
本文档总结了一些常见的业务流程,以帮助您更好地理解整个场景的实施流程。
- 登录和登出

- 创建/销毁房间
- 音视频流传输
- 音视频拉流
应用产品
基本集成指南
为了确保在线考试的顺利进行,建议您提前通过控制台提交工单,注册考试人数、人员地区分布、分辨率、比特率等信息。
激活服务
语音聊天室场景通常需要依赖云平台的两个付费PaaS服务,聊天和实时通信(TRTC) 进行构建。互动白板是可选的,可以选择或自托管。
1. 您需要登录控制台 创建应用程序。选择RTC引擎和聊天服务。这时,SDKAppID会自动在控制台中创建。两者的账户和认证系统可以重用。随后,您可以根据需要选择升级RTC或聊天应用版本。例如,高级版本可以解锁更多增值功能和服务。
注意:
- 建议为测试和生产环境分别创建两个独立的应用程序。每个账户(UIN)在一年内提供10,000分钟的免费使用时间。
- TRTC月度套餐分为试用版、基础版和专业版,可以解锁不同的增值功能和服务。有关详细信息,请参见版本功能和月度套餐说明。
2. 一旦应用程序创建成功,您可以在应用管理 - 应用概览部分找到其基本信息。务必安全存储SDKAppID和SDKSecretKey以备后用,并避免泄露,以防止未授权的流量使用。
导入SDK
将TRTC SDK和聊天SDK的zip压缩包下载到本地机器并解压。您可以在项目的根目录下创建一个名为thirdparty的目录来存储所有SDK。将TRTC SDK和聊天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集成
- 要在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)和聊天等云服务都采用这种安全保护机制。TRTC在进入房间时进行身份验证,而聊天在登录时进行身份验证。
- 调试和测试阶段:UserSig可以通过客户端UserSig生成和TRTC控制台UserSig生成生成,仅用于调试和测试。
- 生产阶段:建议使用服务器端UserSig生成,安全级别更高,有助于防止客户端被反编译和逆向,从而避免密钥泄露的风险。
具体的实现过程如下:
1. 在您的应用程序调用SDK的初始化API之前,请向您的服务器请求UserSig。
2. 您的服务器将根据SDKAppID和UserID生成UserSig。
3. 服务器将UserSig返回给您的应用程序。
4. 您的应用程序通过特定API将UserSig发送到SDK。
5. SDK将SDKAppID、UserID和UserSig提交给云服务器进行验证。
6. 云平台验证UserSig的有效性。
7. 如果UserSig有效,将为Chat SDK和TRTC SDK提供服务。
注意:
- 在调试和测试阶段本地生成UserSig的方法不推荐用于在线环境,因为它可能很容易被反编译和逆向,导致密钥泄露。
- 我们提供多种语言(Java/GO/PHP/Nodejs/Python/C#/C++)的UserSig生成源代码。有关详细信息,请参见UserSig生成源代码。
初始化和监听
API序列图
1.聊天SDK初始化和添加事件监听器
聊天SDK使用功能回调方法。可以封装成回调类以方便使用。
// 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事件通知。对于一些常见错误进行日志打印和处理。有关详细信息,请参见错误代码表。
登录和登出
在初始化聊天SDK后,您需要调用SDK登录API来验证您的账户身份并拥有使用功能的权限。在使用任何其他功能之前,请确保您已成功登录,否则可能会遇到功能故障或不可用。如果您只需要使用TRTC的音视频服务,则可以跳过此步骤。
API序列图
// 登录:userID可以自定义,userSig在步骤1中获得。
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;
}
注意:如果您的应用程序生命周期与聊天SDK一致,则在退出应用程序之前无需登出。但是,如果您仅在进入特定接口后使用聊天SDK,之后不再使用它,则可以登出并取消初始化聊天SDK。
房间管理
API序列图
- 创建房间
当主播(房主)开始直播时,需要创建一个房间。这里的“房间”概念对应于聊天中的“群组”。这个例子展示了如何在客户端创建聊天组,但也可以在服务器上创建组。
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;// 禁止加入
}
// 群组名称
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;
}
注意:
- 在在线考试场景中,建议使用会议组类型:kTIMGroup_ChatRoom。
- TRTC没有单独的创建房间步骤。当您进入一个不存在的房间时,房间会自动创建。
- 进入房间
加入聊天组
bool IMWrapper::JoinGroup(const char* id)
{
int nRet = TIMGroupJoin(id, "想加入群组", [](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房间ID分为整数类型roomId和字符串类型strRoomId。这两种类型的房间是互不连接的,只需选择一种。建议统一房间ID类型。
- 建议在初始化SDK时从业务后端生成和获取UserSig,UserSig仅在进入房间时进行验证。进入后的过期UserSig不会影响体验。
- TRTC房间入口场景可分为实时通话(AudioCall、VideoCall)和互动直播(Live、VoiceChatRoom)两大类。在线考试场景选择视频通话模式。
- TRTC用户角色仅在互动直播模式下区分主播和观众。在此模式下,只有主播有权限进行推流,观众需要切换到主播角色才能进行推流。由于在线考试场景使用视频通话模式,因此无需角色参数。
- 在进入房间的事件回调中,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 {
// 进入房间失败。
}
}
- 退出房间
// 退出房间。
void OnlineExam::ExitExamRoom()
{
getTRTCShareInstance()->exitRoom();
}
// 退出房间事件回调。
void OnlineExam::onExitRoom(int reason)
{
// 0:主动调用exitRoom退出房间;1:被服务器从当前房间移除;2:当前房间被解散。
}
音频流管理
TRTC SDK默认自动订阅音频流,这意味着当用户加入房间时,远程用户的音频会自动播放。如果需要手动订阅音频流,请参见设置订阅模式。
- 学生端流媒体
// 成功进入房间后,学生开始流媒体音视频。
void OnlineExam::onEnterRoom(int result){
if (result > 0) {
// 成功进入房间。
getTRTCShareInstance()->startLocalAudio(TRTCAudioQualitySpeech);
getTRTCShareInstance()->startLocalPreview(hwndView);
} else {
// 进入房间失败。
}
}
// 考试结束,停止流媒体。
void OnlineExam::ExitExamRoom(){
getTRTCShareInstance()->stopLocalAudio();
getTRTCShareInstance()->stopLocalPreview();
getTRTCShareInstance()->exitRoom();
}
- 监考端流媒体拉取
由于学生已经提前进入房间并开始流媒体,监考人员可以在成功进入房间后立即拉取流媒体。在接收到远程流事件回调后,可以确定当前页面的学生。如果是,则可以重新调用以开始拉流,不会产生影响。
void OnlineExam::onUserVideoAvailable(const char* userId, bool available){
if(available){
if(userId == 当前页面的学生){
getTRTCShareInstance()->startRemoteView(userId, TRTCVideoStreamTypeBig, hwndView);
}
}
}
高级功能
考试室分区管理
在在线考试场景中,可能会有数千或甚至数万名学生参加一次考试。然而,TRTC房间对每个房间的同时流媒体限制为50个。因此,需要根据学生数量将学生分成虚拟考试室。如果考试不太严格,学生可以只使用一条视频流,留一条流给监考人员。建议每个虚拟考试室分配40-49名学生到同一个roomId。在进入房间后,学生只推流而不拉流。
对于同时参加同一考试的学生,可以在业务后端将他们划分到每个虚拟考试室,每个房间应映射到一个TRTC房间ID(roomId)。在考试前,应指导学生提前30分钟进入考场并等待。一旦进入房间,应指导学生完成摄像头、麦克风和扬声器的测试。流媒体可以提前5分钟开始,以便尽早发现学生流媒体存在的问题。
// 学生端(以Web为例)。
/// 摄像头测试。
TRTC.startLocalVideo({ view: 'camera-video', publish: false });
/// 麦克风测试。
TRTC.startLocalAudio({ publish: false });
// 扬声器测试,请准备一个可播放的mp3网址,<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(); i++){
getTRTCShareInstance()->startRemoteView(m_curUserList[i].userId, TRTCVideoStreamTypeBig, m_hwndList[i]);
}
}
注意:由于底层流媒体是异步操作,因此可能会有短暂的延迟,因此需要对分页按钮实施点击频率限制,限制在1-2秒左右。如果点击过于频繁,可能会导致底层系统响应延迟,从而导致意外异常。
分屏显示
在普通在线考试中,摄像头通常使用笔记本电脑的内置摄像头,仅能捕捉学生的正面。对于更严格的考试,这并不能满足需求。为了防止考试作弊,有必要同时捕捉学生的后侧方向。
与单流监控不同,增加了额外的侧背景视频监控,通常通过手机进行,以便于移动和调整位置。重要的是指导学生如何定位他们的设置,例如提供设置的示意图。在定位后,将捕捉到正面和侧面视频的帧图像,并发送到业务后端进行判断,自动确定学生的位置是否合格。
例如,如果用户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(); i++){
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++;
}
}
立即订购
点击这里 快速进入购买页面进行订单。