基於Chrome、Java、WebSocket、WebRTC實現瀏覽器視頻通話

介紹


        最近這段時間折騰了一下WebRTC,看了網上的https://apprtc.appspot.com/的例子(可能需要翻牆訪問),這個例子是部署在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實現,代碼:
[javascript] view plaincopy
  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. }  
建立一個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方法,向特定的用戶發送數據。
大家可以看看這段代碼:
[javascript] view plaincopy
  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. }  
這個是進入房間前的處理,然而客戶端是怎麼發起視頻通話的呢?
[javascript] view plaincopy
  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向客戶端發送連接數據,然後客戶端根據接收到的數據進行視頻接收:
[javascript] view plaincopy
  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瀏覽器進行測試。
大家可以進羣:197331959進行交流。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章