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/)有一个封装库,简单了解了一下,功能挺强大的。

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值