介紹 最近這段時間折騰了一下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。執行個體的特點是:
- HTML5
- 不需要任何外掛程式
- 資源佔用不是很大,對伺服器的開銷比較小,只要用戶端建立串連,視頻傳輸完全有瀏覽器完成
- 通過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串連:
package org.rtc.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.apache.catalina.websocket.StreamInbound;import org.apache.catalina.websocket.WebSocketServlet;import org.rtc.websocket.WebRTCMessageInbound;@WebServlet(urlPatterns = { "/websocket"})public class WebRTCWebSocketServlet extends WebSocketServlet {private static final long serialVersionUID = 1L;private String user;public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {this.user = request.getParameter("u");super.doGet(request, response);} @Override protected StreamInbound createWebSocketInbound(String subProtocol) { return new WebRTCMessageInbound(user); }}
如果你想實現WebSocket必須得用Tomcat7及以上版本,並且引入:catalina.jar,tomcat-coyote.jar兩個JAR包,部署到Tomcat7之後得要去webapps/應用下面去刪除這兩個AR包否則無法啟動,WebSocket訪問和普通的訪問最大的不同在於繼承了WebSocketServlet,關於WebSocket的詳細介紹大家可以訪問http://redstarofsleep.iteye.com/blog/1488639,在這裡就不再贅述。大家可以看看WebRTCMessageInbound這個類的實現:
package org.rtc.websocket;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.CharBuffer;import org.apache.catalina.websocket.MessageInbound;import org.apache.catalina.websocket.WsOutbound;public class WebRTCMessageInbound extends MessageInbound { private final String user; public WebRTCMessageInbound(String user) { this.user = user; } public String getUser(){ return this.user; } @Override protected void onOpen(WsOutbound outbound) { //觸發串連事件,在串連池中添加串連 WebRTCMessageInboundPool.addMessageInbound(this); } @Override protected void onClose(int status) { //觸發關閉事件,在串連池中移除串連 WebRTCMessageInboundPool.removeMessageInbound(this); } @Override protected void onBinaryMessage(ByteBuffer message) throws IOException { throw new UnsupportedOperationException( "Binary message not supported."); } @Override protected void onTextMessage(CharBuffer message) throws IOException { }}
WebRTCMessageInbound繼承了MessageInbound,並綁定了兩個事件,關鍵的在於串連事件,將串連存放在串連池中,等用戶端A發起發送資訊的時候將用戶端B的串連取出來發送資料,看看WebRTCMessageInboundPool這個類:
package org.rtc.websocket;import java.io.IOException;import java.nio.CharBuffer;import java.util.HashMap;import java.util.Map;public class WebRTCMessageInboundPool {private static final Map<String,WebRTCMessageInbound > connections = new HashMap<String,WebRTCMessageInbound>();public static void addMessageInbound(WebRTCMessageInbound inbound){//添加串連System.out.println("user : " + inbound.getUser() + " join..");connections.put(inbound.getUser(), inbound);}public static void removeMessageInbound(WebRTCMessageInbound inbound){//移除串連connections.remove(inbound.getUser());}public static void sendMessage(String user,String message){try {//向特定的使用者發送資料System.out.println("send message to user : " + user + " message content : " + message);WebRTCMessageInbound inbound = connections.get(user);if(inbound != null){inbound.getWsOutbound().writeTextMessage(CharBuffer.wrap(message));}} catch (IOException e) {e.printStackTrace();}}}
WebRTCMessageInboundPool這個類中最重要的是sendMessage方法,向特定的使用者發送資料。大家可以看看這段代碼:
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;}
${user}是怎麼來的呢?其實在進入這個頁面之前是有段處理的:
package org.rtc.servlet;import java.io.IOException;import java.util.UUID;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.apache.commons.lang.StringUtils;import org.rtc.room.WebRTCRoomManager;@WebServlet(urlPatterns = {"/room"})public class WebRTCRoomServlet extends HttpServlet {private static final long serialVersionUID = 1L;public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {this.doPost(request, response);}public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {String r = request.getParameter("r");if(StringUtils.isEmpty(r)){//如果房間為空白,則產生一個新的房間號r = String.valueOf(System.currentTimeMillis());response.sendRedirect("room?r=" + r);}else{Integer initiator = 1;String user = UUID.randomUUID().toString().replace("-", "");//產生一個使用者ID串if(!WebRTCRoomManager.haveUser(r)){//第一次進入可能是沒有人的,所以就要等待串連,如果有人進入了帶這個房間好的頁面就會發起視訊通話的串連initiator = 0;//如果房間沒有人則不發送串連的請求}WebRTCRoomManager.addUser(r, user);//向房間中添加一個使用者String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort() + request.getContextPath() +"/";String roomLink = basePath + "room?r=" + r;String roomKey = r;//設定一些變數request.setAttribute("initiator", initiator);request.setAttribute("roomLink", roomLink);request.setAttribute("roomKey", roomKey);request.setAttribute("user", user);request.getRequestDispatcher("index.jsp").forward(request, response);}}}
這個是進入房間前的處理,然而用戶端是怎麼發起視訊通話的呢?
function initialize() {console.log("Initializing; room=${roomKey}.");card = document.getElementById("card");localVideo = document.getElementById("localVideo");miniVideo = document.getElementById("miniVideo");remoteVideo = document.getElementById("remoteVideo");resetStatus();openChannel();getUserMedia();}function getUserMedia() {try {navigator.webkitGetUserMedia({'audio' : true,'video' : true}, onUserMediaSuccess, onUserMediaError);console.log("Requested access to local media with new syntax.");} catch (e) {try {navigator.webkitGetUserMedia("video,audio",onUserMediaSuccess, onUserMediaError);console.log("Requested access to local media with old syntax.");} catch (e) {alert("webkitGetUserMedia() failed. Is the MediaStream flag enabled in about:flags?");console.log("webkitGetUserMedia failed with exception: "+ e.message);}}}function onUserMediaSuccess(stream) {console.log("User has granted access to local media.");var url = webkitURL.createObjectURL(stream);localVideo.style.opacity = 1;localVideo.src = url;localStream = stream;// Caller creates PeerConnection.if (initiator)maybeStart();}function maybeStart() {if (!started && localStream && channelReady) {setStatus("Connecting...");console.log("Creating PeerConnection.");createPeerConnection();console.log("Adding local stream.");pc.addStream(localStream);started = true;// Caller initiates offer to peer.if (initiator)doCall();}}function doCall() {console.log("Sending offer to peer.");if (isRTCPeerConnection) {pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);} else {var offer = pc.createOffer(mediaConstraints);pc.setLocalDescription(pc.SDP_OFFER, offer);sendMessage({type : 'offer',sdp : offer.toSdp()});pc.startIce();}}function setLocalAndSendMessage(sessionDescription) {pc.setLocalDescription(sessionDescription);sendMessage(sessionDescription);}function sendMessage(message) {var msgString = JSON.stringify(message);console.log('發出資訊 : ' + msgString);path = 'message?r=${roomKey}' + '&u=${user}';var xhr = new XMLHttpRequest();xhr.open('POST', path, true);xhr.send(msgString);}
頁面載入完之後會調用initialize方法,initialize方法中調用了getUserMedia方法,這個方法是通過本地網路攝影機擷取視頻的方法,在成功擷取視頻之後發送串連請求,並在用戶端建立串連管道,最後通過sendMessage向另外一個用戶端發送串連的請求,參數為當前通話的房間號和當前登陸人,是串連產生的日誌:
package org.rtc.servlet;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import javax.servlet.ServletException;import javax.servlet.ServletInputStream;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import net.sf.json.JSONObject;import org.rtc.room.WebRTCRoomManager;import org.rtc.websocket.WebRTCMessageInboundPool;@WebServlet(urlPatterns = {"/message"})public class WebRTCMessageServlet extends HttpServlet {private static final long serialVersionUID = 1L;public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {super.doPost(request, response);}public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {String r = request.getParameter("r");//房間號String u = request.getParameter("u");//通話人 BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream)request.getInputStream())); String line = null; StringBuilder sb = new StringBuilder(); while((line = br.readLine())!=null){ sb.append(line); //擷取輸入資料流,主要是視頻定位的資訊 }String message = sb.toString();JSONObject json = JSONObject.fromObject(message);if (json != null) {String type = json.getString("type");if ("bye".equals(type)) {//用戶端退出視訊交談System.out.println("user :" + u + " exit..");WebRTCRoomManager.removeUser(r, u);}}String otherUser = WebRTCRoomManager.getOtherUser(r, u);//擷取通話的對象if (u.equals(otherUser)) {message = message.replace("\"offer\"", "\"answer\"");message = message.replace("a=crypto:0 AES_CM_128_HMAC_SHA1_32","a=xrypto:0 AES_CM_128_HMAC_SHA1_32");message = message.replace("a=ice-options:google-ice\\r\\n", "");}//向對方發送串連資料WebRTCMessageInboundPool.sendMessage(otherUser, message);}}
就這樣通過WebSokcet向用戶端發送串連資料,然後用戶端根據接收到的資料進行視頻接收:
function onChannelMessage(message) {console.log('收到資訊 : ' + message.data);if (isRTCPeerConnection)processSignalingMessage(message.data);//建立視頻串連elseprocessSignalingMessage00(message.data);}function processSignalingMessage(message) {var msg = JSON.parse(message);if (msg.type === 'offer') {// Callee creates PeerConnectionif (!initiator && !started)maybeStart();// We only know JSEP version after createPeerConnection().if (isRTCPeerConnection)pc.setRemoteDescription(new RTCSessionDescription(msg));elsepc.setRemoteDescription(pc.SDP_OFFER,new SessionDescription(msg.sdp));doAnswer();} else if (msg.type === 'answer' && started) {pc.setRemoteDescription(new RTCSessionDescription(msg));} else if (msg.type === 'candidate' && started) {var candidate = new RTCIceCandidate({sdpMLineIndex : msg.label,candidate : msg.candidate});pc.addIceCandidate(candidate);} else if (msg.type === 'bye' && started) {onRemoteHangup();}}
就這樣通過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進行交流。