WebRTC 全称为:Web Real-Time Communication。它是为了解决 Web 端无法捕获音视频的能力,并且提供了 peer-to-peer(就是浏览器间)的视频交互。实际上,细分看来,它包含三个部分:

  • MediaStream:捕获音视频流
  • RTCPeerConnection:传输音视频流(一般用在 peer-to-peer 的场景)
  • RTCDataChannel: 用来上传音视频二进制数据(一般用到流的上传)

但通常,peer-to-peer 的场景实际上应用不大。对比与去年火起来的直播业务,这应该才是 WebRTC 常常应用到的地方。那么对应于 Web 直播来说,我们通常需要两个端:

  • 主播端:录制并上传视频
  • 观众端:下载并观看视频

这里,我就不谈观众端了,后面另写一篇文章介绍(因为,这是在是太多了)。这里,主要谈一下会用到 WebRTC 的主播端。 简化一下,主播端应用技术简单可以分为:录制视频,上传视频。大家先记住这两个目标,后面我们会通过 WebRTC 来实现这两个目标。

WebRTC 基本了解

WebRTC 主要由两个组织来制定。

  • Web Real-Time Communications (WEBRTC) W3C 组织:定义浏览器 API
  • Real-Time Communication in Web-browsers (RTCWEB) IETF 标准组织:定义其所需的协议,数据,安全性等手段。

当然,我们初级目标是先关心基本浏览器定义的 API 是啥?以及怎么使用? 然后,后期目标是学习期内部的相关协议,数据格式等。这样循序渐进来,比较适合我们的学习。

WebRTC 对于音视频的处理,主要是交给 Audio/Vidoe Engineering 处理的。处理过程为:

  • 音频:通过物理设备进行捕获。然后开始进行降噪消除回音抖动/丢包隐藏编码
  • 视频:通过物理设备进行捕获。然后开始进行图像增强同步抖动/丢包隐藏编码

最后通过 mediaStream Object 暴露给上层 API 使用。也就是说 mediaStream 是连接 WebRTC API 和底层物理流的中间层。所以,为了下面更好的理解,这里我们先对 mediaStream 做一些简单的介绍。

MediaStream

MS(MediaStream)是作为一个辅助对象存在的。它承载了音视频流的筛选,录制权限的获取等。MS 由两部分构成: MediaStreamTrack 和 MediaStream。

  • MediaStreamTrack 代表一种单类型数据流。如果你用过会声会影的话,应该对轨道这个词不陌生。通俗来讲,你可以认为两者就是等价的。
  • MediaStream 是一个完整的音视频流。它可以包含 >=0 个 MediaStreamTrack。它主要的作用就是确保几个轨道是同时播放的。例如,声音需要和视频画面同步。

这里,我们不说太深,讲讲基本的 MediaStream 对象即可。通常,我们使用实例化一个 MS 对象,就可以得到一个对象。

// 里面还需要传递 track,或者其他 stream 作为参数。 // 这里只为演示方便 let ms = new MediaStream(); 

我们可以看一下 ms 上面带有哪些对象属性:

  • active[boolean]:表示当前 ms 是否是活跃状态(就是可播放状态)。
  • id[String]: 对当前的 ms 进行唯一标识。例如:“f61641ec-ee78-4317-9415-58acac066a4d”
  • onactive: 当 active 为 true 时,触发该事件
  • onaddtrack: 当有新的 track 添加时,触发该事件
  • oninactive: 当 active 为 false 时,触发该事件
  • onremovetrack: 当有 track 移除时,触发该事件

它的原型链上还挂在了其他方法,我挑几个重要的说一下。

  • clone(): 对当前的 ms 流克隆一份。该方法通常用于对该 ms 流有操作时,常常会用到。

前面说了,MS 还可以其他筛选的作用,那么它是如何做到的呢? 在 MS 中,还有一个重要的概念叫做: Constraints。它是用来规范当前采集的数据是否符合需要。因为,我们采集视频时,不同的设备有不同的参数设置。常用的为:

{     "audio": true,  // 是否捕获音频     "video": {  // 视频相关设置         "width": {             "min": "381", // 当前视频的最小宽度             "max": "640"          },         "height": {             "min": "200", // 最小高度             "max": "480"         },         "frameRate": {             "min": "28", // 最小帧率              "max": "10"         }     } } 

那我怎么知道我的设备支持的哪些属性的调优呢? 这里,可以直接使用 navigator.mediaDevices.getSupportedConstraints() 来获取可以调优的相关属性。不过,这一般是对 video 进行设置。了解了 MS 之后,我们就要开始真正接触 WebRTC 的相关 API。我们先来看一下 WebRTC 基本API。

WebRTC 的常用 API 如下,不过由于浏览器的缘故,需要加上对应的 prefix:

W3C Standard           Chrome                   Firefox -------------------------------------------------------------- getUserMedia           webkitGetUserMedia       mozGetUserMedia RTCPeerConnection      webkitRTCPeerConnection  RTCPeerConnection RTCSessionDescription  RTCSessionDescription    RTCSessionDescription RTCIceCandidate        RTCIceCandidate          RTCIceCandidate 

不过,你可以简单的使用下列的方法来解决。不过嫌麻烦的可以使用 adapter.js 来弥补

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia 

这里,我们循序渐进的来学习。如果想进行视频的相关交互,首先应该是捕获音视频。

捕获音视频

在 WebRTC 中捕获音视频,只需要使用到一个 API,即,getUserMedia()。代码其实很简单:

navigator.getUserMedia = navigator.getUserMedia ||     navigator.webkitGetUserMedia || navigator.mozGetUserMedia;  var constraints = { // 设置捕获的音视频设置   audio: false,   video: true };  var video = document.querySelector('video');  function successCallback(stream) {   window.stream = stream; // 这就是上面提到的 mediaStream 实例   if (window.URL) {     video.src = window.URL.createObjectURL(stream); // 用来创建 video 可以播放的 src   } else {     video.src = stream;   } }  function errorCallback(error) {   console.log('navigator.getUserMedia error: ', error); } // 这是 getUserMedia 的基本格式 navigator.getUserMedia(constraints, successCallback, errorCallback); 

详细 demo 可以参考:WebRTC。不过,上面的写法比较古老,如果使用 Promise 来的话,getUserMedia 可以写为:

navigator.mediaDevices.getUserMedia(constraints).     then(successCallback).catch(errorCallback); 

上面的注释大概已经说清楚基本的内容。需要提醒的是,你在捕获视频的同时,一定要清楚自己需要捕获的相关参数。

有了自己的视频之后,那如何与其他人共享这个视频呢?(可以理解为直播的方式) 在 WebRTC 中,提供了 RTCPeerConnection 的方式,来帮助我们快速建立起连接。不过,这仅仅只是建立起 peer-to-peer 的中间一环。这里包含了一些复杂的过程和额外的协议,我们一步一步的来看下。

WebRTC 基本内容

WebRTC 利用的是 UDP 方式来进行传输视频包。这样做的好处是延迟性低,不用过度关注包的顺序。不过,UDP 仅仅只是作为一个传输层协议而已。WebRTC 还需要解决很多问题

  1. 遍历 NATs 层,找到指定的 peer
  2. 双方进行基本信息的协商以便双方都能正常播放视频
  3. 在传输时,还需要保证信息安全性

整个架构如下:

上面那些协议,例如,ICE/STUN/TURN 等,我们后面会慢慢讲解。先来看一下,两者是如何进行信息协商的,通常这一阶段,我们叫做 signaling

signaling 任务

signaling 实际上是一个协商过程。因为,两端进不进行 WebRTC 视频交流之间,需要知道一些基本信息。

  • 打开/关闭连接的指令
  • 视频信息,比如解码器,解码器的设置,带宽,以及视频的格式等。
  • 关键数据,相当于 HTTPS 中的 master key 用来确保安全连接。
  • 网关信息,比如双方的 IP,port

不过,signaling 这个过程并不是写死的,即,不管你用哪种协议,只要能确保安全即可。为什么呢?因为,不同的应用有着其本身最适合的协商方法。比如:

  • 单网关协议(SIP/Jingle/ISUP)适用于呼叫机制(VoIP,voice over IP)。
  • 自定义协议
  • 多网关协议

我们自己也可以模拟出一个 signaling 通道。它的原理就是将信息进行传输而已,通常为了方便,我们可以直接使用 socket.io 来建立 room 提供信息交流的通道。

PeerConnection 的建立

假定,我们现在已经通过 socket.io 建立起了一个信息交流的通道。那么我们接下来就可以进入 RTCPeerConnection 一节,进行连接的建立。我们首先应该利用 signaling 进行基本信息的交换。那这些信息有哪些呢? WebRTC 已经在底层帮我们做了这些事情-- Session Description Protocol (SDP)。我们利用 signaling 传递相关的 SDP,来确保双方都能正确匹配,底层引擎会自动解析 SDP (是 JSEP 帮的忙),而不需要我们手动进行解析,突然感觉世界好美妙。。。我们来看一下怎么传递。

// 利用已经创建好的通道。 var signalingChannel = new SignalingChannel();  // 正式进入 RTC connection。这相当于创建了一个 peer 端。 var pc = new RTCPeerConnection({});   navigator.getUserMedia({ "audio": true }) .then(gotStream).catch(logError);  function gotStream(stream) {   pc.addStream(stream);    // 通过 createOffer 来生成本地的 SDP   pc.createOffer(function(offer) {      pc.setLocalDescription(offer);      signalingChannel.send(offer.sdp);    }); }  function logError() { ... } 

那 SDP 的具体格式是啥呢? 看一下格式就 ok,这不用过多了解:

v=0 o=- 1029325693179593971 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE audio video a=msid-semantic: WMS m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:nHtT a=ice-pwd:cuwglAha5fBmGljFXWntH1VN a=fingerprint:sha-256 24:63:EB:DD:18:1B:BB:5E:B3:E8:C5:D7:92:F7:0B:44:EC:22:96:63:64:76:1A:56:64:DE:6B:CE:85:C6:64:78 a=setup:active a=mid:audio a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=inactive a=rtcp-mux ... 

上面的过程,就是 peer-to-peer 的协商流程。这里有两个基本的概念,offeranswer

  • offer: 主播端向其他用户提供其本省视频直播的基本信息
  • answer: 用户端反馈给主播端,检查能否正常播放

具体过程为:

  1. 主播端通过 createOffer 生成 SDP 描述
  2. 主播通过 setLocalDescription,设置本地的描述信息
  3. 主播将 offer SDP 发送给用户
  4. 用户通过 setRemoteDescription,设置远端的描述信息
  5. 用户通过 createAnswer 创建出自己的 SDP 描述
  6. 用户通过 setLocalDescription,设置本地的描述信息
  7. 用户将 anwser SDP 发送给主播
  8. 主播通过 setRemoteDescription,设置远端的描述信息。

不过,上面只是简单确立了两端的连接信息而已,还没有涉及到视频信息的传输,也就是说 UDP 传输。UDP 传输本来就是一个非常让人蛋疼的活,如果是 client-server 的模型话还好,直接传就可以了,但这偏偏是 peer-to-peer 的模型。想想,你现在是要把你的电脑当做一个服务器使用,中间还需要经历如果突破防火墙,如果找到端口,如何跨网段进行?所以,这里我们就需要额外的协议,即,STUN/TURN/ICE ,来帮助我们完成这样的传输任务。

NAT/STUN/TURN/ICE

在 UDP 传输中,我们不可避免的会遇见 NAT(Network address translator)服务器。即,它主要是将其它网段的消息传递给它负责网段内的机器。不过,我们的 UDP 包在传递时,一般只会带上 NAT 的 host。如果,此时你没有目标机器的 entry 的话,那么该次 UDP 包将不会被转发成功。不过,如果你是 client-server 的形式的话,就不会遇见这样的问题。但,这里我们是 peer-to-peer 的方式进行传输,无法避免的会遇见这样的问题。

为了解决这样的问题,我们就需要建立 end-to-end 的连接。那办法是什么呢?很简单,就是在中间设立一个 server 用来保留目标机器在 NAT 中的 entry。常用协议有 STUN, TURN 和 ICE。那他们有什么区别吗?

  • STUN:作为最基本的 NAT traversal 服务器,保留指定机器的 entry
  • TURN:当 STUN 出错的时候,作为重试服务器的存在。
  • ICE:在众多 STUN + TURN 服务器中,选择最有效的传递通道。

所以,上面三者通常是结合在一起使用的。它们在 PeerConnection 中的角色如下图:

如果,涉及到 ICE 的话,我们在实例化 Peer Connection 时,还需要预先设置好指定的 STUN/TRUN 服务器。

var ice = {"iceServers": [      {"url": "stun:stun.l.google.com:19302"},       // TURN 一般需要自己去定义      {       'url': 'turn:192.158.29.39:3478?transport=udp',       'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',       'username': '28224511:1379330808'     },     {       'url': 'turn:192.158.29.39:3478?transport=tcp',       'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',       'username': '28224511:1379330808'     } ]};  var signalingChannel = new SignalingChannel(); var pc = new RTCPeerConnection(ice); // 在实例化 Peer Connection 时完成。  navigator.getUserMedia({ "audio": true }, gotStream, logError);  function gotStream(stream) {   pc.addStream(stream); // 将流添加到 connection 中。    pc.createOffer(function(offer) {     pc.setLocalDescription(offer);    }); }  // 通过 ICE,监听是否有用户连接 pc.onicecandidate = function(evt) {   if (evt.target.iceGatheringState == "complete") {        local.createOffer(function(offer) {         console.log("Offer with ICE candidates: " + offer.sdp);         signalingChannel.send(offer.sdp);        });   } } ... 

在 ICE 处理中,里面还分为 iceGatheringState 和 iceConnectionState。在代码中反应的就是:

  pc.onicecandidate = function(e) {     evt.target.iceGatheringState;     pc.iceGatheringState        };   pc.oniceconnectionstatechange = function(e) {     evt.target.iceConnectionState;     pc.iceConnectionState;   }; 

当然,起主要作用的还是 onicecandidate

  • iceGatheringState: 用来检测本地 candidate 的状态。其有以下三种状态:
    • new: 该 candidate 刚刚被创建
    • gathering: ICE 正在收集本地的 candidate
    • complete: ICE 完成本地 candidate 的收集
  • iceConnectionState: 用来检测远端 candidate 的状态。远端的状态比较复杂,一共有 7 种: new/checking/connected/completed/failed/disconnected/closed

不过,这里为了更好的讲解 WebRTC 建立连接的基本过程。我们使用单页的连接来模拟一下。现在假设,有两个用户,一个是 pc1,一个是 pc2。pc1 捕获视频,然后,pc2 建立与 pc1 的连接,完成伪直播的效果。直接看代码吧:

  var servers = null;   // Add pc1 to global scope so it's accessible from the browser console   window.pc1 = pc1 = new RTCPeerConnection(servers);   // 监听是否有新的 candidate 加入   pc1.onicecandidate = function(e) {     onIceCandidate(pc1, e);   };   // Add pc2 to global scope so it's accessible from the browser console   window.pc2 = pc2 = new RTCPeerConnection(servers);   pc2.onicecandidate = function(e) {     onIceCandidate(pc2, e);   };   pc1.oniceconnectionstatechange = function(e) {     onIceStateChange(pc1, e);   };   pc2.oniceconnectionstatechange = function(e) {     onIceStateChange(pc2, e);   };   // 一旦 candidate 添加成功,则将 stream 播放   pc2.onaddstream = gotRemoteStream;   // pc1 作为播放端,先将 stream 加入到 Connection 当中。   pc1.addStream(localStream);    pc1.createOffer(     offerOptions   ).then(     onCreateOfferSuccess,     error   );    function onCreateOfferSuccess(desc) {   // desc 就是 sdp 的数据   pc1.setLocalDescription(desc).then(     function() {       onSetLocalSuccess(pc1);     },     onSetSessionDescriptionError   );   trace('pc2 setRemoteDescription start');    // 省去了 offer 的发送通道   pc2.setRemoteDescription(desc).then(     function() {       onSetRemoteSuccess(pc2);     },     onSetSessionDescriptionError   );   trace('pc2 createAnswer start');   pc2.createAnswer().then(     onCreateAnswerSuccess,     onCreateSessionDescriptionError   ); } 

看上面的代码,大家估计有点迷茫,来点实的,大家可以参考 单页直播。在查看该网页的时候,可以打开控制台观察具体进行的流程。会发现一个现象,即,onaddstream 会在 SDP 协商还未完成之前就已经开始,这也是,该 API 设计的一些不合理之处,所以,W3C 已经将该 API 移除标准。不过,对于目前来说,问题不大,因为仅仅只是作为演示使用。整个流程我们一步一步来讲解下。

  1. pc1 createOffer start
  2. pc1 setLocalDescription start // pc1 的 SDP
  3. pc2 setRemoteDescription start // pc1 的 SDP
  4. pc2 createAnswer start
  5. pc1 setLocalDescription complete // pc1 的 SDP
  6. pc2 setRemoteDescription complete // pc1 的 SDP
  7. pc2 setLocalDescription start // pc2 的 SDP
  8. pc1 setRemoteDescription start // pc2 的 SDP
  9. pc2 received remote stream,此时,接收端已经可以播放视频。接着,触发 pc2 的 onaddstream 监听事件。获得远端的 video stream,注意此时 pc2 的 SDP 协商还未完成。
  10. 此时,本地的 pc1 candidate 的状态已经改变,触发 pc1 onicecandidate。开始通过 pc2.addIceCandidate 方法将 pc1 添加进去。
  11. pc2 setLocalDescription complete // pc2 的 SDP
  12. pc1 setRemoteDescription complete // pc2 的 SDP
  13. pc1 addIceCandidate success。pc1 添加成功
  14. 触发 oniceconnectionstatechange 检查 pc1 远端 candidate 的状态。当为 completed 状态时,则会触发 pc2 onicecandidate 事件。
  15. pc2 addIceCandidate success。

webRTC 基础介绍的更多相关文章

  1. 《Getting Started with WebRTC》第二章 WebRTC技术介绍

    <Getting Started with WebRTC>第二章 WebRTC技术介绍 本章作WebRTC的技术介绍,主要讲下面的概念:   .  怎样建立P2P的通信   .  有效的信 ...

  2. Web3D编程入门总结——WebGL与Three.js基础介绍

    /*在这里对这段时间学习的3D编程知识做个总结,以备再次出发.计划分成“webgl与three.js基础介绍”.“面向对象的基础3D场景框架编写”.“模型导入与简单3D游戏编写”三个部分,其他零散知识 ...

  3. C++ 迭代器 基础介绍

    C++ 迭代器 基础介绍 迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围.迭代器就如同一个指针.事实上,C++的指针也是一种迭代器.但是,迭代器不仅仅是指针,因此你不能认为他们一定 ...

  4. Node.js学习笔记(一)基础介绍

    什么是Node.js 官网介绍: Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js us ...

  5. Node.js 基础介绍

    什么是Node.js 官网介绍: Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js us ...

  6. 1、git基础介绍及远程/本地仓库、分支

    1. Git基础介绍 基于Git进行开发时,首先需要将远程仓库代码clone到本地,即为本地仓库.后续大部分时间都是基于本地仓库上的分支进行编码,最后将本地仓库的代码合入远程仓库. 1.1. 远程仓库 ...

  7. git基础介绍

    git基础介绍 这是git操作的基础篇,是以前的写的操作文档,就没有进行手打,直接把图片贴进来了,你们担待哈,有不正确的地方可以指正出来,我将在第一时间去修改,多谢哈! 一.文件状态:git系统的文件 ...

  8. OSPF基础介绍

    OSPF基础介绍 一.RIP的缺陷 1.以跳数评估的路由并非最优路径 2.最大跳数16导致网络尺度小 3.收敛速度慢 4.更新发送全部路由表浪费网络资源 二.OSPF基本原理 1.什么是OSPF a& ...

  9. iOS系统及客户端软件测试的基础介绍

    iOS系统及客户端软件测试的基础介绍 iOS现在的最新版本iOS5是10月12号推出,当前版本是4.3.5 先是硬件部分,采用iOS系统的是iPad,iPhone,iTouch这三种设备,其中iPho ...

随机推荐

  1. Linux定时任务运行thinkPHP某个方法

    先上实力: 1.查看正在执行的crontab,用命令crontab  -l ,这样就可以看到哪些任务一直在执行了.2.crontab -e  自动打开文件 编辑定时任务程序 在打开的页面中点击“i”键 ...

  2. xshell链接ubuntu16

    用xshell 链接 ubuntu16 失败 ,是因为没有装 ssh 服务 sudo apt-get install openssh-server         //安装ssh服务 ps -ef | ...

  3. 树莓派无显示屏连接wifi

    在烧好Raspbian系统的TF卡boot分区新建 wpa_supplicant.conf 文件,内容如下(修改自己的WIFI名和密码,key_mgmt根据路由器配置),保存后启动树莓派即可自动连接W ...

  4. JAVA线程池的创建与使用

    为什么要用线程池? 我们都知道,每一次创建一个线程,JVM后面的工作包括:为线程建立虚拟机栈.本地方法栈.程序计数器的内存空间(下图可看出),所以线程过多容易导致内存空间溢出.同时,当频繁的创建和销毁 ...

  5. Java自动化环境搭建笔记(2)

    Java自动化环境搭建笔记(2) 自动化测试 在笔记一中已经完成了一键构建项目.xml指定规划测试集.数据解耦与allure报告生成的开发.接下来便是: 浏览器驱动通过配置启动 页面元素定位解耦,通过 ...

  6. Go入门:创建第一个Go工程

    前言 我是一名iOS开发. 因为公司后台都用的Go. 因为对服务端不了解. 所以想自己学习学习. 环境 因为自己的电脑是mac.然后在阿里云买的是centOS的服务器. 所以下面搭建的环境都是在cen ...

  7. xadmin引入django-debug-toolbar调试工具

    一.安装: pip install django-debug-toolbar 安装django-debug-toolbar库 https://github.com/jazzband/django-de ...

  8. textarea还剩余字数统计,支持复制粘贴的时候统计字数

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...

  9. 字符流,字节流,属性集(Properties)

    字节输入流(InputStream) java.io.InputStream抽象类是表示字节输入流的所有类的超类.可以读取字节信息到内存中.它定义了字节输入流的基本共性功能方法. public voi ...

  10. 项目Beta冲刺--1/7

    项目Beta冲刺--1/7 作业要求 这个作业属于哪个课程 软件工程1916-W(福州大学) 这个作业要求在哪里 项目Beta冲刺 团队名称 基于云的胜利冲锋队 项目名称 云评:高校学生成绩综合评估及 ...