WebRTC 点对点直播

标签: webrtc | 发表时间:2017-02-20 20:39 | 作者:jimmy_thr
出处:https://segmentfault.com/blogs

摘自: villainhr

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 处理的。处理过程为:

engineer.svg-62.3kB

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

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

最后通过 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. 在传输时,还需要保证信息安全性

整个架构如下:

WebRTC_stack.svg-39.5kB

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

signaling 任务

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

  • 打开/关闭连接的指令

  • 视频信息,比如解码器,解码器的设置,带宽,以及视频的格式等。

  • 关键数据,相当于 HTTPS 中的 master key 用来确保安全连接。

  • 网关信息,比如双方的 IP,port

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

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

  • 自定义协议

  • 多网关协议

signaling.svg-59.5kB

我们自己也可以模拟出一个 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: 用户端反馈给主播端,检查能否正常播放

具体过程为:

webRTC (1).png-7.7kB

  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 的方式进行传输,无法避免的会遇见这样的问题。

NAT_error.svg-30.4kB

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

  • STUN:作为最基本的 NAT traversal 服务器,保留指定机器的 entry

  • TURN:当 STUN 出错的时候,作为重试服务器的存在。

  • ICE:在众多 STUN + TURN 服务器中,选择最有效的传递通道。

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

ICE.svg-39.2kB

如果,涉及到 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 处理中,里面还分为 iceGatheringStateiceConnectionState。在代码中反应的就是:

    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。

此外,还有另外一个概念, RTCDataChannel 我这里就不过多涉及了。如果有兴趣的可以参阅 webrtc, web 性能优化 进行深入的学习。

相关 [webrtc 点对点 直播] 推荐:

WebRTC 点对点直播

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

基于 WebRTC 的互动直播实践

- - IT瘾-dev
互动直播已经逐渐成为直播的主要形式. 映客直播资深音视频工程师叶峰峰在LiveVideoStackCon 2018大会的演讲中详细介绍了INKE自研连麦整体设计思路、如何基于WebRTC搭建互动直播SDK以及针对用户体验进行优化. 本文由LiveVideoStack整理而成. 整理 / LiveVideoStack.

Firefox 22发布,支持WebRTC和asm.js

- - Solidot
Mozilla发布了Firefox 22的桌面版和移动版. 桌面版的主要变化包括默认启用开源视频语音通信工具WebRTC,WebRTC可以实现跨浏览器或跨平台的即时通讯功能. Firefox 22另一个重大变化是支持JavaScript的高性能子集asm.js,Mozilla此前曾与 Epic Games合作,利用asm.js将虚幻引擎3移植到浏览器上,在Web上实现接近原生的性能.

Mozilla为Firefox加入WebRTC聊天功能

- - Solidot
Mozilla Future Releases博客宣布,最新的Firefox Nightly版加入了基于WebRTC的语音视频聊天功能. WebRTC提供了跨浏览器或跨平台的即时通讯功能. Mozilla产品管理总监Chad Weiner说,没有插件,不需要下载,只需要一个支持WebRTC的浏览器(如Firefox和Chrome),一个摄像头和麦克风,你就可以语音和视频呼叫其他使用这些浏览器的用户.

基于WebRTC的多人视频会议

- -
最近两周在调研和搭建基于WebRTC的多人视频会议系统. http://jitsi.shengbin.me/试用. 这个系统无需注册和登录,只要多人访问同一个URL(含有系统为每个房间分配的特定ID),就可以进行视频会议. 如果上面那个链接失效,可以尝试国外一个同样的系统:. 使用视频会议系统需要客户端电脑提供摄像头功能;至于带宽,当然是越大越好了.

谷歌开放实时通信框架 WebRTC

- gaochao - 开源中国社区最新软件
北京时间6月2日凌晨消息,谷歌今日宣布向开发人员开放WebRTC架构的源代码. WebRTC是一项在浏览器内部进行实时视频和音频通信的技术,是谷歌去年以6820万美元收购收购Global IT Solutions公司而获得一项技术. 谷歌今日在官方博客中称:“我们希望让浏览器成为实时通信的创新地所在,到目前为止,实时通信需要使用受版权保护的信号处理技术,并通过插件或下载客户端才能实现,而WebRTC则允许开发人员使用HTML和JavaScript API来创.

谷歌开放实时通信框架WebRTC源代码

- 天绝@Lee - cnBeta全文版
谷歌今日宣布向开发人员开放WebRTC架构的源代码. WebRTC是一项在浏览器内部进行实时视频和音频通信的技术,是谷歌去年以6820万美元收购收购Global IT Solutions公司而获得一项技术. 谷歌今日在官方博客中称:“我们希望让浏览器成为实时通信的创新地所在,到目前为止,实时通信需要使用受版权保护的信号处理技术,并通过插件或下载客户端才能实现,而WebRTC则允许开发人员使用HTML和JavaScript API来创建实时应用.

微软 Skype for Browsers 应用将支持 WebRTC 标准

- - LiveSino - LiveSide 中文版
最近有 新的招聘暗示,Skype Web App(Skype for Browsers)将支持 WebRTC 标准. Gigaom 注意到了这则“高级软件开发工程师,WebRTC”的招聘,工作地点是在 Skype London 和 Palo ALto 办公室,重点部分如下:. 你将帮助创建一种架构,来允许在 Skype 网络中 WebRTC 的终端与其他终端直接互通,而不需要网关.

PeerJS 0.1.7:一个用于浏览器内P2P的WebRTC封装器

- - InfoQ cn
Michelle Bu与 Eric Zhang在3月6日发布了 PeerJS 0.1.7,它封装了WebRTC. 后者是W3C倡议的旨在促进浏览器内P2P通信的一种技术. 尽管 WebSocket的作用发展迅速,但PeerJS代表的是服务器所组织数据之传输方式的一种根本性转变. Bu说:“WebSocket和WebRTC数据通道看起来一样——都支持二进制数据,还允许从一个客户端发送可能最终会到达另一客户端的任意数据,然而它们本质上是不同的.

Chrome21稳定版发布,初步实现WebRTC

- - 脚本爱好者
谷歌的 Chrome 团队今天正式发布了 Chrome 21 稳定版本,Mac 和 Linux 平台上的版本号为 21.0.1180.57,Windows 平台上的版本号为 21.0.1180.60.   该版本中包含了一系列新的特性,新增了一个用于高质量视频音频通讯的. getUserMedia API,该 API 允许 Web 应用程序访问摄像头和麦克风,而无需使用插件,这是实现.