WebRTC1-原理探究

1.抛砖引玉

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流或/和音频流或者其他任意数据的传输
实时查看WebRTC在浏览器中的支持情况: http://caniuse.com/#search=webRTC
FirFox 45+,Chrome 29+,Oprea 36+,Edge 14+,Android Brower 50+支持,其余支持情况有问题。

备注:有的时候会使用adapter.js,这个js文件是为了提高兼容性,可以直接使用API 不用加前缀
使用:

几个概念

  • SDP(Session Description Protocol)
    SDP是一种会话描述协议,用来描述双方的IP地址和端口号,通信所使用的带宽,会话的名称、标识符、激活时间,双方所要传输的媒体类型(视频、音频、文本)、媒体格式等等。该协议仅包含所要传递的媒体的描述信息,而不直接传递媒体内容。
  • ICE(Interactive Connectivity Establishment)
    ICE是一种以UDP为基础用于实现穿越NAT网管或者防火墙的协议。
  • TURN&&STUN
    两种协议都是用来明确自己的外网地址的,差别是如果要服务器辅助进行数据交换则设置TURN服务器,不需要则设置STUN服务。

核心API

  • Navigator.getUserMedia
    用来获取视频和音频,在浏览器装有摄像头和麦克风的情况下使用.navigator.getUserMedia(constraints, successCallback, errorCallback);constraints是用来控制视频和音频是否获取,一般设为{video:true,audio: true},即视频和音频都获取。
  • RTCPeerConnection
    RTCPeerConnection是一个表示两个浏览器端的连接的对象,其含有关于这个连接的所有信息和相关方法,是WebRTC的核心API,负责制作建立连接的SDP、ICE等报文,管理连接状态等等。
  • RTCDataChannel
    RTCDataChannel由RTCPeerConnection创建,需要传递视频、音频以外的数据时使用,它代表浏览器两端间的一个数据通道,和这个数据通道有关的属性和方法都记录在这个对象里。

2.按图索骥


过程:

  1. 首先双方都建立一个RTCPeerConnection的实例,其中一方(称为offer方)用RTCPeerConnection.createOffer()创建一个会话描述sessionDescription,该会话描述包含SDP报文信息和该sessionDescription的类型(type)
  2. 接下来调用RTCPeerConnection.setLocalDescription()方法将本地的localDescription设置为刚才创建的sessionDescription。之后将创建的sessionDescription发送给对方(称为answer方),发送方式没有规定,可以通过服务器中转,可以通过IM软件发送(这里使用WebSocket信令服务器)。
  3. answer端接收到sessionDescription后调用RTCPeerConnection. setRemoteDescription方法设置,然后调用RTCPeerConnection. createAnswer方法产生自己的sessionDescription
  4. 再将创建的sessionDescription发送给offfer方,同样发送方式没有规定。offer方接收到sessionDescrip后调用RTCPeerConnection. setRemoteDescription方法设置,这样双方的SDP信息就交换完成了。
  5. 在完成SDP的交换后双方还要交换ICE candidate信息。双方首先设置RTCPeerConnection.onicecandidate回调函数,当candidate可用时,双方中的一方将所有icecandidate发送给对方,发送方式同样没有规定,接收方调用RTCPeerConnection.addIceCandidate方法接收candidate信息。经过这些步骤后双方连接就建立完成了。

3.纸上可谈兵

3.1.由简入繁

html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<meta name="keywords" content="JavaScript, WebRTC" />
<meta name="description" content="WebRTC" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1">
<title>获取本地视频</title>
<link rel="stylesheet" href="simpleVideo.css">
</head>

<body>
<!-- <video /> -->
<video></video>
<p><br></p>
<script src='../lib/adapter.js'></script>
<script type="text/javascript" src="simpleVideo.js"></script>
</body>
</html>

js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var constraints = {
video: true,
audio:true
};

function successCallback(stream) {
window.stream = stream; // stream available to console
console.log(stream);
console.log(stream.getVideoTracks());
console.log(stream.getAudioTracks());

var video = document.querySelector("video");
video.src = window.URL.createObjectURL(stream);
video.play();
}

function errorCallback(error) {
console.log("getUserMedia error: ", error);
}

getUserMedia(constraints, successCallback, errorCallback);

3.2.顺藤摸瓜

这是个完整的例子,参照2.按图索骥部分理解

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
<!DOCTYPE html>
<html>

<head>
<meta name="keywords" content="JavaScript, WebRTC" />
<meta name="description" content="WebRTC codelab" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1">
<title>WebRTC codelab: step 2</title>
<link rel="stylesheet" href="css/index.css">
<!-- css可以使用一些滤镜效果 -->
<style>

</style>
<script src='js/lib/adapter.js'></script>
</head>

<body>
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay muted></video>
<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
<script src="index.js">
</body>

</html>

index.js

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<script>
var localStream, localPeerConnection, remotePeerConnection;

var localVideo = document.getElementById("localVideo");
var remoteVideo = document.getElementById("remoteVideo");

var startButton = document.getElementById("startButton");
var callButton = document.getElementById("callButton");
var hangupButton = document.getElementById("hangupButton");
startButton.disabled = false;
callButton.disabled = true;
hangupButton.disabled = true;
startButton.onclick = start;
callButton.onclick = call;
hangupButton.onclick = hangup;

function trace(text) {
console.log((performance.now() / 1000).toFixed(3) + ": " + text);
}

function gotStream(stream) {
trace("Received local stream");
localVideo.src = URL.createObjectURL(stream);
localStream = stream;
callButton.disabled = false;
}

function start() {
trace("Requesting local stream");
startButton.disabled = true;
getUserMedia({
audio: true,
video: true
}, gotStream,
function(error) {
trace("getUserMedia error: ", error);
});
}

function call() {
callButton.disabled = true;
hangupButton.disabled = false;
trace("Starting call");

if (localStream.getVideoTracks().length > 0) {
trace('Using video device: ' + localStream.getVideoTracks()[0].label);
}
if (localStream.getAudioTracks().length > 0) {
trace('Using audio device: ' + localStream.getAudioTracks()[0].label);
}

var servers = null;//本机测试不用其他服务器

localPeerConnection = new RTCPeerConnection(servers);//offer方
trace("Created local peer connection object localPeerConnection");
localPeerConnection.onicecandidate = gotLocalIceCandidate;//offer方发送ICE
remotePeerConnection = new RTCPeerConnection(servers);//answe方
trace("Created remote peer connection object remotePeerConnection");
remotePeerConnection.onicecandidate = gotRemoteIceCandidate;//answer方发送ICE
remotePeerConnection.onaddstream = gotRemoteStream;//设置视频流

localPeerConnection.addStream(localStream);
trace("Added localStream to localPeerConnection");
localPeerConnection.createOffer(gotLocalDescription, handleError);//作为offer,产生自己的SessionDescription【SD】信息
}

function gotLocalDescription(description) {//description是offer方的SD
localPeerConnection.setLocalDescription(description);
trace("Offer from localPeerConnection: \n" + description.sdp);
remotePeerConnection.setRemoteDescription(description);//answer方接收offer的SD
remotePeerConnection.createAnswer(gotRemoteDescription, handleError);//answer方发送自己的SD
}

function gotRemoteDescription(description) {
remotePeerConnection.setLocalDescription(description);//anwer方设置本身自己的SD
trace("Answer from remotePeerConnection: \n" + description.sdp);
localPeerConnection.setRemoteDescription(description);//offer接收answer方的SD
}

function hangup() {
trace("Ending call");
localPeerConnection.close();
remotePeerConnection.close();
localPeerConnection = null;
remotePeerConnection = null;
hangupButton.disabled = true;
callButton.disabled = false;
}

function gotRemoteStream(event) {
remoteVideo.src = URL.createObjectURL(event.stream);
trace("Received remote stream");
}

function gotLocalIceCandidate(event) {
if (event.candidate) {
remotePeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));//answer方接收ICE
trace("Local ICE candidate: \n" + event.candidate.candidate);
}
}

function gotRemoteIceCandidate(event) {
if (event.candidate) {
localPeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));//offer方接收ICE
trace("Remote ICE candidate: \n " + event.candidate.candidate);
}
}

function handleError() {}
</script>

另外加一些CSS可以很容易实现滤镜效果

1
2
3
4
5
	video {
filter: hue-rotate(180deg) saturate(200%);
-moz-filter: hue-rotate(180deg) saturate(200%);
-webkit-filter: hue-rotate(180deg) saturate(200%);
}

运行这两段代码,就可以调出来本地视频窗口

参考资料

强烈推荐的WebRTC入门教程
google webRTC
WebRTC官网
WebRTC-W3School
MDN-WebRTC

WebRTC实践教程
使用WebRTC搭建前端视频聊天室——信令篇
可以用WebRTC来做视频直播吗?-知乎
实时猫–WebRTC服务商