4 WebRTC
4.1 历史
相信读者都有过使用Tencent QQ或者FaceTime进行视频通话的经历,这样的应用场景相当典型和流行,但是基本上来说它们都是每个公司推出的私有产品,而且通信等协议也都是保密的,这使得一种产品的用户基本上不可能同其他产品的用户进行视频通信。还有一些更大的应用场景,那就是众多用户一起召开视频会议,这比简单的点对点更为复杂,很多公司已投身其中,因为这一市场非常广大。
几年前,笔者是很难想象这么复杂的需求和应用场景能够在Web领域中被实现,但是现在Chromium和Firefox浏览器都支持Web视频通信,而且神奇的是它们之间也可以相互通信,听起来非常不可思议——只是需要Web开发者使用JavaScript和HTML5技术就可以完成,还是免费的,而且还不需要额外安装应用软件,不需要额外安装插件。现在真是互联网和HTML5技术的好时代,这也是HTML5的多媒体领域的一个重大进展。
WebRTC(Web Real Time Communication)技术,中文全称为Web实时通信技术,它是一种提供实时视频通信的规范,目前是W3C推荐的规范。它是一个开放的规范,任何人都可以免费使用,目前Chromium/Chrome和Firefox浏览器都支持了该规范。WebRTC是HTML5对多媒体支持的一个重大进展,因为该技术不仅使用了音视频的输入和输出,而且还涉及连接等网络连接,是一个非常复杂但非常有用的技术。图11-19是一个简单的示意图,说明两个支持WebRTC规范的浏览器之间是如何进行视频通信的。事实上,WebRTC既允许使用服务器来进行通信,也支持点对点(Peer-to-Peer)通信,当然需要服务器的辅助。
图11-19 Chromium/Chrome和Firefox浏览器的视频通信示例图
不过,很多事情并非是一蹴而就的,何况这么复杂的WebRTC技术,WebRTC发展至今也经历了很长的过程。首先得从一个音视频的捕获需求开始。最初,HTML5希望能够提供一种捕获用户音频和视频的技术,这就是getUserMedia,当然刚开始也不是它,而是使用下面的语句来完成音频和视频的捕获。
<input type="file" accept="video/*;capture=camcorder"> <input type="file" accept="audio/*;capture=microphone">
但是它们太简单了,只能将捕获的信息保存为一个文件或者一个快照,这显然不能满足很多实际的需求,之后一个新的元素定义诞生,就是使用“device”元素,这一元素很快被抛弃。这就是getUserMedia的前身,但标准化组织很快从“device”元素转向getUserMedia,它的基本使用方式是:
navigator.getUserMedia({video: true, audio: true}, function (videostream) { … });
思想很简单,就是在navigator这个全局对象下加入一个新接口,该接口使用两个参数,第一表示它需要捕获视频或者音频或者两者都需要,第二个是一个回调函数,当捕获成功后,将捕获的视频流可以输出到一个视频元素,这就像是一个从服务器加载的视频文件,当然它是一个视频流,所以某些查找(Seek)操作不能工作。这里,不再使用新元素,而是利用原有的“video”元素,实在是一个好的设计。
在这之后,一个更为大胆和激进的想法诞生了,就是将RTC技术引入到HTML5中来,这就是WebRTC技术,因为getUserMedia是入口,所以自然而然地被使用到WebRTC技术的规范中来。
11.4.2 原理和规范
大家可以在脑海中想象一下如何要构建一个网络视频通信、需要哪些部分的参与及共同工作才能完成整个过程。总体上,这一过程中需要三种类型的技术,其一是视频,其二是音频,其三是网络传输。在这三种技术上,具体需要以下一些部分。
- 音视频输入和输出设备 :同音视频播放不同,因为它们只是需要输出设备,这里需要输入和输出设备(麦克风和摄像头)。同时,输入使用getUserMedia技术,而输出,基本上可以采用音视频播放的基本框架,当然,需要一些额外的支持。
- 网络连接的建立 :因为视频通信需要不停地传送大量数据,所以需要建立一种可靠的网络连接来让各个参与方传输数据。
- 数据捕获、编码和发送 :当用户打开设备之后,需要捕获这些数据并对它们进行编码,因为原始数据的数据量太大,然后需要将编码后的数据通过连接传输出去。
- 数据接收、解码和显示 :接收来自其他方的数据流并进行解码,然后显示出来,这个需求跟播放媒体文件的需求比较类似。
根据上面的解释,不难理解图11-20所描述的过程,结合这些主要组成部分,构成了一个比较完整的音视频通信过程。
图11-20 使用WebRTC技术的视频通信详细过程
下面了解一下规范中如何针对上面的描述来定义相应的JavaScript接口的。根据目前W3C推荐的规范草案,主要包括以下几个部分。
- Media Capture and Streams规范和WebRTC对它的扩展,这个主要是依赖摄像头和麦克风来捕获多媒体流,WebRTC对它进行扩展,使得多媒体流可以满足网络传输用途,也就是“video”元素可以来源于多媒体流而不仅仅是资源文件。
- 点到点的连接,也就是规范中的RTCPeerConnection接口,它能够建立端到端的连接,两者直接通过某种方式传输控制信息,至于方式并没有进行规定。
- RTCDataChannel接口,通过该接口,通信双方可以发送任何类型的消息,例如文本或者二进制数据,这个不是必须的。不过这一功能极大地方便了开发者,其主要思想来源于WebSocket。
11.4.3 实践——一个WebRTC例子
在介绍内部原理之前,笔者希望通过剖析W3C规范中的一个例子来进一步加深对它的理解。示例代码11-5来源于W3C WebRTC规范中的示例代码,笔者稍微作了一些修改以简化理解,并加入注释作进一步说明。
这里主要是点对点的直接通信,当然需要借助于网络提供的NAT服务,其中包括三个文件,第一个是双方共享的JavaScript代码,第二个是发起端的HTML代码,第三个是接收端的HTML代码。通常第二个和第三个可以是一样的,这里为了方便理解作了少许区别。因为代码中已经作了较为详细的讲解,所以后面不再赘述其中的原理。
示例代码11-5 使用WebRTC技术的P2P网络视频通信
JavaScript文件:common.js // 创建消息通道,例如使用XMLHttpRequest或者WebSocket。根据WebRTC规范的说明 // 本身WebRTC不提供双方进行控制信息传输的通道,由开发者自行选择合适的方法 // 这里,简单使用一个函数表示创建了一个通道,该通道包含一个能够发送消息的“send” var signalChannel = createSignalChannel(); // 将ICE的Candidate发送给对方,这个是ICE定义的,主要是ICE协议用来建立连接需 // 要的信息。双方需要交互这个信息 function sendCandidate(candidate) { if (candidate) signalChannel.send(JSON.stringify({ "candidate": candidate })); } function sendDescription() { signalingChannel.send(JSON.stringify({ "sdp": conn.localDescription })); } signalingChannel.onmessage = function (event) { // 如果没有建立连接,需要创建,在这里表明这是接受者端 if (conn == null) start(); // 从控制信息中获取信息内容 var message = JSON.parse(event.data); // 如果信息类型是设置Description相关的,就调用setRemoteDescription if (message.sdp) { conn.setRemoteDescription(new RTCSessionDescription(message.sdp), function () { // 如果受到一个请求(offer),需要答复它,这里应该是接收方处理的 if (conn.remoteDescription.type == "offer") { conn.createAnswer(function(desc) { conn.setLocalDescription(desc, sendDescription); }); } else if (message.candidate) { conn.addIceCandidate(new RTCIceCandidate(message.candidate)); } }; // 用来存放RTCPeerConnection对象 var conn = null; // 用来显示从自身设备捕获的多媒体流 var selfView = document.getElementById("selfView"); // 用来显示从对方传送过来的多媒体流 var remoteView = document.getElementById("remoteView"); // 开始创建连接等 function start() { // 创建连接 conn = new RTCPeerConnection({ "iceServers": [{ "url": "stun:stun. example.org" }] }); // 保存从自身捕获的多媒体流 var capturedStream = null; // 捕获音视频 navigator.getUserMedia({ "audio": true, "video": true }, function (stream) { // 将捕获的多媒体流使用“video”元素播放出来 selfView.src = URL.createObjectURL(stream); capturedStream = stream; } // 将多媒体流加入连接 conn.addStream(capturedStream); // 接收到ICE Candidate事件,需要将candidate信息传送给对方 conn.onicecandidate = function (event) { sendCandidate(event.candidate); }; // 这个是由发起者调用,因为接收者不会发送该事件 conn.onnegotiationneeded = function() { // 创建一个Offer,然后发送给接收者 conn.createOffer(function (desc) { conn.setLocalDescription(desc, sendDescription); ); }); // 将远端多媒体流使用“video”元素显示出来 conn.onaddstream = function (evt) { remoteView.src = URL.createObjectURL(evt.stream); }; } 发起者HTML文件节选: <video id="selfView" autoplay ></video> <video id="remoteView" autoplay></video> <script src='common.js'></script> <script> // 上面的两个"video"元素分别用来显示自己捕获的多媒体流和对方的多媒体流 // 实际情况中,可能是某个用户作为发起者单击了“开始”按钮,启动音视频通信 start(); </script> 接受者HTML文件节选: <video id="selfView" autoplay ></video> <video id="remoteView" autoplay></video> <script src='common.js' type='javascript'></script>
相信通过上面的代码介绍,读者应该理解使用WebRTC构建一个P2P视频通信的基本过程,这其中网络连接的部分主要基于ICE协议(NAT)和SDP协议,以及一些支持NAT的辅助设施(STUN和URN),有兴趣的读者可以自行查阅相关技术文档。
4.4 WebKit和Chromium的实现
下面来看一看WebKit和Chromium是如何支持WebRTC规范的。笔者首先需要澄清一下关于WebRTC的两种解释,本节中会有两种WebRTC用法:一种是指WebRTC这项技术和规范;另一种是WebRTC这个项目,它是支持WebRTC规范的一个开源项目。默认情况下是指前者,如果是指WebRTC这个开源项目,笔者会明确指出。
下面来了解一下从webrtc.org上介绍的关于支持WebRTC技术的内部框架和功能模块,图11-21来源于“http://www.webrtc.org/reference/architecture”的架构图,但是缩减了其中一些部分。这里所示的是实现了WebRTC功能的开源项目架构图。
图11-21中主要包括三大方面,即语音、视频和传输,它们三个构成了WebRTC的主要组成部分。其中iSAC(internet Speech Audio Codec)和iLBC(internet Low Bitrate Codec)是两种不同的音频编码格式,是为了适应互联网的语音传输要求而存在的,前者是针对带宽比较大的情况,后者针对带宽较小的情况,目前都是可以免费使用的。其中VP8同样是Google提供免费视频格式,前面介绍过了。传输部分主要是加入了对前面协议的支持模块。在会话管理中,主要使用一个开源项目libjingle来进行管理。下面灰色部分主要是WebRTC工作时依赖的下层功能的接口,在Chromium浏览器中,它会提供相应接口和功能给WebRTC使用。
图11-21 WebRTC项目的架构图
上面是WebRTC开源项目的架构图,在Chromium中,通常使用WebRTC项目来完成WebRTC规范的功能,并使用libjingle项目来建立点到点的连接。所以,Chromium主要的目的是将WebRTC和libjingle的能力桥接到浏览器中来,先看WebRTC规范中建立连接所需要的相关基础设施,图11-22是WebKit、Chromium及Chromium中使用libjingle的类的层次图。
图11-22 WebKit和Chromium建立连接的基础设施
基础设施主要分成三个层次,首先是WebKit,也就是最上面的部分。该部分最上面的类是RTCPeerConnection,从名字就可以猜出,该类是对WebRTC连接的接口类,实际上它就是从规范中定义的RTCPeerConnection接口文件生成的基本框架,当然真正和JavaScript引擎打交道还需要一个桥接类。该桥接类包含一个实际实现的连接类句柄m_peerHandler,它是这个连接所包含的本地多媒体流和远端对方的多媒体流。读者可以想象一下视频会议的场景,首先Webkit需要将本地的多媒体流收集起来,通过连接传输给对方,本地可以选择是否通过“video”播放。同时需要接收从对方传输过来的多媒体流,这也是WebRTC的主要部分。当然还包括DataChannel相关对象,这里没有标出。
至于接下来的部分就是WebKit的实现类,该类能够满足RTCPeerConnection的功能要求,但是它需要通过不同移植的实现才能完成,因为本身WebKit的WebCore并没有这样的能力。在WebKit的Chromium中同样定义了两个类WebRTCPeerConnectionHandler和WebRTCPeerConnectionHandlerClient,根据WebKit的类名定义方式,前者是需要Chromium来实现,而后者则是由Chromium调用,并由WebKit来实现的,这里主要是应用连接事件的监听函数,所以WebKit能够将它们传递给JavaScript引擎。
之后是Chromium的实现类。RTCPeerConnectionHandler类继承自WebKit的Chromium移植的接口类,并做了具体的实现,这就是content::RTCPeerConnectionHandler,它同时集成自PeerConnectionHandleBase类,而该类拥有了支持建立连接所需的能力,当然它是依赖于libjingle项目提供的连接能力。
libjingle提供了建立和管理连接的能力,支持透过NAT和防火墙设备、代理等建立连接。libjingle不仅支持点到点的连接,也支持多用户连接。同时还包含了连接所使用的MediaStream接口,这是因为Chromium本身不直接使用WebRTC项目提供的接口,而是调用libjingle来建立连接,并使用libjingle提供的MediaStream接口,而libjingle本身则会使用WebRTC项目的音视频处理引擎。
接下来要介绍的是多媒体流,它需要一个非常复杂的框架,首先来看WebKit是如何支持getUserMedia这个接口的。图11-23描述了WebKit,以及WebKit的Chromium移植中所定义的接口,图中虚线上半部分是WebKit中的类,下半部分是Chromium中的类。
图11-23 WebKit支持多媒体流的基础设施
最上层是WebKit支持多媒体流编程接口提供的具体实现类,如NavigatorMediaStream类,而直接同V8 JavaScript引擎桥接的类是V8NavigatorUser-MediaSuccessCallback,它是一个绑定类。因为getUserMedia接口主要是返回一个MediaStream对象,而MediaStream类可以提供众多访问数据流的接口,而连接的目的就是需要将MediaStream对应的多媒体流传输出去。图中UserMediaRequest类负责请求创建一个MediaStream对象。在WebKit的Chromium移植中,定义WebUserMediaClient为一个接口类,Chromium需要新建子类来实现这一功能,这就是Chromium中的MediaStreamImpl类,它在后面的介绍中还会出现。
WebKit中使用MediaStreamRegistry类来注册和管理对应的类,管理类根据ID信息来识别各个多媒体数据流。在接口层中,Chromium移植使用WebMediaStream类来表示多媒体流,使用WebMediaStreamRegistry类来表示注册管理类。
下面的问题是MediaStream接口需要提供各种事件给网页,因为很多实际的工作是在Chromium中来完成的,所以MediaStreamImpl会将这些事件从Chromium传递给WebKit。同时因为Chromium的多进程和沙箱模型,一些工作需要在Browser进程中完成,所以可以见到如图11-24所描述的跨进程的基础设施。
图11-24 Chromium支持MediaStream接口基础设施
IPC左侧部分是Browser进程中的两个主要类,分别是消息处理类和MediaStream的管理类,该管理类知道MediaStream对应的网页是什么,并将事件(如创建和销毁等)传回Renderer进程。右侧是消息派发类,主要帮助MediaStreamImpl类来完成与Browser进程相关的MediaStream消息的传递。
实际上,MediaStream可以表示本地的多媒体流,也可以表示远端的多媒体流。对于本地的多媒体流,需要音频和视频的捕获机制,同时使用上面建立的连接传输给远端。对于远端的多媒体流,需要使用连接来接收数据,并使用到音频和视频的解码能力。下面分成四个部分来分别介绍。
首先是音频的捕获机制,图11-25描述了该机制使用的主要类。当网页需要创建多媒体流的时候,MediaStreamImpl会创建音频捕获类,也就是WebRtcAudioCapturer类,如图中上半部分。因为捕获音频需要音频输入设备,所以使用AudioDeviceFactory工厂类创建一个逻辑上的AudioInputDevice对象。另外一个重要的类是WebRtcAudioDeviceImpl,用来表示音频的设备,该类继承自WebRtcAudioDeviceNotImpl类。这其实是继承自libjingle和WebRTC项目中的抽线接口的一个桥接类,用来表示它们需要的音频设备,当然包括输入设备。同样因为Renderer进程不能访问音频输入设备,所以需要IPC来完成这一功能,Browser进程的AudioInputController会控制和访问设备,而AudioInputDeviceManager可以管理和控制所有输入设备。
图11-25 Chromium本地捕获音频的基础设施
其次是处理远端多媒体流中的音频解码和播放功能。图11-26是Chromium处理远端音频流所需要的一些主要类及关系图。这里不涉及连接如何接收传输的数据,因为Chromium是使用libjingle和WebRTC项目来完成连接的功能。Chromium使用WebRtcAudioRender类来完成音频渲染,该桥接类会被WebMediaPlayer作为渲染音频的实现类,其作用主要是将MediaStream的数据同实际的音频渲染类结合起来。
图11-26 Chromium处理远端音频基础设施
再次是从视频输入设备请求捕获本地视频流,图11-27是该功能依赖的一些主要类。
图11-27 Chromium本地捕获视频的基础设施
首先看虚线右侧Renderer进程中的设施。同样是MediaStreamImpl类发起,由辅助工厂类MeidaStreamDependencyFactory帮助创建一个RtcVideoCapaturer,用来获取视频。该类有两个作用,其一是实现libjingle和WebRTC项目中的接口类,因为需要视频输入的实现,这个同音频部分非常类似。另外就是将调用请求交给一个代理类来完成,这就是RtcVideoCaptureDelegate类。下面的就比较好理解了,分别是管理类VideoCaptureImplManager和视频捕获类VideoCaptureImpl,并包括一个发送消息到Browser进程的辅助类。在Browser进程使用控制类VideoCaptureController来获取VideoCaptureDevice,该类会使用摄像头等视频输入设备,效果都相对比较简单直观。
最后是处理远端多媒体流中的视频解码和播放功能。当MediaStreamImpl对象接收到远端的多媒体流之后,它会使用WebRTC来对视频数据进行解码,因为可以使用硬件来解码,所以提高了处理的性能。
图11-28 Chromium处理远端视频基础设施
把WebRTC整个过程综合起来分析,可以有一种更为整体和直观的感受,如图11-29所示。读者可以结合这个图来回味一下之前所描述的众多细节。图中没有本地捕获的音视频的播放过程,因为它们不是必须的,而且同播放远端多媒体流类似,这里便不再赘述。
图11-29 WebKit、Chromium、libjingle和WebRTC等项目支持WebRTC规范的框架
目前,在Chrome浏览器中,基于WebRTC的网页已经可以在移动操作系统(如Android)上获得支持,移动领域的进展必将推动该技术的进一步发展。回顾本章介绍的多媒体各个方面的技术,读者可以看出,HTML5技术不仅引入了多媒体的支持,而且加入了之前插件也不能支持的众多更新更复杂的技术,但是却极大提升了HTML5的应用范围。