Java+WebSocket+WebRTC实现视频通话实例

来源:互联网 发布:淘宝店铺图片多大尺寸 编辑:程序博客网 时间:2024/05/17 04:38

http://www.tuicool.com/articles/qmE3ii

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实现,代码:
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);//建立视频连接      else        processSignalingMessage00(message.data);    }        function processSignalingMessage(message) {      var msg = JSON.parse(message);      if (msg.type === 'offer') {        // Callee creates PeerConnection        if (!initiator && !started)          maybeStart();        // We only know JSEP version after createPeerConnection().        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的失效时间。

截图






其他


源码问题,由于各方面的原因源码暂时不能公布出来,主要是现在源码比较乱,而且还有很多BUG需要整理。

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

0 0