介紹
最近這段時間折騰了一下
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)
實現
- 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("-", "");
- 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;
-
- 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;
-
- 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);
- else
- processSignalingMessage00(message.data);
- }
-
- function processSignalingMessage(message) {
- var msg = JSON.parse(message);
-
- if (msg.type === 'offer') {
-
- if (!initiator && !started)
- maybeStart();
-
-
- if (isRTCPeerConnection)
- pc.setRemoteDescription(new RTCSessionDescription(msg));
- else
- pc.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的失效時間。
截圖
演示地址
源碼下載
大家可以按照這種思路去自己實現,建議大家最好用Chrome瀏覽器進行測試。
大家可以進羣:197331959進行交流。