本文由 rwebrtc 翻译
WebRTC 技术是激烈的开放的 Web 战争中一大突破。-Brendan Eich, inventor of JavaScript
无插件实时通信
想象一下手机、TV 和电脑都通过统一平台进行沟通。试想一下,很容易的在你的网站中添加视频聊天和 P2P 数据分享。这是 WebRTC 技术的愿景。
想试一试吗?WebRT C在 Chrome、Opera 和 Firefox 中就可以使用。在 apprtc.appspot.com 中可以试一试这些例子:
- 在 Chrome、Opera 或 Firefox 中打开 apprtc.appspot.com。
- 点击允许按钮让这个应用使用你的摄像头( Web 应用程序)。
- 在一个新的标签页中打开底部的 URL,最好在另外的电脑上打开。
在这篇 文章 中有一个这个应用的例子。
快速开始
没有时间阅读这篇文章,或者只想尽快写代码?
- 看一看关于 Google I/O 大会上关于 WebRTC 的概述。(幻灯片在 这里 )
- 如果你没有用过 getUserMedia,看一看项目中的这篇 文章,在 这里 可以看到示例代码。
- 了解一下 RTCPeerConnection 的 API,点击 这里 和 这里 ,这个例子在一个简单的界面上实现了 WebRTC。
- 了解更多关于 WebRTC 使用的服务器、防火墙和 NAT 穿透相关的内容。读 这里 看调试日志。
- 等不及了想试试 WebRTC 吗?这里有 20 多个 Demos,试试这些 javaScript 的 APIs。
- 使用你的机器有问题吗? 这里 可以找到测试用例。
或者,直接跳到我们的 WebRTC codelab:一步一步的跟着指导,构建一个完整的视频聊天应用程序,包括一个简单的信令服务器。
WebRTC 技术简史
让人类通过网络进行音视频通信是网络最后的巨大挑战:实时通信( RTC )。实时通信就像网络上在文本框中输入文本一样自然,没有它,就限制了我们开发新的方式使人们互动交流起来。
从历史上看,RTC 变化很大很复杂,需要昂贵的音视频技术授权或者花费巨大代价去开发,RTC 技术与现有的内容、数据和服务整合一直都很困难和耗时,在网络上尤其如此。
Gmail 视频聊天在 2008 年开始流行,在 2011 年 Google 推出视频群聊,它使用 GoogleTalk 服务,就像 Gmail 一样。Google 收购了 GIPS,它是一个为 RTC 开发出许多组件的一个公司,例如编解码和回声消除技术。Google 开源了 GIPS 开发的技术,与相关机构 IET 和 W3C 制定行业标准。在 2011 年 5 月,爱立信实现第一 个 WebRTC应用。
WebRTC 已经实现了对于实时通信,免插件音频数据传输的标准制定。需求是:
- 许多网络服务已经使用了 RTC,但是需要下载,本地应用或者是插件。包括 Skype、Facebook、Google Hangouts。
- 下载安装升级插件是复杂的,可能出错的,令人厌烦的。
- 插件可能很难部署、调试、故障排除等——可能需要技术授权,复杂集成和昂贵的技术。说服人们去安装插件是很难的。
WebRTC 项目的指导原则是APIs应该是开源的,免费的,标准化的,浏览器内置的,比现有技术更高效的。
我们在哪
WebRTC 应用在了各种 App 上,包括 WhatsApp、Facebook Manager、appear.in 和 TokBox 平台上。甚至在 iOS 浏览器上的实验 WebRTC。WebRTC 也被 WebKitGTK+ 和 QT 内置使用。
微软在 Edge 中增加了 MediaCapture 和 Stream API。
WebRTC 实现了三个 APIs:
- MeidaStream(aka getUserMedia)
- RTCPeerConnection
- RTCDataChannel
getUserMedia 在 Chrome、Opera、FireFox 和 Edge 中都实现了。看一看跨浏览器的 Demo 和亚马逊的 例子,使用 getUserMedia 作为音频的输入。
RTCPeerConnection 在 Chrome、Opera 和 FireFox 中实现。关于名字的解释:经过几次迭代,RTCPeerConnection 被 Chrome 和 Opera 实现为 webkitRTCPeerConnection,被 FireFox 实现为 mozRTCPeerConnection。其他的名字都是过时的,当标准稳定时,前缀会被删除。在Github 上有超级简单的演示项目,构建在 apprtc.appspot.com 上。这个应用使用 adapter.ja,一个 JS 库,来帮助 WebRTC 跨浏览器。
RTCDataChannel 被 Chrome、Opera 和 FireFox 支持。检出 Github 上的 Demo 看看怎么使用。
警告
有些平台宣称支持 WebRTC 其实可能仅仅支持 getUserMedia,而不支持其他的 RTC 组件。
我的第一个 WebRTC
WebRTC 需要做几件事:
- 获取音视频流或其他数据流。
- 获取网络信息例如 IP 地址和端口号,与其他客户端交换这些信息来进行连接,包括穿透防火墙。
- 向 signaling 报告错误或启动会话关闭连接等。
- 对于分辨率解码器等与其他客户端交换信息。
- 传输音视频或其他数据信息。
为了获得和传输数据,WebRTC 实现了以下 APIs:
- MediaStream:从客户摄像头或麦克风获取数据流。
- RTCPeerConnection:音视频通话,包括加密和带宽等的管理。
- RTCDataChannel:P2P 数据传输管道。
(接下来详细讨论 WebRTC 的网络和信道)
MediaStream(aka getUserMedia)
MediaStream API 代表同步流媒体。例如一个来自摄像头和麦克风的流媒体输入已经同步了音视频。(不要混淆 MediaStream 和 track 标签,它们完全不同)。
也许理解 MediaStream 的容易的方式是看它的表现:
- 在 Chrome、Opera 打开 demo:
https://webrtc.github.io/samples/src/content/getusermedia/gum。
- 打开调试器。
- 检查全局范围内的流变量。
每个 MediaStream 有一个输入,navigator.getUserMedia(),也包括一个输出,输出到 video 标签或者是 RTCPeerConnection。
getUserMedia() 方法有三个参数:
- 一个约束对象。
- 成功回调,如果被调用,传递一个 MediaStream。
- 失败回调,如果被调用,传递一个错误对象。
每个 MediaStream 有一个标签,例如"as‘Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ"
一个 MediaStreamTracks 数组被 getAudioTracks() 和 getVideoTracks() 返回。
对于https://webrtc.github.io/samples/src/content/getusermedia/gum/
例如 stream.getAudioTracks() 返回一个空的数组,因为没有音频,假设一个摄像头被连接上了,stream.getVideoTracks() 返回一个 MediaStreamTrack 数组,代表这个摄像头的流。每个 MediaStreamTrack 有一个类型(音频或视频),一个标签(有时就像 FaceTime HD 照相机),表示一个或多个音视频通道。在这个例子中,只有视频没有音频。很容易想象更多的例子:例如,一个聊天应用,前置想象头后置摄像头和麦克风,屏幕共享应用。
在 Chrome 或 Opera 中,URL.createObjectURL() 方法可以转换一个 MediaStream 到一个 Blog URL,可以被设置作为视频的源。(在 FireFox 和 Opera 中,这个视频源可以通过 stream 本身创建),自从 M25 版本,基于 Chromium 的浏览器( Chrome 和 Opera )允许通过 getUserMedia 获取的音频数据放置到 audio 和 video 标签上(但是注意到默认情况下将时静音状态)。
getUserMedia 可以被作为 网页音频输入API :
function gotStream(stream) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
// Create an AudioNode from the stream
var mediaStreamSource = audioContext.createMediaStreamSource(stream);
// Connect it to destination to hear yourself
// or any other node for processing!
mediaStreamSource.connect(audioContext.destination);
}
navigator.getUserMedia({audio:true}, gotStream);
基于 Chromium 的应用或扩展也可以使用 getUserMedia 。在安装的时候仅一次添加 audioCapture 和 / 或 videoCapture 权限。此后用户不被询问关于摄像头和麦克风的权限。
对于使用 HTTPS 的网页也是同样:对于 getUserMedia 只需呀授予一次权限。第一次时,在标签上信息栏上显示允许按钮。
TODO 同时,Chrome 将不鼓励 getUserMedia() 访问 http,在 2015 年末,在 M44 版本,你可能在访问 HTTP 时看到一个警告。
最终,不止照相机和麦克风,其他的任何数据流都可以放到 MediaStream 中。可以从硬盘中获取数据流,或者从任何其他传感器或其他输入获取数据。
注意到 getUserMedia() 必须在服务器上使用,不可以在本地文件中使用。否则会报错。
getUserMedia() 可以与其他 javaScript 类库一起使用:
- Webcam Toy 是一个照片类 APP,使用 WebGL 来给照片添加奇怪的效果,可以被分享和保存到本地。
- FaceKat 是一个脸部跟踪的游戏,使用了 headtrackr.js。
- ASCII Camera 使用了 Ganvas API 来生成 ASCII 照片。
约束
已经被 Chrome、FireFox 和 Opera 实现。这些可以被用来设置由 getUserMedia() 和 RTCPeerConnection addStream() 获取到的视频分辨率。这个目的是实现其他约束,例如面对模式(前后摄像头),帧速率,高度和宽度,使用的是 applyconstraints() 方法。
在https://webrtc.github.io/samples/src/content/getusermedia/resolution/
有一个例子。
一个难以解决的问题:浏览器中,一个标签中 getUserMedia 设置的约束影响随后打开的标签页。设置不允许的值时给出了一个错误信息:
navigator.getUserMedia error:
NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}
屏幕和标签捕获
Chrome 应用也使得分享一个 video 标签在一个单一的浏览器标签中成为可能,或者整个桌面通过 chrome.tabCapture 和 chrome.desktopCapture 的 APIs 。在 这里 可以找到一个桌面 capture 的例子。对于截屏视频,代码和更多的信息查看 这里。
TODO 使用 csreen capture 作为一个 MediaStream 在 chrome 的源也是可能的,其中使用了 chromeMediaSource 约束,看这里 Demo ,注意到屏幕抓取需要 HTTPS 并且应该用命令行标记,被 WebRTC 讨论解释。
Signaling:会话控制,网络和媒体信息
WebRTC 使用 RTCPeerConnection 在浏览器间传递数据流,但是也需要一个机器协调沟通发送控制 i 信息,这被称为 signaling 过程。Signaling 方法和协议没有被 WebRTC 规定:signaling 不是 RTCPeerConnection 的 API 的一部分。
相反,WebRTC 应用的开发者可以选择任何一种他们喜欢的消息协议,例如 SIP 或者是 XMPP,或者任何全双工的通信通道。这个 apprtc.appspot.com 例子使用了 XHR 和 Channel API 作为这个 Signaling。这个 Codelab 使用了 Socket.io,是一个 Node服务器。
Signaling 被使用来交换三种信息:
- 连接控制信息:初始化或者关闭连接报告错误。
- 网络配置:对于外网,我们电脑的 IP 地址和端口?
- 多媒体数据:使用什么编码解码器,浏览器可以处理什么信息?
在进行 P2P 数据传输前,这些信息必须全部通过 Signaling 进行交换。
例如,假设 Alice 想要与 Bob 通信。这是这个 例子 ,显示了这个过程中 Signaling 的信息。这个代码假设某些信号的存在,在 createSignaling() 方法中创建。也注意到有在 Chrome 和 Opera 上有前缀。
var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;
// run start(true) to initiate a call
function start(isCaller) {
pc = new RTCPeerConnection(configuration);
// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};
// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};
// get the local stream, show it in the local video element and send it
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
if (isCaller)
pc.createOffer(gotDescription);
else
pc.createAnswer(pc.remoteDescription, gotDescription);
function gotDescription(desc) {
pc.setLocalDescription(desc);
signalingChannel.send(JSON.stringify({ "sdp": desc }));
}
});
}
signalingChannel.onmessage = function (evt) {
if (!pc)
start(false);
var signal = JSON.parse(evt.data);
if (signal.sdp)
pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
else
pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};
首先,Alice 和 Bob 交换网络信息。("finding candidates" 指的是使用 ICE 框架寻找网络端口)
- Alice 使用 onicecandidate 句柄创建一个 RTCPeerConnection。
- 当网络处理程序可用时,句柄运行。
- Alice 向 Bob 发送序列化数据,通过无论那种方式:WebSocket 或者其他方式。
- 当 Bob 从 Alice 获取到数据后,调用 addIceCandidate 添加远程节点描述中。
WebRTC 客户端(被称为 peers,例如 Alice 和 Bob ),也需要交换本地和远程音视频媒体信息,例如使用的协议和编码器。TODO Signaling 来交换信息。
- Alice 执行 RTCPeerConnection 的 createOffer() 方法,这通过 RTCSessionDescription 回调:Alice 本地会话描述。
- 在这个回调中,Alice 通过 setLocationDescription() 设置本地描述然后给 Bob 发送阶段描述通过他们的信道。注意到 RTCPeerConnection 直到 setRemoteDescription 被调用才会开始发送数据:在 这里 被确定。
- Bob 将 Alice 发送过来的消息进行设置作为远程信息的描述,通过 setRemoteDescription() 方法。
- Bob 运行 RTCPeerConnection 的 createAnswer() 的方法,通过从 Alice 获取到的描述信息,本地会话可以适配她的。这个 createAnswer() 回调被一个 RTCSessionDescription 回调:Bob 设置本地描述,发送给 Alice。
- 当 Alice 获取到了 Bob 的本地描述,通过 setRemoteDescription 设置这个描述信息。
- 开始通信。
RTCSessionDescription 对象符合 SDP,一个 SDP 类似于如下:
```
v=0
o=- 3883943731 1 IN IP4 127.0.0.1
s=
t=0 0
a=group:BUNDLE audio video
m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126
// ...
a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810
```
网络和媒体信息的采集和交换可以同时进行,但这两个过程必须完成之前,音频和视频流之间的同龄人可以开始。
网络信息的采集和交换可以同时进行,但是在发送音频和视频前都需要完成。。
这个 offer/andwer 被叫做 JSEP (JavaScript Session Establishment Protocol),在[这里]有一个很好的 WebRTC 实现的解释。
一旦这个 signaling 完成了,数据可以直接的在端到端之间进行数据传输。如果失败了,通过中介服务器 relay 服务进行转发。Streaming 是 RTCPeerConnection 的工作。
RTCPeerConnection
RTCPeerConnection 是 WebRTC 的一部分,它是稳定的有效率的端到端传输数据的句柄。
下面是一个 WebRTC 的架构,显示了 RTCPeerConnection 的作用,正如你看到的,绿色部分和复杂!
从 JavaScript 观点看,从图表中主要需要理解 RTCPeerConnection 向开发者屏蔽了底层复杂的东西。WebRTC 使用的协议和解码器做了大量的工作才使得实时通信成为了可能,甚至在不可靠的网络的情况下:
- 数据包丢包隐藏
- 回声消除
- 宽带自适应
- 动态抖动缓冲
- TODO 自动增益控制
- 降噪
- 图片清除
在 W3C代码 从 signaling 观点显示了一个简化的例子。下面是两个使用 WebRTC 工作的两个应用,第一个是一个简单的 RTCPeerConnection 的例子;第二个时一个完全的可操作的视频客户端。
没有服务器的 RTCPeerConnection
下面的代码是从 这里 的 Demo 。包括网页上本地和远程的 RTCPeerConnection 。这个没有什么用,呼叫和被呼叫都在同一个网页上,但是确实使得理解 RTCPeerConnection 的 API 更清楚了,由于这个界面的 RTCPeerConnection 在没有 Signaling 时也可以直接交换信息。
其他问题:TODO 看 http://www.w3.org/TR/webrtc/#constraints 获取更多信息。
在这里例子中,pc1 代表本地,pc2 代表远程。
本地
- 创建一个新的 RTCPeerConnection 添加 getUserMedia 获取到的数据流。
// servers is an optional config file (see TURN and STUN discussion below)
pc1 = new webkitRTCPeerConnection(servers);
// ...
pc1.addStream(localStream);
- 创建一个 offer 并且设置本地的关于 pc1 的描述作为和远程 pc2 的描述。这在没有使用 signaling 的情况下可以直接完成,因为本地和远程都在一个界面上。
pc1.createOffer(gotDescription1);
//...
function gotDescription1(desc){
pc1.setLocalDescription(desc);
trace("Offer from pc1 \n" + desc.sdp);
pc2.setRemoteDescription(desc);
pc2.createAnswer(gotDescription2);
}
远程
创建 pc2,添加 pc1 的流,在视频节点中显示:
pc2 = new webkitRTCPeerConnection(servers);
pc2.onaddstream = gotRemoteStream;
//...
function gotRemoteStream(e){
vid2.src = URL.createObjectURL(e.stream);
}
RTCPeerConnection 和服务器
在真实的世界中,WebRTC 需要服务器,但是时很简单的,下面的可能是现实的:
- 通过用户名可以找到对方。
- WebRTC 客户端交换网络信息。
- 端到端间交换媒体数据信息,例如分辨率和视频格式。
- WebRTC 客户端进行 NAT 和防火墙穿透。
换一句话说,WebRTC 需要四中服务类型的信息。
- 用户发现和通信。
- Signaling。
- NAT/防火墙穿透。
- 当端到端通信失败时,采用 Relay 服务通信。
NAT 穿透、端到端连接、用户发现和建立信令服务器超过了本文范畴。我想说的是,STUN 协议和它的延伸 ICE 冰框架可以使用 RTCPeerConnection 处理 NAT 和其他网络变化。
ICE 是一个连接端到端的框架,例如视频客户端。最初,ICE 尝试直接连接,通过采用 UDP 实现更少延迟,在这个过程中,STUN 服务有一个简单的任务:使一个在防火墙后面的设备找出公网 IP 和端口。Google 有两个 STUN 服务器,其中一个在 apprtc.appspot.com 个例子中就使用了。
如果 UDP 失败了,ICE 尝试 TCP:首先是 HTTP,然后是 HTTPS 。如果直接连接失败了——特别的是因为企业的 NAT 和防火墙—— ICE 使用一个中继 relay 的 TURN 服务器。换句话说,ICE 首先使用 UDP 的 STUN 服务器直接与对端连接,如果失败了,后退到 TURN 服务器。这表达了寻找网络和端口的过程。
WebRTC 工程师 Jastin 提供了关于 ICE、STUN 和 TURN 的更多的信息在 2013 年 Google 的 IO 大会上。(幻灯片 中有关于 TURN 和 STUN 的例子)
一个简单的 Chat 客户端
下面使用的信令服务器使用的是:https://apprtc.appspot.com。
如果你觉得这是有点令人困惑,你可能更喜欢我们的 WebRTC codelab 。这篇指南介绍了如何一步步构建一个完整的视频聊天应用,包括使用 Socket.io 搭建一个简单的信令服务器。
apprtc.appspot.com 是一个尝试 WebRTC 的好地方,这里使用了 Signaling 和 NAT/firewall 作为 STUN 服务器。这个应用使用 adapter.js 处理不同的 RTCPeerConnection 和 getUserMedia() 的实现。
这个代码是故意写的更长一点输出日志:查看控制台理解事件执行顺序。下面给出了一个详细的代码。
接下来
这个 Demo以initialize() 函数开始:
function initialize() {
console.log("Initializing; room=99688636.");
card = document.getElementById("card");
localVideo = document.getElementById("localVideo");
miniVideo = document.getElementById("miniVideo");
remoteVideo = document.getElementById("remoteVideo");
resetStatus();
openChannel(‘AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/‘);
doGetUserMedia();
}
注意被 openChannel() 使用的 room 和 token 变量,是被 Google APP 自己提供的:看一下 这里,在这个库里看添加了什么值。
这个代码初始化 video 变量将显示本地摄像头视频流和远程客户端视频流。resetStatus() 简单的设置状态信息。
openChannel() 函数设置 WebRTC 客户端之间的信息。
function openChannel(channelToken) {
console.log("Opening channel.");
var channel = new goog.appengine.Channel(channelToken);
var handler = {
‘onopen‘: onChannelOpened,
‘onmessage‘: onChannelMessage,
‘onerror‘: onChannelError,
‘onclose‘: onChannelClosed
};
socket = channel.open(handler);
}
对于 Signaling,这个 Demo 是用的是 Channel API ,这使得客户端间 JavaScript 没有轮询也可以通信。( WebRTC Signaling 上面有详细介绍)。
使用 Channel API 创建一个通道就像这样:
- Client 生成一个独特的 ID。
- Client A 向 APP Engine 请求一个数据管道,发送这个 ID。
- APP Engine 通过 Channel API,为这个 ID 和 Token 请求一个数据管道。
- APP 将这个 token 返还给 A。
- Client 打开 Socket 连接监听这个服务器的管道。
发送消息像这样:
- Client B 发送一个 POST 请求给 App Engine。
- App Engine 通过一个请求给 Channel。
- Channel 携带着信息给 Client A。
- Client A 的接受消息回调被调用。