要实现浏览器器端到端的通信,要用到两项技术一项是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" οnclick="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" οnclick="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秒在监控端就可以出现视频画面了。
下面是效果演示图: