`
923723914
  • 浏览: 635924 次
文章分类
社区版块
存档分类
最新评论

基于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实现,代码:
		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的失效时间。

截图






演示地址

你可以和你的朋友一起进入http://blog.csdn.net/leecho571/article/details/8207102,感受下Ext结合WebSocket、WebRTC构建的即时通讯


源码下载


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

分享到:
评论

相关推荐

    第五次作业函数第一题代码

    第五次作业函数第一题--

    基于深度学习的作物病害诊断内含数据集和运行环境说明.zip

    本项目旨在利用深度学习方法实现作物病害的自动诊断。作物病害是农业生产中的重要问题,及时诊断和处理对于减少产量损失至关重要。 我们采用深度学习算法,通过分析作物的图像,实现对病害的自动识别和分类。项目使用的数据集包括公开的作物病害图像数据集,如ISIC等,并进行了预处理,包括图像增强、分割和特征提取等。 在运行环境方面,我们使用Python编程语言,基于TensorFlow、PyTorch等深度学习框架进行开发。为了提高计算效率,我们还使用了GPU加速计算。此外,我们还采用了Docker容器技术,确保实验结果的可重复性。 项目完成后,将实现对作物病害的快速、准确诊断,为农业生产提供有力支持,有助于减少产量损失。同时,项目成果也可应用于其他图像识别和分类任务。

    机械设计CD驱动印刷设备step非常好的设计图纸100%好用.zip

    机械设计CD驱动印刷设备step非常好的设计图纸100%好用.zip

    tensorflow-2.7.2-cp37-cp37m-manylinux2010-x86-64.whl

    python烟花代码

    python烟花代码示例

    附件中是一个简单的烟花效果的代码示例: 在Python中,可以使用多种方式来模拟烟花效果,其中一种常用的方法是使用turtle模块,它提供了一个画布和一个小海龟,可以用来绘制各种图形。 这段代码首先导入了turtle模块和random模块,然后在屏幕上绘制了10次烟花爆炸的效果。每次爆炸都是由5个小圆组成,颜色随机选择,圆的大小也是随机的。 请注意,这段代码需要在支持turtle模块的Python环境中运行,并且需要有图形界面的支持。如果你在没有图形界面的环境中(比如某些服务器或者命令行界面),这段代码可能无法正常运行。

    商业化产品经理,到底如何实现产品商业化?.docx

    商业化产品经理,到底如何实现产品商业化?.docx

    Panduit 工业以太网部件内部销售指南

    Panduit 工业以太网部件内部销售指南

    Java版三维装箱代码示例

    在Java中,实现一个三维装箱(也称为三维背包问题)的算法通常涉及到组合优化和动态规划。这个问题是一个典型的优化问题,其中目标是在三个维度的限制下最大化价值的总和。下面是一个简单的Java代码示例,它使用动态规划来解决三维装箱问题。 请注意,这个代码只是一个简单的示例,它假设所有物品的第三个维度的大小都是1,并且没有给出如何回溯选择物品的完整逻辑。在实际应用中,三维装箱问题可能更加复杂,需要考虑所有三个维度的限制,并且可能需要更复杂的算法来解决。 此外,这个问题的解决方案可能需要根据具体问题的要求进行调整,例如物品是否可以分割、是否允许超过一个的物品等。如果你有特定的问题描述或者需要进一步的帮助,请提供更多的细节。

    常用品牌EPLAN部件库

    常用品牌EPLAN部件库

    单片机开发的教程.doc

    单片机开发的教程可以分为以下几个步骤: 1. 了解单片机基础知识:在学习单片机开发之前,需要了解单片机的相关知识,包括单片机的基本结构、指令系统、编程语言等。 2. 选择开发板:选择一款适合自己学习开发板的型号和厂商,通常需要关注开发板的性价比、开发环境是否友好等因素。 3. 学习开发环境:根据所选的开发板,学习相关的开发环境和使用方法,例如Keil、IAR等集成开发环境。 4. 掌握编程语言:单片机常用的编程语言包括C语言和汇编语言,根据实际情况选择其中一种进行学习。 5. 基础操作:熟悉单片机的引脚定义和IO口配置,了解单片机的启动代码,可以通过修改启动代码进行基本功能调试。 6. 综合实践:根据具体项目需求,进行单片机开发的综合实践。在实践中需要掌握如何编写程序、如何进行硬件调试、如何使用相关工具软件等技能。 下面是一个单片机开发的简单教程介绍: 首先,确定所使用的单片机型号和开发板类型。在这个阶段,需要查阅相关资料,了解开发板的规格书、芯片规格等基本资料。 其次,安装并配置开发环境。根据所选的开发板,安装相应的集成开发环境(IDE),并配置好开发环境。 接着,学习并掌

    Q1.ipynb

    Q1.ipynb

    (自适应手机端)IT网络建站公司pbootcms模板 互联网营销企业网站源码下载.zip

    (自适应手机端)IT网络建站公司pbootcms模板 互联网营销企业网站源码下载.zip

    Bematech 激光扫描器用户手册

    Bematech 激光扫描器用户手册

    激励视频接入文档.pdf

    激励视频接入文档.pdf

    java jdk1.8 202版本下载window linux打包

    java jdk1.8 202版本下载window linux打包

    Lite Beam M5快速指南 Lite Beam M5天线设置指南

    Lite Beam M5快速指南

    互联网金融导论.docx

    互联网金融导论.docx

    字节跳动青训营-抖音项目

    字节跳动青训营——抖音项目

    node-v12.22.10-linux-s390x.tar.xz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

Global site tag (gtag.js) - Google Analytics