Lucius's Blog

WebRTC-初探

什么是WebRTC

WebRTC(Web Real-Time Communication),顾名思义,是基于Web技术上实现实时通信技术。例如QQ视频,Facetime,但不需要在浏览器上安装任何的插件,根据其标准以及HTML5标签和JavaScript语法就可以实现这些功能。

WebRTC最大的特点有两点:浏览器与浏览器之间的对等连接,即一个浏览器通过标准协议与另一个浏览器进行实时通信;另一个特点是WebRTC提供了信令服务器,该服务器在浏览器和对等连接另一端之间提供信令通道,关于这一点下面会讲到。目前是1.0版本,之后可能会有更多的特性。

一个最简单的WebRTC结构,但实际上情况比这个复杂更多,例如多个浏览器互相建立对等连接的对方会话。

如何使用WebRTC

建立一个简单的WebRTC会话,只需要以下四个步骤:
1)获取本地媒体。window.navigator.getUserMedia,获取单个本地MediaStream
2)在浏览器和对等端(其他浏览器)建立连接,简单来说就是不通过服务器通信,而是浏览器与浏览器之间的媒体连接。new RTCPeerConnection(c),简单的说明下c为ICE打洞通过NAT设备和防火墙需要的配置信息;
3)将媒体和数据通道关联至此连接,new RTCSessionDescription
4)交换会话描述;
5)关闭连接。
结构图如以下左边图,若添加了信令通道,则如右边图。

这里简单说下3)跟4)。当建立完连接之后,本地以及远程都会通过RTCPeerConnection交换RTCSessionDescription,而RTCSessionDescription的意义是为了两端之间协商如何建立媒体信息,例如编解码器。交换完毕之后,即可建立媒体或数据会话。此时,两个浏览器开始打洞(穿透NAT设备和防火墙)。打洞完毕之后,即可开始协商RTCSessionDescription交换的信息。为了确保安全,交换密钥。最后开始媒体或数据通话。

WebRTC的体系结构如下,有必要提醒的是,浏览器A发起的1、2和浏览器B发起的3、4是不排先后的。而且实际上的体系结构是根据需求改变的,而并非以下图的一成不变。

  1. 浏览器A向服务器请求网页;
  2. 服务器返回携带WebRTC应用的网页给浏览器A;
  3. 浏览器B向服务器请求网页;
  4. 服务器返回携带同A一样内容的网页给浏览器B;
  5. 浏览器A想与浏览器B通信,于是通过服务器,将描述对象(offer, 提议)发送给服务器;
  6. 服务器将浏览器A发送过来的描述对象发送给浏览器B;
  7. 浏览器B接收到提议之后,也将自己的描述对象(answer, 应答)发送到服务器;
  8. 服务器接收到浏览器B的描述对象之后发送给浏览器A;
  9. 浏览器A接收到浏览器B的应答之后,两端开始打洞,根据两边的描述对象来确定最佳的访问方式;
  10. 打完洞之后,就开始协商密钥以确保之后的会话安全;
  11. 两端开始互相传输视频,语音以及数据。

目前兼容情况(2018-04)

本地媒体

MediaStreamTrack,是WebRTC的基本单位,可以是自源的原始媒体,也可以是浏览器转换过的。例如,MediaStreamTrack可以是摄像头以高分辨率录制的低采样率版本。MediaStreamTrack中的readyState有new、live、ended,可通过enabled=false禁用。而MediaStream是MediaStreamTrack的集合,目前通过window.navigator.getUserMedia获取的或使用对等连接接收的流,未来说不定还有其他的获取方式。MediaStreamTrack - Web APIs | MDN

下面有简单的代码示例下如何获取本地媒体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
navigator.getUserMedia({'audio': true, 'video': true}, gotUserMedia, didntGetUserMedia);

function didntGetUserMedia(s) {}

function gotUserMedia(s) {
var videoEle = document.getElementById('videoEle');
videoEle.srcObject = s; // 目前还无法使用Blob
console.log(s.getVideoTracks(), s.getAudioTracks());

// 约束,是指浏览器会将媒体流尽可能的满足约束的条件,如宽高,比例,帧率等约束条件。
var constraints = {
width: {min: 640, ideal: 1280},
height: {min: 480, ideal: 720},
advanced: [
{width: 1920, height: 1280},
{aspectRatio: 1.333}
]
};
var t = (s.getVideoTracks())[0];
t.applyConstraints(constraints).then((v) => {
// t.getSettings:返回所有设定的约束值
// t.getConstraints:返回当前约束的值
console.log(t.getSettings(), t.getConstraints());
});
}

s.getTracks()的信息如下图。

通过以上代码可以看出gotUserMedia可以正常的返回音视频轨道,但是还无法正常的播放。

信令(signaling)

其作用主要体现以下几点:

  • 协商媒体功能和设置(必备);
  • 标识和验证会话参与者的身份;
  • 控制媒体会话、指示进度、更改会话和终止会话;
  • 当会话双方同时尝试建立或更改会话时,实施双占用分解。

媒体协商

因为必备,所以来简单说明下。这一步主要的工作是在参与对等连接的两个浏览器之间交换SDP(Session Description Protocol)对象中包含的信息,例如音视频数据、编解码器以及有关的带宽信息等。此外还用于交换候选地址,便于ICE打洞。候选地址表示浏览器可从中接收潜在的媒体数据包的IP地址和UDP端口。另外还必须在信令通道交换密钥。只有交换完候选地址才能开始ICE打洞,之后才能建立对等连接。

信令传输

常见的信令传输有:HTTP轮询、WebSocket和数据通道。由于前两种比较常见,我们就只说说数据通道的原理。

在最初建立数据通道时,是无法传输WebRTC信令的,因为数据通道也需要信令才能建立,所以一开始,是通过HTTP或者WebSocket来传输最开始的信令的。当两个浏览器之间建立数据通道之后,它会提供直接的低延迟连接,这时候就可以处理来往的信令了。

1
2
3
4
5
6
7
8
9
10
11
12
// 伪代码
funciton createSignalingChannel(msgHandler) {
var socket = new WebSocket('wss://192.168.0.1:49152/');
socket.addEventListener('message', msgHandler, false);
return {'send': socket.send};
}
// 打开WebSocket
signalingChannel = createSignalingChannel(onMessage);
// 通过WebSocket发送消息
signalingChannel.send(message);
// 处理通过WebSocket传入的消息
function onMessage(msg) { //... }

对等媒体

对等媒体,指的就是两个浏览器之间能够直接建立视频、音频和数据连接,能够节约带宽。但必须采用特殊的协议,如STUN服务器和TURN服务器。不然会受到NAT和防火墙的拦截。

对等媒体和网络地址转换(NAT)

所谓NAT,简单来说就是将私有地址转换成公有地址的一种方式,例如内网IP:内网端口 -> 外网IP:外网端口。常见设备如路由器。所以说,每个处于NAT的网络都是独立的,网络与网络之间的访问都是通过NAT来访问的。

在上面说过对等媒体会受到NAT的影响,就是因为端对端的连接不像我们常用的C/S结构,能够遍历NAT,因此我们需要穿过它,如STUN服务器和TURN服务器。

对等连接和提议/应答协商

对等连接

RTCPeerConnection是WebRTC用于两端之间建立媒体与数据连接的API。其API是绑定在JSEP(JavaScript Session Establishment Protocol)协议上来进行媒体协商的,而且该JSEP基本上由浏览器处理。值得说明的一点,WebRTC的对等连接并非是TCP意义上的连接。而是一组路径建立进程(ICE)以及一个可确定应建立哪些媒体和数据路径的协商器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var pc = new RTCPeerConnection({
iceServers: [
{'url': 'stun:stun.l.google.com:19302'},
{'url': 'turn:user@turn.myserver.com',
'credential': 'test'}
]
});

window.navigator.getUserMedia({'audio': true, 'video': true}, successCB, failureCB);

function successCB(stream) {
// 添加本地媒体流到对接的浏览器端,但是此操作不会产生任何媒体流。只是为了告诉浏览器,本地浏览器与对等端浏览器就该MediaSteam进行协商
pc.addStream(stream);
}

提议/应答协商

提议/应答的主要意义就是确保双方都知道要发送和接收的媒体类型,以及如何正确解码和处理媒体流。例如:要使用的一个或多个解码器、解码器的参数、用于媒体加密和身份验证的密钥信息等。这样双方就可以达成一致,方便后续工作的处理。RTCSessionDescription,来表示提议和应答。

1
2
3
4
// 将自己的会话描述告诉本地浏览器
pc.setLocalDescription(mySessionDescription);
// 将对等端的会话描述告诉本地浏览器
pc.setRemoteDescription(remoteSessionDescription);

上面的伪代码存在一个问题,那就是一端只能是接收方,而另一端只能是发送方。这样的话,万一发送方跟接收方的角色对调则会发生问题。好在WebRTC提供了如下的解决方法:

1
2
3
4
// 创建提议
pc.createOffer(gotOffer, didntGetOffer);
// 创建应答
pc.createAnswer(gotOffer, didntGetOffer);

整个过程的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// A浏览器
// 访问,其中key为房间号
GET '/connect?key=123456'
// 假如访问成功,那么就进入等待状态
if(readyState === DONE && status === 200) {
// 等待另一端的访问
Waiting
}
// 获取本地媒体
navigator.getUserMedia({'video': true, 'audio': true})

// B浏览器同样访问服务器并且加入房间号为123456

// A浏览器收到消息,并建立对等连接
connect()
// config用到的是Google的公用的stun服务器,为候选地址,是ICE打洞通过NAT设备和防火墙需要的配置信息。
config = [{"url":"stun:stun.l.google.com:19302"}]
pc = new RTCPeerConnection({iceServers:config});
pc.onicecandidate = onIceCandidate;
pc.onaddstream = onRemoteStreamAdded;
pc.onremovestream = onRemoteStreamRemoved;

// A浏览器发起提议 或 B浏览器发起提议。我们这里默认使用A浏览器发起。
pc.createOffer(gotDescription, doNothing, constraints);
gotDescription(sDesc) -> pc.setLocalDescription(sDesc);
// 通过服务器将信息发送给B浏览器
seng(sDesc);
// A浏览器触发onicecandidate事件,即如果我们浏览器有候选配置,则将其发送给对等端
onicecandidate(e) -> send({type: 'candidate', candidate: e. candidate})
// B浏览器收到offer,之后将收到的返回给A浏览器
pc.setRemoteDescription(new RTCSessionDescription(msg));
// B浏览器创建answer,将接收到的描述添加到本地
pc.createAnswer(gotDescription, doNothing, constraints);
gotDescription(sDesc) -> pc.setLocalDescription(sDesc);
// A浏览器收到B浏览器发送的候选配置
pc.addIceCandidate(new RTCIceCandidate({sdpMLineIndex:msg. candidate.mlineindex, candidate:msg. candidate.candidate}));
// ps: 以上的提议 / 应答的过程会经历几次。
// 这时候你不但能看到自己帅气的脸,还可以看到对方美丽的脸。

数据通道

WebRTC数据通道是在浏览器之间,绕过服务器来直接交换数据的。在此之前我们都是借助WebSocket或HTTP来满足需求。而现在,我们可以通过WebRTC数据通道来传输,而且数据通道支持流量大、延迟低的连接。

WebRTC数据通道是基于WebSocket建立的,也具有send方法和onmessage方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pc = new RTCPeerConnection();
// dc-1为标签,创建完之后会导致对端收到RTCDataChannelEvent
dc = pc.createDataChannel('dc-1');
// 可在对端处理该事件
pc = new RTCPeerConnection();
pc.ondatachannel = function(e) {
dc = e.channel;
}

// send / onmessage
dc.send('string type msg');
dc.send(new Blob(['blob message'], {type: 'text/plain'}));
dc.send(new ArrayBuffer(32));
dc.send(new uInt8Array([1,2,3]));
dc.onmessage = function(e) {
console.log('Received message' + e.data);
}

// .createDataChannel(),第二个参数为控制配置
// 限制通道在第一次数据传输失败时重新传输数据的次数
pc.createDataChannel('', {maxRetransimits:3});
// 限制通道允许重试操作持续的毫秒数
pc.createDataChannel('', {maxRetransimitTime:30});
// ps:maxRetransimits、maxRetransimitTime是互斥的
// 不止这两个配置
// order: 接收顺序,false则为接收顺序混乱也无所谓

更多例子:WebRTC samples

参考资料

我只是试下能不能被赞赏😳