WebRTC 点对点直播

腾讯云技术社区-掘金主页持续为你们呈现云计算技术文章,欢迎你们关注!javascript


做者:villainthrjava

摘自:villainhrgit

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

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

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

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

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

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 性能优化 进行深刻的学习。

原文连接:ivweb.io/topic/58aae…

相关推荐:
【腾讯云的1001种玩法】 Laravel 整合微视频上传管理能力,轻松打造视频App后台
阐述腾讯云直播视频解决方案


此文已由做者受权腾讯云技术社区发布,转载请注明文章出处;获取更多云计算技术干货,可请前往腾讯云技术社区

相关文章
相关标签/搜索