最近几天笔试,发现好多的线上笔试都会有视频监考的功能,个人对其挺感兴趣,所以花了一天时间,研究了一下,写了一个小demo,下面说的有任何纰漏希望大家多多指正,下面开说了,大多数的视频监考就是通过浏览器,获取你电脑上的摄像头,来实现视频监考的功能的,所以相当于你的电脑是客户端,而公司那边是服务器,所以这大体上是一个客户端服务器模式,但是要通过浏览器来做客户端,通过浏览器来做服务端,这时候就要涉及到浏览器和浏览器之间的通信了,但是浏览器和浏览器之间直接通信比较困难,所以还是要用一个中间服务器来做转发,通过中间服务器做好连接后,那么在通信过程中,就是浏览器之间端到端的交互了,就不需要服务器的干预了。
要实现浏览器器端到端的通信,要用到两项技术一项是webSocket,一项是webRTC,websocket是浏览器和中间服务器做交互的手段,而webRTC是获取视频流和音频流的手段,首先一个浏览器A和一个浏览器B,要做交互,肯定得通过中间服务器C,所以浏览器A和中间服务器C会建立一个连接,而浏览器B和中间服务器同时也会建立一个连接,如果说浏览器A要向浏览器B发送一个字符串,那么A先要通过websocket把字符串发送到中间服务器,而中间服务器会找到,浏览器B对应的webSocket对象,通过这个对象把字符串发送给浏览器B,这就完成了浏览器A与B之间的交互,那么浏览器A与浏览器B之间要建立一个端到端的连接是需要通过这样的方式来实现的。
上面讲述了,两个浏览器之间的交互过程,对于websocket的知识大家可以上网看看,上面只是基本原理。下面看看webRTC是怎么工作的,在HTML5中,我们可以通过js代码获取到我们本地的视频流,但是我们本地的视频流不是给我们自己看的,是给监考的公司看的,所以我们需要在浏览器之间建立连接,然后把视频流发送过去。这个连接就是webRTC的核心东西了,在浏览器中可以用js代码新建一个WEBRTC的连接,var pc = new webkitRTCPeerConnection(iceServer);
这条语句就是建立一个连接,也就是代表这个浏览器,那么在另一个浏览器中,我们同样可以建立这样一个连接,但是这连个连接是独立的,他们像是两根管道,这时候需要我们把它链接起来。
这时A要监考B具体过程如下:
首先交换浏览器之间的描述信息,像是ip,端口,视频信息,等等的一些信息,这统称为描述信息,那么两个浏览器都有描述信息,首先浏览器A主动向B发起连接,A首先把自己的描述信息(localDescription)加入自己的连接,然后A向B发送一个offer包,这个发送是通过websocket来发送的,发送到服务器,然后服务器转发到B,B收到以后通过offer包可以获得B的描述信息,B把收到的远程描述信息(remoteDescription)加入自己的连接,然后再把自己的本地描述信息,放入自己的连接,再向A发送一个answer包,A接收到answer包以后,可以获得B的描述信息了,这时把B的描述信息,加入到自己的连接中,这样两个浏览器都包含有对方的描述信息,这样就基本完成了两个浏览器之间的连接,接下来就是其他信息的以下交互,主要是为了不仅仅能够在局域网内建立连接,在这些交互做完以后,那么B把自己的视频流加到连接里,这样在A就可以获取视频流了,然后整个通信过程就不需要webSocket的参与了,只是在B下线或者是A下线的时候,或通知中间服务器删除对应的连接。
下面是我的主要代码:
服务器java代码,处理浏览器的登录退出,以及消息的转发:
public class Admin extends StreamInbound{ @Override protected void onBinaryData(InputStream arg0) throws IOException { // TODO Auto-generated method stub } @Override protected void onTextData(Reader ir) throws IOException { // TODO Auto-generated method stub BufferedReader br=new BufferedReader(ir); //char[] buf=new char[2000]; //char[] sbuf=new char[6000]; //StringBuilder sb=new StringBuilder(); /*int n=0; int index=0; while((n=ir.read(buf))>0){ System.arraycopy(buf, 0, sbuf, index, n); Arrays.fill(buf, '0'); index+=n; }*/ StringBuilder sb=new StringBuilder(); String temp=null; while((temp=br.readLine())!=null){ sb.append(temp); } //String[] test=sb.toString().split("\r\n"); //StringBuilder sb2=new StringBuilder(); //for(int i=0;i<test.length;i++){ // sb2.append(test[i]); //} //System.out.println(test.length); //System.out.println(sb.toString()); //System.out.println("转发给了客户端"); //转发给客户端 //ConnectionPool.getAdmin().getWsOutbound().writeTextMessage(CharBuffer.wrap(sb.toString().toCharArray())); Map<Long,StreamInbound> map=ConnectionPool.getClientMap(); for(Map.Entry<Long, StreamInbound> entry:map.entrySet()){ System.out.println("fawegawergawrehgeahtresathresathreshtrehsr"); entry.getValue().getWsOutbound().writeTextMessage(CharBuffer.wrap(sb.toString())); } } @Override protected void onClose(int status) { // TODO Auto-generated method stub //super.onClose(status); System.out.println(status); if(ConnectionPool.logout()){ System.out.println("服务端出去了"); }else{ System.out.println("当前没有服务端不能登出"); } } @Override protected void onOpen(WsOutbound outbound) { // TODO Auto-generated method stub //super.onOpen(outbound); if(ConnectionPool.login(this)){ System.out.println("服务端进来了"); }else{ System.out.println("当前已有管理员"); } } }
public class Server extends WebSocketServlet{ @Override protected StreamInbound createWebSocketInbound(String arg0, HttpServletRequest req) { // TODO Auto-generated method stub String info=req.getParameter("info").trim(); if(info.equals("client")){ return new Mession(System.currentTimeMillis()); }else{ return new Admin(); } } }
public class Mession extends StreamInbound{ private long time; public Mession(long time){ this.time=time; } @Override protected void onBinaryData(InputStream arg0) throws IOException { System.out.println("get"); } @Override protected void onTextData(Reader ir) throws IOException { /*char[] buf=new char[2000]; StringBuilder sb=new StringBuilder(); int n=0; while((n=ir.read(buf))>0){ sb.append(buf,0,n); Arrays.fill(buf, ' '); }*/ //char[] buf=new char[2000]; //char[] sbuf=new char[6000]; //StringBuilder sb=new StringBuilder(); /*int n=0; int index=0; while((n=ir.read(buf))>0){ System.arraycopy(buf, 0, sbuf, index, n); Arrays.fill(buf, '0'); index+=n; }*/ BufferedReader br=new BufferedReader(ir); String temp=null; StringBuilder sb=new StringBuilder(); while((temp=br.readLine())!=null){ sb.append(temp); } //String[] test=sb.toString().split("\r\n"); //StringBuilder sb2=new StringBuilder(); //System.out.println(sb.toString().leng); //for(int i=0;i<test.length;i++){ // sb2.append(test[i]); //} //System.out.println(sb.toString()); //System.out.println("转发给了客户端"); //String message=sb.toString(); //转发给服务器 ConnectionPool.getAdmin().getWsOutbound().writeTextMessage(CharBuffer.wrap(sb.toString())); } @Override protected void onClose(int status) { // TODO Auto-generated method stub //super.onClose(status); //移除连接 ConnectionPool.removeConnection(time); System.out.println("connection has closed!!!"); } @Override protected void onOpen(WsOutbound outbound) { // TODO Auto-generated method stub //super.onOpen(outbound); //把连接放入池中 ConnectionPool.addConnection(time, this); try { System.out.println("向客户端发送了数据"); outbound.writeTextMessage(CharBuffer.wrap("hello".toCharArray())); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("connection open!!!"); } }
public class ConnectionPool { private final static Map<Long,StreamInbound> connections=new HashMap<Long,StreamInbound>(); private static StreamInbound admin=null; public static Map<Long,StreamInbound> getClientMap(){ return connections; } public static StreamInbound getAdmin(){ return admin; } public static boolean login(StreamInbound admin){ if(ConnectionPool.admin==null){ ConnectionPool.admin=admin; System.out.println(ConnectionPool.admin.getReadTimeout()); return true; }else{ return false; } } public static boolean logout(){ if(ConnectionPool.admin==null){ return false; }else{ ConnectionPool.admin=null; return true; } } public static void addConnection(long time,StreamInbound connection){ connections.put(time, connection); System.out.println("加入连接"); } public static void removeConnection(long time){ connections.remove(time); System.out.println("移动出连接"); } }
下面是前段js代码和html代码:
server.html
<html> <head> <title>server</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> <script type="text/javascript"> function onOpen(event) { document.getElementById('messages').innerHTML = 'Connection established'; } function onError(event) { document.getElementById('messages').innerHTML += '<br/>'+event.data; } function start() { var webSocket =new WebSocket("ws://localhost:8888/WS/wstest?info=admin"); webSocket.onopen = function(event) { onOpen(event); }; webSocket.onerror = function(event) { onError(event); }; webSocket.onclose=function(event){ //document.getElementById('messages').innerHTML //+= '<br/>'+str(event.data); alert(event.data); } var iceServer = { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }; // 创建PeerConnection实例 (参数为null则没有iceserver,即使没有stunserver和turnserver,仍可在局域网下通讯) var pc = new webkitRTCPeerConnection(iceServer); // 发送ICE候选到其他客户端 // 如果检测到媒体流连接到本地,将其绑定到一个video标签上输出 pc.onaddstream = function(event){ //alert("检测到流"); document.getElementById('remoteVideo').src = webkitURL.createObjectURL(event.stream); }; // 发送offer和answer的函数,发送本地session描述 /*var sendOfferFn = function(desc){ alert(desc.sdp) //pc.setRemoteDescription(desc); // pc.setLocalDescription(desc); webSocket.send(JSON.stringify({ "event": "_offer", "data": { "sdp": desc } })); };*/ pc.onicecandidate = function(event){ if (event.candidate !== null) { webSocket.send(JSON.stringify({ "event": "_ice_candidate", "data": { "candidate": event.candidate } })); } }; var sendAnswerFn = function(desc){ pc.setLocalDescription(desc); webSocket.send(JSON.stringify({ "event": "_answer", "data": { "sdp": desc } })); }; // 获取本地音频和视频流 /* navigator.webkitGetUserMedia({ "audio": true, "video": true }, function(stream){ //绑定本地媒体流到video标签用于输出 // document.getElementById('localVideo').src = URL.createObjectURL(stream); //向PeerConnection中加入需要发送的流 pc.addStream(stream); //如果是发起方则发送一个offer信令 pc.createOffer(sendOfferFn, function (error) { console.log('Failure callback: ' + error); }); }, function(error){ //处理媒体流创建失败错误 console.log('getUserMedia error: ' + error); }); */ //处理到来的信令 webSocket.onmessage = function(event){ //alert(event.data) //document.getElementById('messages').innerHTML //+= '<br/>'+event.data; var jsonstr="'"+event.data+"'" var json = JSON.parse(event.data); console.log('onmessage: ', json); //如果是一个ICE的候选,则将其加入到PeerConnection中,否则设定对方的session描述为传递过来的描述 if( json.event == "_ice_candidate" ){ //alert("收到候选"); pc.addIceCandidate(new RTCIceCandidate(json.data.candidate)); } else { if(json.event == "_offer") { pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp),function(){ //pc.setRemoteDescription(null,function(){ pc.createAnswer(sendAnswerFn, function (error) { alert(error); console.log('Failure callback: ' + error); }); },function(){ alert("error"); pc.createAnswer(sendAnswerFn, function (error) { alert("error"); console.log('Failure callback: ' + error); }); }); } // pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp,function(){ // alert(1); //})); // if (isRTCPeerConnection) // pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp)); // else // pc.setRemoteDescription(pc.SDP_OFFER, // new SessionDescription(json.data.sdp.sdp)); //pc.setRemoteDescription(new RTCSessionDescription(pc.SDP_OFFER,json.data.sdp)); //pc.SDP_OFFER //pc.setRemoteDescription(pc.SDP_OFFER,new SessionDescription(json.data.sdp.sdp)); // 如果是一个offer,那么需要回复一个answer /* if(json.event == "_offer") { alert(json.event) pc.createAnswer(sendAnswerFn, function (error) { document.getElementById('messages').innerHTML += '<br/>'+error; console.log('Failure callback: ' + error); }); }*/ } }; } </script> </head> <body> <input type="submit" value="Adminlogin" onclick="start()"> <div id="messages"> </div> <video id="remoteVideo" autoplay="autoplay"></video> <video id="localVideo" autoplay></video> </body> </html>
contronlled.html
<html> <head> <title>client</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> <script type="text/javascript"> function onOpen(event) { document.getElementById('messages').innerHTML = 'Connection established'; } function onError(event) { document.getElementById('messages').innerHTML += '<br/>'+event.data; } function start() { var webSocket =new WebSocket("ws://localhost:8888/WS/wstest?info=client"); webSocket.onopen = function(event) { onOpen(event); }; webSocket.onerror = function(event) { onError(event); }; webSocket.onclose=function(event){ //document.getElementById('messages').innerHTML //+= '<br/>'+str(event.data); alert(event.data) } var iceServer = { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }; // 创建PeerConnection实例 (参数为null则没有iceserver,即使没有stunserver和turnserver,仍可在局域网下通讯) var pc = new webkitRTCPeerConnection(iceServer); // 发送offer和answer的函数,发送本地session描述 var sendOfferFn = function(desc){ pc.setLocalDescription(desc); webSocket.send(JSON.stringify({ "event": "_offer", "data": { "sdp": desc } })); }; pc.onicecandidate = function(event){ if (event.candidate !== null) { webSocket.send(JSON.stringify({ "event": "_ice_candidate", "data": { "candidate": event.candidate } })); } }; // 获取本地音频和视频流 navigator.webkitGetUserMedia({ "audio": true, "video": true }, function(stream){ //绑定本地媒体流到video标签用于输出 //document.getElementById('localVideo').src = URL.createObjectURL(stream); //向PeerConnection中加入需要发送的流 pc.addStream(stream); //如果是发起方则发送一个offer信令 pc.createOffer(sendOfferFn, function (error) { console.log('Failure callback: ' + error); }); }, function(error){ //处理媒体流创建失败错误 console.log('getUserMedia error: ' + error); }); //处理到来的信令 webSocket.onmessage = function(event){ //alert(event.data); //document.getElementById('messages').innerHTML // += '<br/>'+event.data; var jsonstr="'"+event.data+"'"; var json = JSON.parse(event.data); console.log('onmessage: ', json); //如果是一个ICE的候选,则将其加入到PeerConnection中,否则设定对方的session描述为传递过来的描述 if( json.event == "_ice_candidate" ){ pc.addIceCandidate(new RTCIceCandidate(json.data.candidate)); } else { //接收到确认符号 if(json.event == "_answer"){ pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp),function(){},function(){}); // 发送ICE候选到其他客户端 } } }; } </script> </head> <body> <input type="submit" value="clientLogin" onclick="start()"> <div id="messages"> </div> <video id="remoteVideo" autoplay></video> <video id="localVideo" autoplay></video> </body> </html>
上面就是主要的代码了:具体运行流程把这些代码部署到tomcat上,打开监控端浏览器中输入http://localhost:8888/WS/server.html,打开被监控端http://localhost:8888/WS/controlled.html,然后点击监控页面中的AdminLogin按键,先让监控端注册到中间服务器上面,然后点击被监控端的clientLogin按键,然后后浏览器会询问时候开启摄像头,点击开启,等待1到3秒在监控端就可以出现视频画面了。
下面是效果演示图:
版权声明:本文为博主原创文章,未经博主允许不得转载。