智能会议系统(30)---WebRTC学习之一:开篇

WebRTC学习之一:开篇

一.无插件的实时通讯

       想像一下,如果你的手机、电视、电脑都可以通过一个平台进行通信,想像一下,你可以在Web应用中轻松地加入视频聊天和p2p数据分享,这就是WebRTC的愿景。
       想试一试吗?WebRTC现在已经被集成到Chrome、Opera和Firefox,在apprtc.appspot.com有个简单的视频聊天应用可供测试。
1.在Chrome、Opera或Firefox中打开apprtc.appspot.com
2.点击允许按钮允许应用启用你的摄像头。
3.在新的选项卡中打开页面底部显示的URL,当然能在另外一台电脑上打开该URL会更好。

关于这个应用的具体教程详见“一个简单的视频聊天客户端”章节。

二.快速开始

       如果你没有时间阅读这篇文章,想直接编码,你可以这样:
1.看一看Gooogle关于WebRTC的幻灯片(here)。
2.你果你没有用过getUserMedia,要先学习一下它,教程:HTML5 Rocks article,例子:simpl.info/gum
3.掌握RTCPeerConnection API,教程:本文的代码段,例子:simpl.info/pc,这个例子在一个单独的网页中实现了WebRTC。
4.了解一下WebRTC信令服务、防火墙和NAT转发,教程:apprtc.appspot.com
5.实在等不及了,可以通过这20+ demos练习WebRTC的JavaScript API。
6.如果有什么问题,可以试试问题帮助页面test.webrtc.org
       或者你可以直接跳到这一步:在WebRTC codelab上一步一步的学习如何构建一个完整的视频聊天应用程序,包括一个简单的信令服务器。

三.关于WebRTC的小故事

       其实一个Web开发的终极挑战就是通过音频和视频进行实时通信,视频通信应该像文本通信一样自然,如果没有它,我们在用户交互方面的创新能力会受到限制。
       在过去,实时通信都比较复杂,需要非常丰富的音频和视频技术才能进行开发。 完整的实现实时通信需要整合大量的数据和服务,在Web上实现尤其困难。
       2008年,Gmail视频聊天火了。2011年谷歌发布了Hangouts,收购了GIPS,GIPS为RTC开发了许多组件,比如编码和回声消除技术。谷歌开源了GIPS的相关技术,并且与IETF和W3C等标准化组织达成了行业共识。2011年5月爱立信构建了第一个WebRTC应用。
       WebRTC目前实现了实时、无插件的音频、视频和数据通信,我们迫切需要它,因为:
1.许多web service在使用RTC,但是需要下载原生app或者插件,比如Skype、Facebook和谷歌Hangouts。
2.下载、安装和升级插件非常繁琐,而且容易出错。
3.插件不容易发现问题,测试很困难,大部分都需要授权,开发成本太高。
       WebRTC项目的宗旨是:API是开源、免费的、标准的、可内建于浏览器且比其他现存的技术更加高效。

四.WebRTC使用现状

       目前WhatsAPP、Facebook Messenger等应用都使用了WebRTC,不仅如此WebRTC还出现在其他平台中,比如TokBox。WebRTC可以被整合到WebKitGTK+或者Qt原生应用中。
WebRTC实现了下列三个API:

1.MediaStream (别名getUserMedia)

2.RTCPeerConnection

3.RTCDataChannel
       getUserMedia可用于Chrome、Opera、Firefox和Edge。你可以看看这个跨浏览器的demo和Chris Wilson的amazing examples,这些例子使用getUserMedia作为音频的输入。
       RTCPeerConnection可用于Chrome、Opera和Firefox。经过几次迭代之后RTCPeerConnection被Chrome和Opera实现为webkitRTCPeerConnection,被Firefox实现为mozRTCPeerConnection。其他的实现已经被废弃。当标准化进程稳定之后,这两个实现名字的前缀会被移除。Chromium的一个超级简单的RTCPeerConnection实现在GitHub上,大量的视频聊天应用在apprtc.appspot.com
       RTCDataChannel可用于Chrome、Opera和Firefox。在GitHub上有关于数据通道的例子,可以去实践一下。

五.我的第一个WebRTC应用

       开发WebRTC应用需要做好下列准备:
1.获取音视频流或者其他数据
2.得到网络信息,如IP地址和端口,通过网络和其它WebRTC客户端交换数据,解决NATs/防火墙穿透问题。
3.协调信令通信来报告错误、启动或关闭会话。
4.交换媒体和客户端信息,比如分辨率和编解码参数。
5.传输音视频流或者其他数据。
       为了实现数据流通信,WebRTC实现了下列API:
1.MediaStream从设备获取数据流,比如说摄像头和麦克风。
2.RTCPeerConnection:音视频通话,包括设备加密和带宽管理。
3.RTCDataChannel:p2p通信。

六.MediaStream (别名getUserMedia)

       MediaStream API代表媒体流的同步。比如,从摄像头和麦克风获取的媒体流具有同步视频和音频轨道。不要将这里的MediaStream轨道和<track>元素混淆,它们是完全不同的概念。
理解MediaStream最简单的方法如下:
1.在Chrome或Opera中打开例子https://webrtc.github.io/samples/src/content/getusermedia/gum
2.打开控制台
3.检查stream变量,该变量是全局的。
       每个MediaStream都有输入,即navigator.getUserMedia();也有输出,被传递到video元素或RTCPeerConnection
getUserMedia()方法有三个参数:
1.一个约束对象。
2.一个成功的回调,如果成功会回传一个MediaStream。
3.一个失败的回调,如果失败会回传一个error对象。
       每个MediaStream都有一个label,比如'Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ',getAudioTradks()和getAudioTracks()方法会回传一个MediaStreamTracks对象的数组。
在例子https://webrtc.github.io/samples/src/content/getusermedia/gum中,stream.getAudioTracks()回传了一个空数组(因为没有音频),假设摄像头正常工作并连接,stream.getVideoTracks()回传一个MediaStreamTracks对象的数组。数组中的每个MediaStreamTracks对象包含一种媒体(‘video’或‘audio’)和一个label(比如'FaceTime HD Camera (Built-in)'),而且还代表了一个或多个音视频的数据通道。在这个例子中,只有一个视频轨道,没有音频。当然,很容易就能扩展到其他情况。
       在Chrome或Opera中, URL.createObjectURL()方法将MediaStream转换成Blob URL,该Blob URL可以设置为video元素的输入(在Firefox和Opera中,视频源可以通过数据流本身设置)。版本M25开始,基于Chromium的浏览器(Chrome和Opera)允许来自getUserMedia的音频数据传递到aduio或video元素。
       getUserMedia还可用作Web Audio API的输入节点

 
  1. function gotStream(stream) {

  2. window.AudioContext = window.AudioContext || window.webkitAudioContext;

  3. var audioContext = new AudioContext();

  4.  
  5. // Create an AudioNode from the stream

  6. var mediaStreamSource = audioContext.createMediaStreamSource(stream);

  7.  
  8. // Connect it to destination to hear yourself

  9. // or any other node for processing!

  10. mediaStreamSource.connect(audioContext.destination);

  11. }

  12. navigator.getUserMedia({audio:true}, gotStream);

       在manifest中添加audioCapture和videoCapture权限可以在加载的时候得到(仅一次)授权,毕竟加载之后用户不会再有对摄像头或麦克风的访问请求。
       最终的目的是使MediaStream适用于任何数据源,不仅限于摄像头和麦克风,还包括来自磁盘或者传感器等输入设备二进制数据。
       需要注意的是getUserMedia()必须在服务器上使用,而不是本地文件中,否则的话将会抛出权限的错误PERMISSION_DENIED。
       getUserMedia()通常和其他的JavaScript API及库一起使用:
Webcam Toy是一个photobooth应用,它使用WebGL来添加一些特效,让用户可以共享照片或是保存到本地。
FaceKat是一个人脸追踪的游戏,使用headtrackr.js。

ASCII Camera使用Canvas API来生成ASCII码的图片。

七.约束

       Constraints已经在Chrome、FireFox和Opera中实现了。通过约束可以设置getUserMedia()和RTCPeerConnection的addStream()获取视频的分辨率,约束的实现是为了通过applyConstraints()方法控制视频高度和宽度的比例、帧率、和正反摄像头模式等等……
       这里有一个例子:https://webrtc.github.io/samples/src/content/getusermedia/resolution/
       一个陷阱:getUserMedia约束设置在浏览器的一个标签中,会约束之后打开的所有标签。设置一个非法的值会提示以下错误:

 
  1. navigator.getUserMedia error:

  2. NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}

八.屏幕和标签捕获

       Chrome应用可以通过chrome.tabCapture和chrome.desktopCapture这两个API实时分享浏览器标签或者整个桌面。桌面捕获的例子:WebRTC samples GitHub repository。更多关于屏幕录制、编码的信息和有参考:Screensharing with WebRTC
       在Chrome中,可以将屏幕捕获当做MediaStream的数据源,此时使用的是实验性的chromeMediaSource约束,一个例子:this demo。需要注意的是屏幕捕获功能需要HTTPS支持,并且只用于开发中,通过一个命令行标志来启用。

九.信令:会话控制,网络和媒体信息

       WebRTC使用RTCPeerConnection在浏览器(别名peer)之间互通数据流,但是需要一种机制去协调通信或者发送控制消息,这个过程被称为信令。WebRTC没有指定信令方法和协议,信令不是RTCPeerConnection API的一部分。
       因此,WebRTC应用的开发者可用选择其擅长的消息协议,比如SIP或XMPP,或者其他合适的双工通信协议。
apprtc.appspot.com这个例子使用XHR和Channel API作为信令机制。codelab是我们通过Socket.io构建,运行在Node server上的应用。
       信令通常用于交互三类信息:
1.会话控制消息;初始化或者关闭通信,报告错误。
2.网络信息:对于外部而言,我的IP地址和端口是什么?
3.媒体信息:什么编码和分辨率浏览器可以处理,我的浏览器要和谁通信。
       在p2p的流传输之前,必须通过信令成功的交换信息。
       假如Alice想和Bob通信,这里有个简单的例子来自WebRTC W3C Working Draft,展示了实际的信令处理过程。例子中假设存在某种信令机制,该机制通过createSignalingChannel()方法创建。注意在Chrome和Opera中,RTCPeerConnection是带有前缀的。

 
  1. var signalingChannel = createSignalingChannel();

  2. var pc;

  3. var configuration = ...;

  4.  
  5. // run start(true) to initiate a call

  6. function start(isCaller) {

  7. pc = new RTCPeerConnection(configuration);

  8.  
  9. // send any ice candidates to the other peer

  10. pc.onicecandidate = function (evt) {

  11. signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));

  12. };

  13.  
  14. // once remote stream arrives, show it in the remote video element

  15. pc.onaddstream = function (evt) {

  16. remoteView.src = URL.createObjectURL(evt.stream);

  17. };

  18.  
  19. // get the local stream, show it in the local video element and send it

  20. navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {

  21. selfView.src = URL.createObjectURL(stream);

  22. pc.addStream(stream);

  23.  
  24. if (isCaller)

  25. pc.createOffer(gotDescription);

  26. else

  27. pc.createAnswer(pc.remoteDescription, gotDescription);

  28.  
  29. function gotDescription(desc) {

  30. pc.setLocalDescription(desc);

  31. signalingChannel.send(JSON.stringify({ "sdp": desc }));

  32. }

  33. });

  34. }

  35.  
  36. signalingChannel.onmessage = function (evt) {

  37. if (!pc)

  38. start(false);

  39.  
  40. var signal = JSON.parse(evt.data);

  41. if (signal.sdp)

  42. pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));

  43. else

  44. pc.addIceCandidate(new RTCIceCandidate(signal.candidate));

  45. };

       首先,Alice和Bob交换网络信息,‘finding candidates’表示通过ICE framework查找网络接口和端口。
1.Alice创建一个RTCPeerConnection对象,该对象内置onicecandidate处理器。
2.这个处理器在网络candidate生效时开始运行。
3.Alice通过信令通道发送序列化的数据给Bob,信令通道可以是WebSocket或者其他机制。
4.当Bob收到Alice的candidate消息后,调用addIceCandidate将candidate添加到远端描述。
        WebRTC客户端(别名peer,这里指Alice和Bob)需要明确并交换本地和远程音视频媒体信息,比如分辨率和编码格式。交换媒体信息的信令,是通过交换会话描述协议(SDP)来实现的。
1.Alice调用了RTCPeerConnection的createOffer()方法,它的回调参数传入的是RTCSessionDescription(Alice的本地会话描述)。
2.在回调中,Alice调用setLocalDescription()方法设置了本地会话描述,然后将该会话描述通过信令通道发送给Bob。注意,RTCPeerConnection并不会采集candidate直到setLocalDescription()被调用。
3.Bob使用setRemoteDescription()方法将Alice发送给他的会话描述设置为远程会话描述。
4.Bob调用了RTCPeerConnection的createAnswer()方法,并传入它从Alice接收到的远程会话描述,此时一个与Alice兼容的本地会话产生了。createAnswer()的回调参数传入的是RTCSessionDescription(Bob将它设置为本地会话描述并发送给Alice)。
5.当Alice收到Bob的会话描述,她使用setRemoteDescription()方法将其设置为远程会话描述。
6.Ping
       RTCSessionDescription对象遵从SDP(Session Description Protocol),一个SDP对象看起来如下所示:

 
  1. v=0

  2. o=- 3883943731 1 IN IP4 127.0.0.1

  3. s=

  4. t=0 0

  5. a=group:BUNDLE audio video

  6. m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126

  7.  
  8. // ...

  9.  
  10. a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810

       交换网络和媒体信息可以同时进行,但这两个过程必须在音视频流开始传输之前完成。

      上述的offer/answer架构被称为JSEP(JavaScript Session Establishment Protocol),JSEP架构如下所示:

      一旦信令过程成功,就可以直接进行Caller和callee之间p2p的数据流传输了。

十.RTCPeerConnection

RTCPeerConnection是WebRTC的组件,用来稳定高效的处理端对端的数据流通信。

下图是WebRTC的架构图,标明了RTCPeerConnection扮演的角色。你可能注意到了,绿色部分是相当复杂的。

       从JavaScript的角度来看,理解这个图最重要的是理解RTCpeerConnection这一部分。WebRTC对编解码器和协议做了大量的工作,使实时通信成为可能,甚至在一些不可靠的网络中:
1.包补偿
2.回声消除
3.自适应带宽
4.视频抖动缓冲
5.自动增益控制
6.噪声抑制
7.图像清除
       章节九中的例子从信令的角度进行了讲解,下面我们将学习两个WebRTC应用;一个简单的演示了RTCPeerConnection,另一个是功能齐全的视频聊天客户端。

十一.无服务器的RTCPeerConnection

       下面的代码来自https://webrtc.github.io/samples/src/content/peerconnection/pc1,包含基于网页的本地和远程RTCPeerConnection。这个例子中caller和callee在同一个网页中,能更加清晰的展示RTCPeerConnection API的工作流程,因为RTCPeerConnection对象之间可以直接交换数据和消息,不需要通过中继信道机制。
       一个陷阱:RTCPeerConnection()第二个约束类型的参数是可选的,它与getUserMedia()中使用的约束类型不同。
       本例中pc1表示本地端(caller),pc2表示远程端(callee)
caller
1.创建一个RTCPeerConnection,并通过getUserMedia()添加数据流。

 
  1. // servers is an optional config file (see TURN and STUN discussion below)

  2. pc1 = new webkitRTCPeerConnection(servers);

  3. // ...

  4. pc1.addStream(localStream);

2.创建一个offer,并将它设置为pc1的本地会话描述,设置为pc2的远程会话描述。可以直接在代码中设置,不需要使用信令,因为caller和callee在同一个网页中。

 
  1. pc1.createOffer(gotDescription1);

  2. //...

  3. function gotDescription1(desc){

  4. pc1.setLocalDescription(desc);

  5. trace("Offer from pc1 \n" + desc.sdp);

  6. pc2.setRemoteDescription(desc);

  7. pc2.createAnswer(gotDescription2);

  8. }

callee
1.创建pc2,接收pc1的数据流,并显示到video元素中

 
  1. pc2 = new webkitRTCPeerConnection(servers);

  2. pc2.onaddstream = gotRemoteStream;

  3. //...

  4. function gotRemoteStream(e){

  5. vid2.src = URL.createObjectURL(e.stream);

  6. }

十二.有服务器的RTCPeerConnection

       实际应用中,WebRTC需要服务器,无论多简单,下面四步是必须的:
1.用户通过交换名字之类的信息发现对方。
2.WebRTC客户端应用交换网络信息。
3.客户端交换媒体信息包括视频格式和分辨率。
4.WebRTC客户端穿透NAT网关和服务器。
       换句话说,WebRTC需要四种类型的服务端功能。
1.用户发现和通信
2.信令
3.NAT/防火墙穿透
4.中继服务器,防止端到端的通信失败
       以上这些不在本文讨论范围之内。可以说基于STUNTURN协议的ICE框架,使得RTCPeerConnection处理NAT穿透和其他网络难题成为可能。
       ICE框架用于端到端的连接,比如说两个视频聊天客户端。起初,ICE尝试通过UDP直接连接两端,这样可以保证低延迟。在这个过程中,STUN服务器有一个简单的任务:使NAT后边的端能找到它的公网地址和端口(谷歌有多个STUN服务器,其中一个用在了apprtc.appspot.com例子)。

       如果UDP传输失败,ICE会尝试TCP:首先是HTTP,然后才会选择 HTTPS。如果直接连接失败,通常因为企业的NAT穿透和防火墙,此时ICE使用中继(Relay)服务器。换句话说,ICE首先使用STUN和UDP直接连接两端,失败之后返回中继服务器。‘finding cadidates’就是寻找网络接口和端口的过程。

       WebRTC工程师Justin Uberti在幻灯片2013 Google I/O WebRTC presentation中提供了许多关于ICE、STUN和TURN的信息。

十三.一个简单的视频聊天客户端

       如果你觉得这个例子比较难,你也行会喜欢上我们的WebRTC codelab。那里一步步的介绍了如何建立一个完整的视频聊天应用,包括一个运行于Node server上基于Socket.io的信令服务器。
       apprtc.appspot.com是一个测试WebRTC的好地方,里面有视频聊天的例子,它实现了信令和基于STUN服务器的NAT/防火墙穿透。这个例子使用adapter.js处理不同的RTCPeerConnection和getUserMedia()实现。

       下面我们详细的过一遍代码。

如何开始

这个例子从initialize()函数开始运行。

 
  1. function initialize() {

  2. console.log("Initializing; room=99688636.");

  3. card = document.getElementById("card");

  4. localVideo = document.getElementById("localVideo");

  5. miniVideo = document.getElementById("miniVideo");

  6. remoteVideo = document.getElementById("remoteVideo");

  7. resetStatus();

  8. openChannel('AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/');

  9. doGetUserMedia();

  10. }

       需要注意的是,变量room和openChannel()参数的值都是由Google App Engine应用自身提供的。查看一下index.html template 就知道该赋什么值了。
       这段代码初始化HTML video元素的相关变量,video元素播放来自本地摄像头(localVieo)和远程摄像头(remoteVideo)的视频流。resetStatus()设置了一条状态消息。
       openChannel()函数建立了WebRTC客户端间的消息通道。

 
  1. function openChannel(channelToken) {

  2. console.log("Opening channel.");

  3. var channel = new goog.appengine.Channel(channelToken);

  4. var handler = {

  5. 'onopen': onChannelOpened,

  6. 'onmessage': onChannelMessage,

  7. 'onerror': onChannelError,

  8. 'onclose': onChannelClosed

  9. };

  10. socket = channel.open(handler);

  11. }

关于信令,本例使用的是Google App Engine Channel API,这使得JavaScritp客户端无需轮询就能实现消息传输。

       使用Channel API建立通道的流程大致如下:
1.客户端A生成一个唯一ID。
2.客户端A向Google App Engine应用请求一个通道标识(即openChannel()的参数),并将它的ID传给Google App Engine应用。
3.Google App Engine应用会调用Channel API为客户端ID分配一个通道和一个通道标识。
4.Google App Engine应用将通道标识发给客户端A。

5.客户端A打开socket并监听服务器上建立的通道。

       发送消息的流程大致如下:
1.客户端B给Google App Engine应用发送了一个POST请求,要求升级程序。
2.Google App Engine应用给通道发送一个请求消息。
3.消息经通道传递给客户端A

4.客户端A的onmessage回调函数被调用。

       重申一次,信令传输机制是由开发者选择的。WebRTC并没有指定信令机制。本例的Channel API能被其他的方式取代,比如WebSocket。
       initialize()调用完openChannel()之后,紧接着调用getUserMedia(),这个函数可以检测出浏览器是否支持getUserMedia API。如果一切顺利,onUserMediaSuccess会被调用。

 
  1. function onUserMediaSuccess(stream) {

  2. console.log("User has granted access to local media.");

  3. // Call the polyfill wrapper to attach the media stream to this element.

  4. attachMediaStream(localVideo, stream);

  5. localVideo.style.opacity = 1;

  6. localStream = stream;

  7. // Caller creates PeerConnection.

  8. if (initiator) maybeStart();

  9. }

       这样一来,本地摄像头就能显示在localVideo元素中了。
       此时,initiator被设置成1(直到caller的会话终止),maybeStart()被调用。

 
  1. function maybeStart() {

  2. if (!started && localStream && channelReady) {

  3. // ...

  4. createPeerConnection();

  5. // ...

  6. pc.addStream(localStream);

  7. started = true;

  8. // Caller initiates offer to peer.

  9. if (initiator)

  10. doCall();

  11. }

  12. }

       该函数使用了一种巧妙的结构,可以工作于多个异步回调:maybeStart()可能被任何函数调用,但是只有当localStream被定义、channelReady为true且通信还未开始的情况下,maybeStart()才会运行。因此,当连接还未建立,本地流已经可用,且信令通道已经准备好时,连接才会创建并加载本地视频流。接着started被设置为true。所以连接不会被创建多次 。
RTCPeerConnection: 发起通话
       在maybeStart()中被调用的createPeerConnection(),才是关键所在。

 
  1. function createPeerConnection() {

  2. var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};

  3. try {

  4. // Create an RTCPeerConnection via the polyfill (adapter.js).

  5. pc = new RTCPeerConnection(pc_config);

  6. pc.onicecandidate = onIceCandidate;

  7. console.log("Created RTCPeerConnnection with config:\n" + " \"" +

  8. JSON.stringify(pc_config) + "\".");

  9. } catch (e) {

  10. console.log("Failed to create PeerConnection, exception: " + e.message);

  11. alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");

  12. return;

  13. }

  14.  
  15. pc.onconnecting = onSessionConnecting;

  16. pc.onopen = onSessionOpened;

  17. pc.onaddstream = onRemoteStreamAdded;

  18. pc.onremovestream = onRemoteStreamRemoved;

  19. }

       这段代码的目的是使用STUN服务器建立一个连接,并将onIceCandidate()作为回调函数。然后给RTCPeerConnection每个事件指定处理器(函数):当会话连接或打开,当远程流被加载或移除。在本例中,这些处理器只是记录了状态消息——除了onRemoteStreamAdded(),它给remoteVideo元素设置了数据源。

 
  1. function onRemoteStreamAdded(event) {

  2. // ...

  3. miniVideo.src = localVideo.src;

  4. attachMediaStream(remoteVideo, event.stream);

  5. remoteStream = event.stream;

  6. waitForRemoteVideo();

  7. }

       一旦createPeerConnection()在maybeStart()中被调用,就会发起通话,创建Offer并发送消息给对端。

 
  1. function doCall() {

  2. console.log("Sending offer to peer.");

  3. pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);

  4. }

       这里的offer创建过程类似于上面无信令的例子。但是,除此之外,一条消息被发送到了对端,详见setLocalAndSendMessage():

 
  1. function setLocalAndSendMessage(sessionDescription) {

  2. // Set Opus as the preferred codec in SDP if Opus is present.

  3. sessionDescription.sdp = preferOpus(sessionDescription.sdp);

  4. pc.setLocalDescription(sessionDescription);

  5. sendMessage(sessionDescription);

  6. }

用Channel API传输信令
        当RTCPeerConnection在createPeerConnection()中成功创建的时候,onIceCandidate()回调函数会触发,并发送关于candidate的信息。

 
  1. function onIceCandidate(event) {

  2. if (event.candidate) {

  3. sendMessage({type: 'candidate',

  4. label: event.candidate.sdpMLineIndex,

  5. id: event.candidate.sdpMid,

  6. candidate: event.candidate.candidate});

  7. } else {

  8. console.log("End of candidates.");

  9. }

  10. }

        从客户端到服务器的消息外传,是通过sendMessage()方法内的XHR请求实现的。

 
  1. function sendMessage(message) {

  2. var msgString = JSON.stringify(message);

  3. console.log('C->S: ' + msgString);

  4. path = '/message?r=99688636' + '&u=92246248';

  5. var xhr = new XMLHttpRequest();

  6. xhr.open('POST', path, true);

  7. xhr.send(msgString);

  8. }

        XHR多用于从客户端发送信令消息到服务端,但是某些机制需要用来实现服务端到客户端的消息传输:本例用的是Google App Engine Channel API。来自此API的消息会传递到processSignalingMessage():

 
  1. function processSignalingMessage(message) {

  2. var msg = JSON.parse(message);

  3.  
  4. if (msg.type === 'offer') {

  5. // Callee creates PeerConnection

  6. if (!initiator && !started)

  7. maybeStart();

  8.  
  9. pc.setRemoteDescription(new RTCSessionDescription(msg));

  10. doAnswer();

  11. } else if (msg.type === 'answer' && started) {

  12. pc.setRemoteDescription(new RTCSessionDescription(msg));

  13. } else if (msg.type === 'candidate' && started) {

  14. var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label,

  15. candidate:msg.candidate});

  16. pc.addIceCandidate(candidate);

  17. } else if (msg.type === 'bye' && started) {

  18. onRemoteHangup();

  19. }

  20. }

       如果消息是来自对端的answer(offer的回应),RTCPeerConnection设置远程会话描述,通信开始。如果消息是offer(来自callee),RTCPeerConnection设置远程会话描述,发送answer给callee,然后调用RTCPeerConnection的startIce()方法发起连接。

 
  1. function doAnswer() {

  2. console.log("Sending answer to peer.");

  3. pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);

  4. }

       于是乎,caller和callee都发现了对方并交换相关信息,会话被初始化,实时数据通信可以开始了。
网络技术

       WebRTC目前只实现了一对一的通信,但是可用于更复杂的网络环境:比如,多个peer各自直接通信,即p2p;或者通过MCU(Multipoint Control Unit)服务器来实现流的转发、合成或音视频的录制。

       许多WebRTC应用只演示了浏览器间的通信,但是通过网关服务器可以实现WebRTC与telephones(别名PSTN)和VOIP系统直接的通信。2012年5月,Doubango Telecom开源了sipml5 SIP client,该客户端基于WebRTC和WebSocket,能实现浏览器和IOS或Android应用之间的视频通话。

十四.RTCDataChannel

       除了音频和视频,WebRTC支持其他类型数据的实时通信。
       TCDataChannel API支持p2p低延迟和高吞吐量的二进制数据流交换,这里有个例子:http://webrtc.github.io/samples/src/content/datachannel/datatransfer
       很多领域都潜在地使用到了这个API,比如:
1.游戏
2.远程桌面应用
3.实时文字聊天
4.文件传输
5.分散网络
       充分利用了RTCPeerConnection的多个特性,能实现强大而灵活的p2p通信。
1.利用RTCPeerConnection进行会话设置。
2.通过优先级设置多个同步的channel。
3.可靠和非可靠的语义传递。
4.内建立安全的DTLS和拥塞控制。
5.能用于音视频或其他方面
TCDataChannel API语法与WebSocket类似,包括send()方法和message事件。

 
  1. var pc = new webkitRTCPeerConnection(servers,

  2. {optional: [{RtpDataChannels: true}]});

  3.  
  4. pc.ondatachannel = function(event) {

  5. receiveChannel = event.channel;

  6. receiveChannel.onmessage = function(event){

  7. document.querySelector("div#receive").innerHTML = event.data;

  8. };

  9. };

  10.  
  11. sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});

  12.  
  13. document.querySelector("button#send").onclick = function (){

  14. var data = document.querySelector("textarea#send").value;

  15. sendChannel.send(data);

  16. };

       因为是浏览器间的直接通信,所以RTCDataChannel要比WebSocket快得多,即使通信用到了中继服务器。
       RTCDataChannel可用于Chrome、Opera和Firefox。出色的Cube Slam游戏使用TCDataChannel API来交换游戏状态:是敌还是友!Sharefest演示了通过RTCDataChannel分享文件,peerCDN提供了WebRTC如何实现p2p内容分发的一种思路。

       更多关于RTCDataChannel的信息,可以参考IETF的draft protocol spec

十五.安全

       实时通信应用或插件会在许多方面忽视了安全性:

1.浏览器之间、浏览器与服务器之间的音视频或其他数据没有加密。
2.应用在用户没有察觉的情况下录制和分发音视频。
3.恶意软件或病毒可能入侵了正常的插件或应用。
       WebRTC的许多特性可以避免这些问题:
1.WebRTC采用类似DTLSSRTP的安全协议。
2.所有的WebRTC组件强制加密,包括信令机制。
3.WebRTC不是插件:它的组件运行于浏览器沙盒,不是独立的一个进程,这些组件不需要单独安装,且随着浏览器更新。
4.摄像头和麦克风的访问必须经过明确准许,当摄像头和麦克风运行时,界面上会清楚的显示出来。

       关于流媒体安全的讨论超出了本文的范畴。更多信息可参考IETF的WebRTC Security Architecture

猜你喜欢

转载自blog.csdn.net/zhangbijun1230/article/details/81777062