因为产品中要加入网页中网络会议的功能,这几天都在倒腾 WebRTC,现在分享下工作成果。
话说 WebRTCReal 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的详细介绍,可以查看这里。
WebRTC会使用默认的或程序指定的SUTN服务器,获取指向当前主机的外网地址和端口。谷歌浏览器默认的是谷歌域名下的一个STUN,国内可能不大稳定,于是我找到了这个stunserver.org/ ,连接速度比较快,据说当年飞信就是使用的这个,应该比较可靠。如果信不过第三方的STUN服务,也可以自己搭建一台,搭建过程也挺简单。
P2P的建立过程需要依赖服务端中转外网IP及端口、音视频设备配置信息,所以服务端需要使用可以双工通讯的手段,比如WebSocket,来实现信令的中转,称之为信令服务器。
WebRTC会话的建立详解会话的建立主要有两个过程:网络信息的交换、音视频设备信息的交换。以下以 lilei 要和 Lucy 开视频为例描述这两个过程。
网络信息的交换:
view sourceprint?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;
10.
var message =
new
{ type = SignalMessageType.Conect, userId =
this
.UserId };
11.
sessions.Broadcast);
12.
13.
sessions.Add; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">this
);
14.
}
15.
16.
public
override
void
OnMessage
17.
{
18.
var obj = Json.Decode;
19.
var messageType = obj.type;
20.
21.
switch
22.
{
23.
case
SignalMessageType.Offer:
24.
case
SignalMessageType.Answer:
25.
case
SignalMessageType.IceCandidate:
26.
var session = sessions.Cast
27.
var message =
new
{ type = messageType, userId =
this
.UserId, description = obj.description };
28.
session.Send);
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;”代码如下:
view sourceprint?01.
public
class
SignalServerController : ApiController
02.
{
03.
[HttpGet]
04.
public
HttpResponseMessage Connect
05.
{
06.
var session =
new
WebRTCDemo.Session;
07.
HttpContext.Current.AcceptWebSocketRequest;
08.
09.
return
new
HttpResponseMessage;
10.
}
11.
}
JS脚本:view sourceprint?001.
var RtcConnect = function {
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 {
009.
var src = window.webkitURL.createObjectURL;
010.
var video = $.attr;
011.
var container = $.addClass.append.appendTo);
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;
021.
022.
peerConnection.addEventListener {
023.
createVideo;
024.
});
025.
peerConnection.addEventListener {
026.
var description = JSON.stringify;
027.
var message = JSON.stringify; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">4
, userId: userId, description: description });
028.
webSocketHelper.send;
029.
});
030.
031.
navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
032.
var localStream = navigator.getMedia; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">true
, audio:
true
}, getUserMediaSuccess, getUserMediaFail);
033.
peerConnection.addStream;
034.
035.
};
036.
037.
this
.connect = function {
038.
peerConnection.createOffer {
039.
peerConnection.setLocalDescription;
040.
041.
var description = JSON.stringify;
042.
var message = JSON.stringify; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">2
, userId: userId, description: description });
043.
webSocketHelper.send;
044.
});
045.
046.
};
047.
048.
this
.acceptOffer = function {
049.
peerConnection.setRemoteDescription; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">new
RTCSessionDescription);
050.
peerConnection.createAnswer {
051.
peerConnection.setLocalDescription;
052.
var description = JSON.stringify;
053.
054.
var message = JSON.stringify; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">3
, userId: userId, description: description });
055.
webSocketHelper.send;
056.
});
057.
};
058.
059.
this
.acceptAnswer = function {
060.
peerConnection.setRemoteDescription; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">new
RTCSessionDescription);
061.
062.
};
063.
064.
this
.addIceCandidate = function {
065.
peerConnection.addIceCandidate; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">new
RTCIceCandidate);
066.
};
067.
068.
init;
069.
070.
};
071.
072.
var WebSocketHelper = function {
073.
var ws =
null
;
074.
var url =
"ws://"
+ document.location.host +
"/api/Signal/Connect"
;
075.
076.
var init = function {
077.
ws =
new
WebSocket;
078.
ws.onmessage = onmessage;
079.
ws.onerror = onerror;
080.
ws.onopen = onopen;
081.
};
082.
083.
var onmessage = function {
084.
callback);
085.
};
086.
087.
this
.send = function {
088.
ws.send;
089.
};
090.
091.
init;
092.
};
093.
094.
$ {
095.
096.
var rtcConnects = {};
097.
var webSocketHelper =
new
WebSocketHelper {
098.
var rtcConnect = getOrCreateRtcConnect;
099.
switch
{
100.
case
0
:
//Conect
101.
rtcConnect.connect;
102.
break
;
103.
case
2
:
//Offer
104.
rtcConnect.acceptOffer);
105.
break
;
106.
case
3
:
//Answer
107.
rtcConnect.acceptAnswer);
108.
break
;
109.
case
4
:
//IceCandidate
110.
rtcConnect.addIceCandidate);
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; margin: 0px; padding: 0px; border: 0px; outline: 0px; background-image: none; float: none; vertical-align: baseline; position: static; left: auto; top: auto; right: auto; bottom: auto; height: auto; width: auto; font-family: "Courier New", 宋体;">true
, audio:
true
}, function {
120.
var src = window.webkitURL.createObjectURL;
121.
var video = $.attr;
122.
$.addClass.append.appendTo);
123.
124.
video[
0
].play;
125.
}, function { console.error; });
126.
};
127.
128.
var getOrCreateRtcConnect = function {
129.
var rtcConnect = rtcConnects[userId];
130.
if
==
"undefined"
) {
131.
rtcConnect =
new
rtcConnect;
132.
rtcConnects[userId] = rtcConnect;
133.
}
134.
return
rtcConnect;
135.
};
136.
init;
137.
});
View代码:view sourceprint?01.
02.
03.
04.
.videoContainer {
float
: left; padding: 10px
0
10px 10px; width: 210px; margin: 5px; }
05.
.videoContainer > video { width: 200px; height: 150px; margin-top: 5px; }
06.
07.
08.
09.
10.
编译后部署到IIS上,让同事都来试试,略有激动。其他如果想部署自己专用的STUN服务器,这里有STUN服务器的完整开源实现,原生是运行在Linux上的,但也提供了cgwin下编译的windwos版本。如何编译、运行等在它的github主页上说的比较清楚:https://github.com/jselbie/stunserver。
如果觉得自己写那一坨js比较繁琐,这里有一个封装库,简单了解了一下,功能挺强大的。
延伸阅读:1、ASP.NET 使用application和session对象写的简单聊天室程序2、ASP.NET列印所显示的网页内容3、升级ASP.NET 4.5 网页开发技术的新助力4、ASP.net MVC ckeditor网页编辑器 字型、图片上传功能的快速安装笔记5、ASP.NET MVC显示WebForm网页或UserControl控件