websocket+webrtc+tomcat 实现视频监考功能


         最近几天笔试,发现好多的线上笔试都会有视频监考的功能,个人对其挺感兴趣,所以花了一天时间,研究了一下,写了一个小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秒在监控端就可以出现视频画面了。

下面是效果演示图:

        

猜你喜欢

转载自blog.csdn.net/sxiaobei/article/details/47989491
今日推荐