WebRTC实现网页版多人视频聊天室

    • 因为产品中要加入网页中网络会议的功能,这几天都在倒腾 WebRTC,现在分享下工作成果。

      话说 WebRTC

      Real Time Communication 简称 RTC,是谷歌若干年前收购的一项技术,后来把这项技术应用到浏览器中并开源出来,而且搞了一套标准提交给W3C,称为WebRTC,官方地址是:http://www.webrtc.org/。WebRTC要求浏览器内置实时传输音视频的功能,并提供一致的API供JS使用。目前实现这套标准的浏览器有:Chrome、FireFox、Opera。微软虽然也在对WebRTC标准的制定做贡献,但仍然没有在任何版本的IE中支持WebRTC,所以,对于IE浏览器,不得不安装Chrome Frame插件来支持WebRTC;对于Safari浏览器,可以使用WebRtc4all这个插件,地址是:https://code.google.com/p/webrtc4all/。

      WebRTC基础

      WebRTC提供了三个API:MediaStream、RTCPeerConnection、RTCDataChannel。 MediaStream 用于获取本地的 音视频流。不同的浏览器名称不一样,但参数一样,谷歌和Opera是navigator.webkitGetUserMedia,火狐是 navigator.mozGetUserMedia。 RTCPeerConnection:和 getUserMedia 一样 谷歌和火狐分别会有webkit、moz前缀。这个对象主要用于两个浏览器之间建立连接以及传输音视频流。 RTCDataChannel 用于两个浏览器之间传输自定义的数据,用这个对象可以实现互发消息,而不用经过服务端的中转。

      WebRTC的实现是建立浏览器之间的直接连接,而不需要其他服务器的中转,即P2P,这就要求彼此之间需要知道对方的外网地址。但大多数计算机都位于NAT之后,只有少部分主机拥有外网地址,这就要求一种方式可以穿透NAT,STUN和TURN就是这样的技术。对于STUN和TURN的详细介绍,可以查看这里(http://www.h3c.com.cn/MiniSite/Technology_Circle/Net_Reptile/The_Five/Home/Catalog/201206/747038_97665_0.htm)。

      WebRTC会使用默认的或程序指定的SUTN服务器,获取指向当前主机的外网地址和端口。谷歌浏览器默认的是谷歌域名下的一个STUN,国内可能不大稳定,于是我找到了这个 stunserver.org/ ,连接速度比较快,据说当年飞信就是使用的这个,应该比较可靠。如果信不过第三方的STUN服务,也可以自己搭建一台,搭建过程也挺简单。

      P2P的建立过程需要依赖服务端中转外网IP及端口、音视频设备配置信息,所以服务端需要使用可以双工通讯的手段,比如WebSocket,来实现信令的中转,称之为信令服务器。

      WebRTC会话的建立详解

      会话的建立主要有两个过程:网络信息的交换、音视频设备信息的交换。以下以 lilei 要和 Lucy 开视频为例描述这两个过程。

      网络信息的交换:

      技术分享

       

      lilei首先创建了一个RTCPeerConnection对象,这个对象会自动的去向STUN服务器询问自己的外网IP和端口。然后lilei把自己的网络信息经过信令服务器中转后,发送给lucy。 lucy接收到lilei的网络信息之后,也创建了一个RTCPeerConnection对象,并把lilei发过来的信息通过addIceCandidate添加到对象中。 lucy把自己的网络信息经过信令服务器的中转后,发送给lilei。 lilei接收到信息后,通过RTCPeerConnection对象的addIceCandidate方法保存lucy的网络信息。

      音视频设备信息的交换:

      技术分享

       

      lilei通过RTCPeerConnection对象的createOffer方法,获取本地的音视频编码分辨率等信息,通过setLocalDescription添加到RTCPeerConnection中,并把这些信息经过信令服务器中转后发送给lucy。 lucy接收到lilei发过来的信息后,使用RTCPeerConnection对象的setRemoteDescription方法保存。然后通过createAnswer方法获取自己的音视频信息并以同样的手段发送给lilei。 lilei接收到lucy的信 息,调用setRemoteDescription方法保存。

      以上两个过程可以是并发的,并无先后顺序,但必须得等到两个过程都完成后,P2P的连接才真正的建立。一旦连接建立,lilei和lucy就可以直接发送音视频流,而不需要中转。WebRTC在获取本地网络信息的时候,会先尝试STUN,如果失败,则会使用TURN。

      WebRTC + Asp.net Web API 实现视频聊天室

      首先使用WebSocket实现信令服务器部分,在此需要用到微软开发的用于实现WebSocket的dll (http://www.nuget.org/packages/Microsoft.WebSockets/),以及Json.net。

      用于和客户端交互的会话类代码如下: 技术分享技术分享
      01.public class Session : WebSocketHandler
      02.{
      03.private static WebSocketCollection sessions = new WebSocketCollection();
      04. 
      05.public String UserId { get; set; }
      06. 
      07.public override void OnOpen()
      08.{
      09.this.UserId = Guid.NewGuid().ToString(‘N‘);
      10.var message = new { type = SignalMessageType.Conect, userId = this.UserId };
      11.sessions.Broadcast(Json.Encode(message));
      12. 
      13.sessions.Add(this);
      14.}
      15. 
      16.public override void OnMessage(string msg)
      17.{
      18.var obj = Json.Decode(msg);
      19.var messageType = (SignalMessageType)obj.type;
      20. 
      21.switch (messageType)
      22.{
      23.case SignalMessageType.Offer:
      24.case SignalMessageType.Answer:
      25.case SignalMessageType.IceCandidate:
      26.var session = sessions.Cast<Session>().FirstOrDefault(n => n.UserId == obj.userId);
      27.var message = new { type = messageType, userId = this.UserId, description = obj.description };
      28.session.Send(Json.Encode(message));
      29.break;
      30.}
      31.}
      32.}
      33. 
      34.public enum SignalMessageType
      35.{
      36.Conect,
      37.DisConnect,
      38.Offer,
      39.Answer,
      40.IceCandidate
      41.}

      WebAPI控制器需要引用命名空间“Microsoft.Web.WebSockets;”代码如下:


      技术分享
      01.public class SignalServerController : ApiController
      02.{
      03.[HttpGet]
      04.public HttpResponseMessage Connect()
      05.{
      06.var session = new WebRTCDemo.Session();
      07.HttpContext.Current.AcceptWebSocketRequest(session);
      08. 
      09.return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
      10.}
      11.}
      JS脚本: 技术分享
      001.var RtcConnect = function (_userId, _webSocketHelper) {
      002. 
      003.var config = { iceServers: [{ url: ‘stun:stunserver.org‘ }] };
      004.var peerConnection = null;
      005.var userId = _userId;
      006.var webSocketHelper = _webSocketHelper;
      007. 
      008.var createVideo = function (stream) {
      009.var src = window.webkitURL.createObjectURL(stream);
      010.var video = $(‘<video />‘).attr(‘src‘, src);
      011.var container = $(‘<div />‘).addClass(‘videoContainer‘).append(video).appendTo($(‘body‘));
      012. 
      013.video[0].play();
      014.return container;
      015.};
      016. 
      017.var init = function () {
      018. 
      019.window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
      020.peerConnection = window.RTCPeerConnection(config);
      021. 
      022.peerConnection.addEventListener(‘addstream‘, function (event) {
      023.createVideo(event.stream);
      024.});
      025.peerConnection.addEventListener(‘icecandidate‘, function (event) {
      026.var description = JSON.stringify(event.candidate);
      027.var message = JSON.stringify({ type: 4, userId: userId, description: description });
      028.webSocketHelper.send(message);
      029.});
      030. 
      031.navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
      032.var localStream = navigator.getMedia({ video: true, audio: true }, getUserMediaSuccess, getUserMediaFail);
      033.peerConnection.addStream(localStream);
      034. 
      035.};
      036. 
      037.this.connect = function () {
      038.peerConnection.createOffer(function (offer) {
      039.peerConnection.setLocalDescription(offer);
      040. 
      041.var description = JSON.stringify(offer);
      042.var message = JSON.stringify({ type: 2, userId: userId, description: description });
      043.webSocketHelper.send(message);
      044.});
      045. 
      046.};
      047. 
      048.this.acceptOffer = function (offer) {
      049.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
      050.peerConnection.createAnswer(function (answer) {
      051.peerConnection.setLocalDescription(answer);
      052.var description = JSON.stringify(answer);
      053. 
      054.var message = JSON.stringify({ type: 3, userId: userId, description: description });
      055.webSocketHelper.send(message);
      056.});
      057.};
      058. 
      059.this.acceptAnswer = function (answer) {
      060.peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
      061. 
      062.};
      063. 
      064.this.addIceCandidate = function (candidate) {
      065.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
      066.};
      067. 
      068.init();
      069. 
      070.};
      071. 
      072.var WebSocketHelper = function (callback) {
      073.var ws = null;
      074.var url = ws:// + document.location.host + ‘/api/Signal/Connect‘;
      075. 
      076.var init = function () {
      077.ws = new WebSocket(url);
      078.ws.onmessage = onmessage;
      079.ws.onerror = onerror;
      080.ws.onopen = onopen;
      081.};
      082. 
      083.var onmessage = function (message) {
      084.callback(JSON.parse(message.data));
      085.};
      086. 
      087.this.send = function (data) {
      088.ws.send(data);
      089.};
      090. 
      091.init();
      092.};
      093. 
      094.$(function() {
      095. 
      096.var rtcConnects = {};
      097.var webSocketHelper = new WebSocketHelper(function (message) {
      098.var rtcConnect = getOrCreateRtcConnect(message.userId);
      099.switch (message.type) {
      100.case 0//Conect
      101.rtcConnect.connect();
      102.break;
      103.case 2//Offer
      104.rtcConnect.acceptOffer(JSON.parse(message.description));
      105.break;
      106.case 3//Answer
      107.rtcConnect.acceptAnswer(JSON.parse(message.description));
      108.break;
      109.case 4//IceCandidate
      110.rtcConnect.addIceCandidate(JSON.parse(message.description));
      111.break;
      112.default:
      113.break;
      114.}
      115.});
      116. 
      117.var init = function() {
      118.navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
      119.var stream = navigator.getMedia({ video: true, audio: true }, function() {
      120.var src = window.webkitURL.createObjectURL(stream);
      121.var video = $(‘<video />‘).attr(‘src‘, src);
      122.$(‘<div />‘).addClass(‘videoContainer‘).append(video).appendTo($(‘body‘));
      123. 
      124.video[0].play();
      125.}, function (error) { console.error(error); });
      126.};
      127. 
      128.var getOrCreateRtcConnect = function (userId) {
      129.var rtcConnect = rtcConnects[userId];
      130.if (typeof (rtcConnect) == ‘undefined‘) {
      131.rtcConnect = new rtcConnect(userId, webSocketHelper);
      132.rtcConnects[userId] = rtcConnect;
      133.}
      134.return rtcConnect;
      135.};
      136.init();
      137.});
       View代码: 技术分享
      01.<html>
      02.<head>
      03.<style>
      04..videoContainer { float: left; padding: 10px 0 10px 10px; width: 210px; margin: 5px; }
      05..videoContainer > video { width: 200px; height: 150px; margin-top: 5px; }
      06.</style>
      07.</head>
      08.<body>
      09.</body>
      10.</html>
       编译后部署到IIS上,让同事都来试试,略有激动。

      其他

      如果想部署自己专用的STUN服务器,这里(http://www.stunprotocol.org/)有STUN服务器的完整开源实现,原生是运行在Linux上的,但也提供了cgwin下编译的windwos版本。如何编译、运行等在它的github主页上说的比较清楚:https://github.com/jselbie/stunserver。

      如果觉得自己写那一坨js比较繁琐,这里(http://www.rtcmulticonnection.org/)有一个封装库,简单了解了一下,功能挺强大的。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。