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