应用场景
在云考试中,为防止作弊行为的发生,会在考生端部署音视频监控系统,当然还有考官方监控墙系统。在实际应用中,考生一方至少包括两路直播流:
(1)前置摄像头:答题的设备要求使用笔记本电脑,使用支持H5的WEB浏览器,并授权打开前置摄像头,产生一路直播流,以监控考生正面活体人像的行为,并进行录像留证。
(2)后方摄像:使用其它可用的摄像设备,如手机、平板等,打开摄像头,产生一路直播流,以监控考生背面、笔记本及前方音视频情况,并进行录像留证。
如果不考虑各种成本,还可以使用屏幕共享功能,以录制笔记本电脑屏幕上的一切操作行为。
腾讯云实时音视频
我们的云考试监控部分的开发采用基于腾讯云WebRTC的技术实现,其产品以多人音视频通话和低延时互动直播两大场景,通过开放API,帮助开发者快速搭建低成本、低延时、高品质的音视频互动解决方案。
产品架构
下图是我们基于腾讯云产品架构图的部分采用和实现方案:
关于RoomID
当创建直播流的时候,我们可以简单的理解为,首先需要创建一个房间(音视频聊天室),该房间就应该分配一个唯一的房号,这房号就是RoomID。
RoomID是一组10位数字的字符串值,但在实际应用中,第一位不要为0,否则腾讯会自动转数值,而变成9位数字,这个位数是不合法的。但这种情况在微信小程序的RTC版本里不会出现。
另外,数值的范围最好在1000000000-5000000000这间,否则也有可能报错误,以上都是曾经踩到的一些坑点,在此分享。
需求示例
考试产品可以提供二维码或接口接入的形式,访问首页如下图演示:
核对信息无误后,创建RoomID和用户名,用户名按实际业务需要创建,比如前置为 roomid_1,后置为 roomid_2,从名称上可以区分前后直播流即可。 进入考试如下图:
创建了前置摄像直播流,提示用户用手机微信扫描以打开后方摄像头功能,如果成功则可以进行答题,如下图:
现在的需求是,如果考生在考试过程中断开其中一路或全部断开则提示其重新连接摄像头。我们采用了腾讯云给出的一种解决方案,利用其API定时查询对应的直播流是否存在,如果不存在则进行提示,以下图为例 :
关键代码
API实现
//查询在线直播流,参数1:部分或全部流名称,页码
//方法返回LVBStream类对象的ArrayList集合
public ArrayList SearchOnlineStream(string partname,string PageNum)
{
ArrayList data = new ArrayList();
//请求地址
string settingUrl = "https://live.tencentcloudapi.com/";
//应用ID和应用key
string secretId = 应用ID;
string secretKey = 应用key;
//时间戳
string timesTamp = GetTimeStamp();
//Nonce
var nonce = new Random().Next(10000, 99999);
//拼接参数 abcdefghijklmnopq
string paramsStr = string.Format(@"Action=DescribeLiveStreamOnlineList&Nonce={0}&PageNum=1&PageSize=100&Region=ap-guangzhou&SecretId={1}&SignatureMethod=HmacSHA1&Timestamp={2}&Version=2018-08-01",
nonce, secretId, timesTamp);
//生成签名参数
// string requestText = settingUrl + "?" + paramsStr;
string requestText = "POST" + settingUrl.Replace("https://", "") + "?" + paramsStr;
//获得请求签名
string signText = GetHmacSha1Sign(secretKey, requestText);
//这里一定要进行URL编码,不然调用API会报错
signText = HttpUtility.UrlEncode(signText, Encoding.UTF8);
// string text = HttpUtility.UrlEncode(SearchText, Encoding.UTF8);
paramsStr = string.Format(@"Action=DescribeLiveStreamOnlineList&Nonce={0}&PageNum=1&PageSize=100&Region=ap-guangzhou&SecretId={1}&Signature={2}&SignatureMethod=HmacSHA1&Timestamp={3}&Version=2018-08-01",
nonce, secretId, signText, timesTamp);
//请求腾讯API,返回身份证信息
string resultStr = SendRequest(settingUrl, paramsStr);
if (resultStr.IndexOf("TotalNum") != -1)
{
Newtonsoft.Json.Linq.JObject jsonObj = Newtonsoft.Json.Linq.JObject.Parse(resultStr);
//"Response":{"MediaInfoSet":[],"TotalCount":0, "RequestId":"85f181fc-d76f-42bb-82d8-7ac4d5ff432a"}}
int total = jsonObj["Response"]["OnlineInfo"].Count();
int totalpage =int.Parse(jsonObj["Response"]["TotalPage"].ToString());
for (int i = 0; i < total; i++)
{
LVBStream ls = new LVBStream();
try
{
ls.StreamName = jsonObj["Response"]["OnlineInfo"][i]["StreamName"].ToString();
ls.AppName = jsonObj["Response"]["OnlineInfo"][i]["AppName"].ToString();
ls.DomainName = jsonObj["Response"]["OnlineInfo"][i]["DomainName"].ToString();
if (partname != "")
{
if (ls.StreamName.IndexOf(partname) != -1)
{
data.Add(ls);
}
}
else
{
data.Add(ls);
}
}
catch (Exception e) { }
}
if (totalpage > 1 && int.Parse(PageNum) == 1)
{
for (int i = int.Parse(PageNum) + 1; i <= totalpage; i++) {
ArrayList data2 = new ArrayList();
data2 = SearchOnlineStream(partname, i.ToString());
foreach (CoWeixin.wxLiveManager.TencentCloud.LVBStream lvb in data2)
{
data.Add(lvb);
}
}
}
return data;
}
return data;
} //search media
public class LVBStream
{
public string StreamName = "";
public string AppName = "";
public string DomainName = "";
public LVBStream()
{
}
}
public static string GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
/// <summary>
/// HMAC-SHA1加密返回签名
/// </summary>
/// <param name="secret">密钥</param>
/// <param name="strOrgData">源文</param>
/// <returns></returns>
public static string GetHmacSha1Sign(string secret, string strOrgData)
{
var hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(secret));
var dataBuffer = Encoding.UTF8.GetBytes(strOrgData);
var hashBytes = hmacsha1.ComputeHash(dataBuffer);
return Convert.ToBase64String(hashBytes);
}
public static string SendRequest(string url, string completeUrl)
{
ServicePointManager.SecurityProtocol = (SecurityProtocolType)192 | (SecurityProtocolType)768 | (SecurityProtocolType)3072;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ProtocolVersion = HttpVersion.Version10;
request.Host = url.Replace("https://", "").Replace("/", "");
byte[] data = Encoding.UTF8.GetBytes(completeUrl);
request.ContentLength = data.Length;
Stream newStream = request.GetRequestStream();
newStream.Write(data, 0, data.Length);
newStream.Close();
HttpWebResponse response = null;
string content;
try
{
response = (HttpWebResponse)request.GetResponse();
StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
content = reader.ReadToEnd();
}
catch (WebException e)
{
response = (HttpWebResponse)e.Response;
using (Stream errData = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(errData))
{
content = reader.ReadToEnd();
}
}
}
return content;
}</code></pre></div></div><h5 id="afb52" name="JS%E8%B0%83%E7%94%A8%E7%9A%84%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%96%B9%E6%B3%95">JS调用的服务端方法</h5><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0"> [WebMethod]
//统计在线流情况,传递RoomID房间号
public static string onlineLiveCount(string roomid)
{
string tip = "";
ArrayList rv = SearchOnlineStream(roomid);
bool isfront = false;
bool isback = false;
foreach (LVBStream lvb in rv)
{
if (lvb.StreamName.IndexOf("_1_main")!=-1) //根据自己的命名规则判断
{
isfront = true;
}
if (lvb.StreamName.IndexOf("_2_main") != -1) //根据自己的命名规则判断
{
isback = true;
}
}
if (rv.Count < 2)
{
if (isfront == false)
{
tip += "未监控到前方摄像头。<br>";
}
if (isfront==true&&isback == false)
{
tip += "未监控到后方摄像头。<br>";
}
}
return tip;
}</code></pre></div></div><h4 id="2mo1r" name="%E5%B0%8F%E7%BB%93">小结</h4><p>以上提供的代码仅供参考,在实际的应用中,我们要编写符合自己业务的逻辑,还要考虑实际的运营成本。有关腾讯RTC产品的价格情况,可以访问:https://cloud.tencent.com/document/product/647/17157</p><p>以上就是自己的一些分享,时间仓促,不妥之处还请大家批评指正!</p><h5 id="18gj6" name=""> </h5><h5 id="c4cos" name=""> </h5>