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秒在监控端就可以出现视频画面了。

下面是效果演示图:

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-12 20:02:49

websocket+webrtc+tomcat 实现视频监考功能的相关文章

使用WebRTC搭建前端视频聊天室——数据通道篇

转自 使用WebRTC搭建前端视频聊天室——数据通道篇 在两个浏览器中,为聊天.游戏.或是文件传输等需求发送信息是十分复杂的.通常情况下,我们需要建立一台服务器来转发数据,当然规模比较大的情况下,会扩展成多个数据中心.这种情况下很容易出现很高的延迟,同时难以保证数据的私密性. 这些问题可以通过WebRTC提供的RTCDataChannel API来解决,他能直接在点对点之间传输数据.这篇文章将介绍如何创建并使用数据通道,并提供了一些网络上常见的用例 为了充分理解这篇文章,你可能需要去了解一些RT

Java后端WebSocket的Tomcat实现(转)

文章摘要随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据. 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据:这种客户端是主动方,服务端是被动方的传统Web模式对于信息变化不频繁的Web

Java后端WebSocket的Tomcat实现

一.WebSocket简单介绍 随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了.近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据. 我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据:这种客户端是主动方,服务端是被动方的传统Web模式

使用WebRTC搭建前端视频聊天室——信令篇

博客原文地址 建议看这篇之前先看一下使用WebRTC搭建前端视频聊天室——入门篇 如果需要搭建实例的话可以参照SkyRTC-demo:github地址 其中使用了两个库:SkyRTC(github地址)和SkyRTC-client(github地址) 这两个库和demo都是我写的,如果有bug或是错误欢迎指出,我会尽力更正 前面的话 这篇文章讲述了WebRTC中所涉及的信令交换以及聊天室中的信令交换,主要内容来自WebRTC in the real world: STUN, TURN and s

HTML5 CSS3 诱人的实例 :模仿优酷视频截图功能

一般的视频网站对于用户上传的视频,在用户上传完成后,可以对播放的视频进行截图,然后作为视频的展示图.项目中也可以引入这样的功能给用户一种不错的体验,而不是让用户额外上传一张展示图. 效果图: 看起来还是很不错,下面我给大家分析下,极其核心代码很简单: _canvas = document.createElement("canvas"); _ctx = _canvas.getContext("2d"); _ctx.fillStyle = '#ffffff'; _ctx

使用 HTML5 webSocket API实现即时通讯的功能

工程下载地址:http://download.csdn.net/detail/wangshuxuncom/6430191 说明: 本工程用于展示如何使用 HTML5 webSocket API实现即时通讯的功能. 本工程编码方式:UTF-8 功能说明: 1.本功能支持多人聊天: 2.有人上线或下线时,会话窗口会自动显示: 3.支持快捷键关闭会话窗口或发送会话信息. 环境要求: 1.Tomcat 要求为7.0以上的版本 注意: 如果要在多台计算机上进行测试,需要将websocket.js文件中"w

canvas与html5实现视频截图功能

这段时间一直在研究canvas,突发奇想想做一个可以截屏视频的功能,然后把图片拉去做表情包,哈哈哈哈哈哈~~ 制作方法: 1.在页面中加载视频 在使用canvas制作这个截图功能时,首先必须保证页面上已经加载完成了这个视频,这样才能够方便的对其操作.如果使用下面直接嵌入<video>标签的方式: 1 2 3 4 5 <video loop controls id="testmp4" width="500" height="400"

Android 跳转系统选择本地视频的功能

今天在项目开发的过程中产品要求添加选择本地视频的功能,于是就翻阅和查找各种资料,进行功能的开发,但是在开发过程中发现,各种不同的品牌的手机跳转至系统选择本地视频的功能结果不太一样,所以我就对一些主流的品牌进行了测试,现做如下总结: 1.选择本地视频的功能 Button click event: Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Video.Media.EXTERNAL_CONTEN

采用七牛接口给视频添加水印功能

由于项目中用到视频水印功能,通过程序效率相对比较低. 最后决定先把视频上传到七牛,然后调用api给指定视频加上水印功能,具体实现 //初始化AK,SK string sACCESS_KEY = "七牛账号对应的AK"; string sSECRET_KEY = "七牛账号对应的SK"; string namekey = DateTime.Now.ToString("HHmmss")+".mp4"; byte[] b = Sys