引用網址:http://基于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。
实例的特点是:
- 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);//建立视频连接 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
文章標籤
全站熱搜