基于Chrome、Java、WebSocket、WebRTC实现浏览器视频通话(转载)

介绍

        最近这段时间折腾了一下 WebRTC,看了网上的 https://apprtc.appspot.com/ 的例子(可能需要FQ访问),这个例子是部署在Google App Engine上的应用程序,依赖GAE的环境,后台的语言是python,而且还依赖Google App Engine Channel API,所以无法在本地运行,也无法扩展。费了一番功夫研读了例子的python端的源代码,决定用Java实现,Tomcat7之后开始支持WebSocket,打算用WebSocket代替Google App Engine Channel API实现前后台的通讯,在整个例子中Java+WebSocket起到的作用是负责客户端之间的通信,并不负责视频的传输,视频的传输依赖于WebRTC。

 

实例的特点是:
        1.  HTML5
        2.  不需要任何插件
        3.  资源占用不是很大,对服务器的开销比较小,只要客户端建立连接,视频传输完全有浏览器完成
        4.  通过JS实现,理论上只要浏览器支持WebSocket,WebRTC就能运行(目前只在Chrome测试通过,Chrome版本24.0.1312.2 dev-m)

实现

对于前端JS代码及用到的对象大家可以访问 http://www.html5rocks.com/en/tutorials/webrtc/basics/查看详细的代码介绍。我在这里只介绍下我改动过的地方,首先建立一个客户端实时获取状态的连接,在GAE的例子上是通过GAE Channel API实现,我在这里用WebSocket实现,代码:

		function openChannel() {
			console.log("Opening channel.");
			socket = new WebSocket("ws://192.168.1.102:8080/RTCApp/websocket?u=${user}");
			socket.onopen = onChannelOpened;
			socket.onmessage = onChannelMessage;
			socket.onclose = onChannelClosed;
		}

  

建立一个WebSocket连接,并注册相关的事件。这里通过Java实现WebSocket连接:

 1 package org.rtc.servlet;
 2  
 3 import java.io.IOException;
 4  
 5 import javax.servlet.ServletException;
 6 import javax.servlet.annotation.WebServlet;
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9  
10 import org.apache.catalina.websocket.StreamInbound;
11 import org.apache.catalina.websocket.WebSocketServlet;
12 import org.rtc.websocket.WebRTCMessageInbound;
13  
14 @WebServlet(urlPatterns = { "/websocket"})
15 public class WebRTCWebSocketServlet extends WebSocketServlet {
16  
17     private static final long serialVersionUID = 1L;
18  
19     private String user;
20     
21     public void doGet(HttpServletRequest request, HttpServletResponse response)
22             throws ServletException, IOException {
23         this.user = request.getParameter("u");
24         super.doGet(request, response);
25     }
26  
27     @Override
28     protected StreamInbound createWebSocketInbound(String subProtocol) {
29         return new WebRTCMessageInbound(user);
30     }
31 }

如果你想实现WebSocket必须得用Tomcat7及以上版本,并且引入:catalina.jar,tomcat-coyote.jar两个JAR包,部署到Tomcat7之后得要去webapps/应用下面去删除这两个AR包否则无法启动,WebSocket访问和普通的访问最大的不同在于继承了WebSocketServlet,关于WebSocket的详细介绍大家可以访问 http://redstarofsleep.iteye.com/blog/1488639,在这里就不再赘述。大家可以看看WebRTCMessageInbound这个类的实现: 

 1 package org.rtc.websocket;
 2  
 3 import java.io.IOException;
 4 import java.nio.ByteBuffer;
 5 import java.nio.CharBuffer;
 6  
 7 import org.apache.catalina.websocket.MessageInbound;
 8 import org.apache.catalina.websocket.WsOutbound;
 9  
10 public class WebRTCMessageInbound extends MessageInbound {
11  
12     private final String user;
13  
14     public WebRTCMessageInbound(String user) {
15         this.user = user;
16     }
17     
18     public String getUser(){
19         return this.user;
20     }
21  
22     @Override
23     protected void onOpen(WsOutbound outbound) {
24         //触发连接事件,在连接池中添加连接
25         WebRTCMessageInboundPool.addMessageInbound(this);
26     }
27  
28     @Override
29     protected void onClose(int status) {
30         //触发关闭事件,在连接池中移除连接
31         WebRTCMessageInboundPool.removeMessageInbound(this);
32     }
33  
34     @Override
35     protected void onBinaryMessage(ByteBuffer message) throws IOException {
36         throw new UnsupportedOperationException(
37                 "Binary message not supported.");
38     }
39  
40     @Override
41     protected void onTextMessage(CharBuffer message) throws IOException {
42         
43     }
44 }

 

WebRTCMessageInbound继承了MessageInbound,并绑定了两个事件,关键的在于连接事件,将连接存放在连接池中,等客户端A发起发送信息的时候将客户端B的连接取出来发送数据,看看WebRTCMessageInboundPool这个类:

 1 package org.rtc.websocket;
 2  
 3 import java.io.IOException;
 4 import java.nio.CharBuffer;
 5 import java.util.HashMap;
 6 import java.util.Map;
 7  
 8 public class WebRTCMessageInboundPool {
 9  
10     private static final Map<String,WebRTCMessageInbound > connections = new HashMap<String,WebRTCMessageInbound>();
11     
12     public static void addMessageInbound(WebRTCMessageInbound inbound){
13         //添加连接
14         System.out.println("user : " + inbound.getUser() + " join..");
15         connections.put(inbound.getUser(), inbound);
16     }
17     
18     public static void removeMessageInbound(WebRTCMessageInbound inbound){
19         //移除连接
20         connections.remove(inbound.getUser());
21     }
22     
23     public static void sendMessage(String user,String message){
24         try {
25             //向特定的用户发送数据
26             System.out.println("send message to user : " + user + " message content : " + message);
27             WebRTCMessageInbound inbound = connections.get(user);
28             if(inbound != null){
29                 inbound.getWsOutbound().writeTextMessage(CharBuffer.wrap(message));
30             }
31         } catch (IOException e) {
32             e.printStackTrace();
33         }
34     }
35 }

 

WebRTCMessageInboundPool这个类中最重要的是sendMessage方法,向特定的用户发送数据。
大家可以看看这段代码:
1         function openChannel() {
2             console.log("Opening channel.");
3             socket = new WebSocket(
4                     "ws://192.168.1.102:8080/RTCApp/websocket?u=${user}");
5             socket.onopen = onChannelOpened;
6             socket.onmessage = onChannelMessage;
7             socket.onclose = onChannelClosed;
8         }

 

${user}是怎么来的呢?其实在进入这个页面之前是有段处理的:

 1 package org.rtc.servlet;
 2  
 3 import java.io.IOException;
 4 import java.util.UUID;
 5  
 6 import javax.servlet.ServletException;
 7 import javax.servlet.annotation.WebServlet;
 8 import javax.servlet.http.HttpServlet;
 9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletResponse;
11  
12 import org.apache.commons.lang.StringUtils;
13 import org.rtc.room.WebRTCRoomManager;
14  
15 @WebServlet(urlPatterns = {"/room"})
16 public class WebRTCRoomServlet extends HttpServlet {
17  
18     private static final long serialVersionUID = 1L;
19     
20     public void doGet(HttpServletRequest request, HttpServletResponse response)
21             throws ServletException, IOException {
22         this.doPost(request, response);
23     }
24  
25     public void doPost(HttpServletRequest request, HttpServletResponse response)
26             throws ServletException, IOException {
27         String r = request.getParameter("r");
28         if(StringUtils.isEmpty(r)){
29             //如果房间为空,则生成一个新的房间号
30             r = String.valueOf(System.currentTimeMillis());
31             response.sendRedirect("room?r=" + r);
32         }else{
33             Integer initiator = 1;
34             String user = UUID.randomUUID().toString().replace("-", "");//生成一个用户ID串
35             if(!WebRTCRoomManager.haveUser(r)){//第一次进入可能是没有人的,所以就要等待连接,如果有人进入了带这个房间好的页面就会发起视频通话的连接
36                 initiator = 0;//如果房间没有人则不发送连接的请求
37             }
38             WebRTCRoomManager.addUser(r, user);//向房间中添加一个用户
39             String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort() +  request.getContextPath() +"/";
40             String roomLink = basePath + "room?r=" + r;
41             String roomKey = r;//设置一些变量
42             request.setAttribute("initiator", initiator);
43             request.setAttribute("roomLink", roomLink);
44             request.setAttribute("roomKey", roomKey);
45             request.setAttribute("user", user);
46             request.getRequestDispatcher("index.jsp").forward(request, response);
47         }
48     }
49 }

这个是进入房间前的处理,然而客户端是怎么发起视频通话的呢?

 1 function initialize() {
 2             console.log("Initializing; room=${roomKey}.");
 3             card = document.getElementById("card");
 4             localVideo = document.getElementById("localVideo");
 5             miniVideo = document.getElementById("miniVideo");
 6             remoteVideo = document.getElementById("remoteVideo");
 7             resetStatus();
 8             openChannel();
 9             getUserMedia();
10         }
11         
12         function getUserMedia() {
13             try {
14                 navigator.webkitGetUserMedia({
15                     'audio' : true,
16                     'video' : true
17                 }, onUserMediaSuccess, onUserMediaError);
18                 console.log("Requested access to local media with new syntax.");
19             } catch (e) {
20                 try {
21                     navigator.webkitGetUserMedia("video,audio",
22                             onUserMediaSuccess, onUserMediaError);
23                     console
24                             .log("Requested access to local media with old syntax.");
25                 } catch (e) {
26                     alert("webkitGetUserMedia() failed. Is the MediaStream flag enabled in about:flags?");
27                     console.log("webkitGetUserMedia failed with exception: "
28                             + e.message);
29                 }
30             }
31         }
32         
33         function onUserMediaSuccess(stream) {
34             console.log("User has granted access to local media.");
35             var url = webkitURL.createObjectURL(stream);
36             localVideo.style.opacity = 1;
37             localVideo.src = url;
38             localStream = stream;
39             // Caller creates PeerConnection.
40             if (initiator)
41                 maybeStart();
42         }
43         
44         function maybeStart() {
45             if (!started && localStream && channelReady) {
46                 setStatus("Connecting...");
47                 console.log("Creating PeerConnection.");
48                 createPeerConnection();
49                 console.log("Adding local stream.");
50                 pc.addStream(localStream);
51                 started = true;
52                 // Caller initiates offer to peer.
53                 if (initiator)
54                     doCall();
55             }
56         }
57  
58         function doCall() {
59             console.log("Sending offer to peer.");
60             if (isRTCPeerConnection) {
61                 pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
62             } else {
63                 var offer = pc.createOffer(mediaConstraints);
64                 pc.setLocalDescription(pc.SDP_OFFER, offer);
65                 sendMessage({
66                     type : 'offer',
67                     sdp : offer.toSdp()
68                 });
69                 pc.startIce();
70             }
71         }
72  
73         function setLocalAndSendMessage(sessionDescription) {
74             pc.setLocalDescription(sessionDescription);
75             sendMessage(sessionDescription);
76         }
77  
78         function sendMessage(message) {
79             var msgString = JSON.stringify(message);
80             console.log('发出信息 : ' + msgString);
81             path = 'message?r=${roomKey}' + '&u=${user}';
82             var xhr = new XMLHttpRequest();
83             xhr.open('POST', path, true);
84             xhr.send(msgString);
85         }

页面加载完之后会调用initialize方法,initialize方法中调用了getUserMedia方法,这个方法是通过本地摄像头获取视频的方法,在成功获取视频之后发送连接请求,并在客户端建立连接管道,最后通过sendMessage向另外一个客户端发送连接的请求,参数为当前通话的房间号和当前登陆人,下图是连接产生的日志: 

 

 

 1 package org.rtc.servlet;
 2  
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6  
 7 import javax.servlet.ServletException;
 8 import javax.servlet.ServletInputStream;
 9 import javax.servlet.annotation.WebServlet;
10 import javax.servlet.http.HttpServlet;
11 import javax.servlet.http.HttpServletRequest;
12 import javax.servlet.http.HttpServletResponse;
13  
14 import net.sf.json.JSONObject;
15  
16 import org.rtc.room.WebRTCRoomManager;
17 import org.rtc.websocket.WebRTCMessageInboundPool;
18  
19 @WebServlet(urlPatterns = {"/message"})
20 public class WebRTCMessageServlet extends HttpServlet {
21  
22     private static final long serialVersionUID = 1L;
23  
24     public void doGet(HttpServletRequest request, HttpServletResponse response)
25             throws ServletException, IOException {
26         super.doPost(request, response);
27     }
28  
29     public void doPost(HttpServletRequest request, HttpServletResponse response)
30             throws ServletException, IOException {
31         String r = request.getParameter("r");//房间号
32         String u = request.getParameter("u");//通话人
33         BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream)request.getInputStream()));
34         String line = null;
35         StringBuilder sb = new StringBuilder();
36         while((line = br.readLine())!=null){
37             sb.append(line); //获取输入流,主要是视频定位的信息
38         }
39         
40         String message = sb.toString();
41         JSONObject json = JSONObject.fromObject(message);
42         if (json != null) {
43             String type = json.getString("type");
44             if ("bye".equals(type)) {//客户端退出视频聊天
45                 System.out.println("user :" + u + " exit..");
46                 WebRTCRoomManager.removeUser(r, u);
47             }
48         }
49         String otherUser = WebRTCRoomManager.getOtherUser(r, u);//获取通话的对象
50         if (u.equals(otherUser)) {
51             message = message.replace("\"offer\"", "\"answer\"");
52             message = message.replace("a=crypto:0 AES_CM_128_HMAC_SHA1_32",
53                     "a=xrypto:0 AES_CM_128_HMAC_SHA1_32");
54             message = message.replace("a=ice-options:google-ice\\r\\n", "");
55         }
56         //向对方发送连接数据
57         WebRTCMessageInboundPool.sendMessage(otherUser, message);
58     }
59 }

 

就这样通过WebSokcet向客户端发送连接数据,然后客户端根据接收到的数据进行视频接收:

 1 function onChannelMessage(message) {
 2             console.log('收到信息 : ' + message.data);
 3             if (isRTCPeerConnection)
 4                 processSignalingMessage(message.data);//建立视频连接
 5             else
 6                 processSignalingMessage00(message.data);
 7         }
 8         
 9         function processSignalingMessage(message) {
10             var msg = JSON.parse(message);
11  
12             if (msg.type === 'offer') {
13                 // Callee creates PeerConnection
14                 if (!initiator && !started)
15                     maybeStart();
16  
17                 // We only know JSEP version after createPeerConnection().
18                 if (isRTCPeerConnection)
19                     pc.setRemoteDescription(new RTCSessionDescription(msg));
20                 else
21                     pc.setRemoteDescription(pc.SDP_OFFER,
22                             new SessionDescription(msg.sdp));
23  
24                 doAnswer();
25             } else if (msg.type === 'answer' && started) {
26                 pc.setRemoteDescription(new RTCSessionDescription(msg));
27             } else if (msg.type === 'candidate' && started) {
28                 var candidate = new RTCIceCandidate({
29                     sdpMLineIndex : msg.label,
30                     candidate : msg.candidate
31                 });
32                 pc.addIceCandidate(candidate);
33             } else if (msg.type === 'bye' && started) {
34                 onRemoteHangup();
35             }
36         }

就这样通过Java、WebSocket、WebRTC就实现了在浏览器上的视频通话。

请教

还有一个就自己的一个疑问,我定义的WebSocket失效时间是20秒,时间太短了。希望大家指教一下如何设置WebSocket的失效时间。

截图

 

 

演示地址

你可以和你的朋友一起进入 http://blog.csdn.net/leecho571/article/details/8207102 ,感受下Ext结合WebSocket、WebRTC构建的即时通讯
建议大家将chrome升级至最新版本 http://www.google.cn/intl/zh-CN/chrome/browser/eula.html?extra=devchannel&platform=win 

源码下载

http://download.csdn.net/detail/leecho571/5117399

 

大家可以按照这种思路去自己实现,建议大家最好用Chrome浏览器进行测试。
大家可以进群:197331959进行交流。

 

————————————————
版权声明:本文为CSDN博主「我的执着」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/leecho571/article/details/8146525

posted on 2021-07-19 17:21  hi-gdl  阅读(973)  评论(0编辑  收藏  举报

导航