WebRTC音视频原理

一、什么是WebRTC

WebRTC,网页即时通讯(Web Real-Time Communication),是直接在 Web 浏览器内驱动实时通信(语音、视频和任意数据)方法的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准,并于 2011 年标准化,是谷歌开源的一款产品。

WebRTC 实现了浏览器快速的实时通信,它允许网络应用或者站点在不借助中间媒介的情况下,建立浏览器之间点对点 (Peer-to-Peer) 的连接,实现视频流和音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,使创建点对点 (Peer-to-Peer) 的数据分享和电话会议成为可能。

二、WebRTC架构

从图中可以看出整个 WebRTC 架构设计大致可以分为以下 3 部分:

  1. 紫色提供给 Web 前端开发使用的 API
  2. 蓝色实线部分提供各大浏览器厂商使用的 API
  3. 蓝色虚线部分包含 3 部分:音频引擎、视频引擎、网络传输 (Transport)。都可以自定义实现

主要的工作包括:

(1)音视频的编解码(VP8/VP9/AV1)

(2)抗丢包和拥塞控制

(3)回声和噪音消除

WebRTC一个很大的作用就体现在这里了——提供可靠的传输、优质的编解码以及回声问题消除,P2P连接这种实时多媒体传输功能直接内嵌到浏览器里面,对于开发人员来说无疑大大地提高了开发效率。

1. WebRTC的主要组成

(1)getUserMedia是负责获取用户本地的多媒体数据,如调起摄像头录像等。

(2)RTCPeerConnection是负责建立P2P连接以及传输多媒体数据。

(3)RTCDataChannel是除音视频数据之外的任何其它数据

三、WebRTC通话流程

首先思考的问题:两个不同网络环境的(具备摄像头/麦克风多媒体设备的)浏览器,要实现点对点 的实时音视频对话,需要经历哪些过程呢 ?

通过上面的流程图可以总结出一下几点:

  1. peerConnnection 建立之前必须交换两个信息:SDP 和 iceCandidate;
  2. Answer 端接收到 Offer 后才会创建 PeerConnection 对象;
  3. Offer 端在创建成功本地 Offer 后才会去收集本地 iceCandidate;Answer 端在创建成功本地 Answer 后才会去收集本地 iceCandidate;
  4. peerConnection 两端都收到 iceCandidate 后才完成 peerConnection 的建立。

1. MediaStream

1.1 获取音视频设备

WebRTC获取所有音视频设备的API:enumerateDevices

 navigator.mediaDevices.enumerateDevices()
.then(successFunction)
.catch(failureFunction)

1.2 获取音视频流

使用WebRTC的getUserMedia实现获取视频和音频,方法很简单,如下代码所示:

window.navigator.mediaDevices
  .getUserMedia({video: true})
  .then(mediaStream => {
  // 画到一个video元素上面
  $('video')[0].srcObject = mediaStream;
});

1.3 获取屏幕共享(录制)视频流

如果想实现录屏(屏幕共享)的话,使用getDisplayMedia

navigator.mediaDevices
    .getDisplayMedia({video:true})

详细API请查看官方文档

2. RTCPeerConnection

peerconnection是webrtc面向外面的音视频交互的统一接口,可以理解为一个功能特别强大的socket接口,里面保存了实时交互的所有信息,同时音视频的转发与接收也是通过peerconnection来完成,在通话的每一端都至少有一个 RTCPeerConnection 对象。在 WebRTC 中它负责与各端建立连接,接收、发送音视频数据,并保障音视频的服务质量。

2.1 RTCPeerConnection 作用

我们把发起 WebRTC 通信的两端被称为对等端,即是 Peer。所谓点对点通信(peer-to-peer)则是说两个客户端直连,发送数据不需要中间服务器,建立成功的连接则称为PeerConnection,而一次 WebRTC 通信可包含多个 PeerConnection。

RTCPeerConnection 则是创建点对点连接的 API,代表一个由本地计算机到远端的 WebRTC 连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。

2.2 RTCPeerConnection API

创建两个连接实例

const peerA = new RTCPeerConnection()
const peerB = new RTCPeerConnection()
  • pc.createOffer:创建 offer 方法,方法会返回 SDP Offer 信息
  • pc.setLocalDescription 设置本地 SDP 描述信息
  • pc.setRemoteDescription:设置远端的 SDP 描述信息,即对方发过来的 SDP 信息
  • pc.createAnswer:远端创建应答 Answer 方法,方法会返回 SDP Offer 信息
  • pc.ontrack:设置完远端 SDP 描述信息后会触发该方法,接收对方的媒体流
  • pc.onicecandidate:设置完本地 SDP 描述信息后会触发该方法,打开一个连接,开始运转媒体流
  • pc.addIceCandidate:连接添加对方的网络信息
  • pc.setLocalDescription:将 localDescription 设置为 offer,localDescription 即为我们需要发送给应答方的 sdp,此描述指定连接本地端的属性,包括媒体格式

2.3 适配各种浏览器

一般情况下,还会在显示页面中添加一个叫做 adapter.js 的脚本,它的作用是为各种浏览器都提供统一的、最新的 WebRTC API 接口。

<script src="https://WebRTC.github.io/adapter/adapter-latest.js"> </script>

3. RTCDataChannel

非音视频数据都是通过 RTCDataChannel 进行传输,是 WebRTC 中专门用来传输非音视频数据的类,RTCDataChannel 支持的数据类型也非常多,包括:字符串、Blob、ArrayBuffer 以及 ArrayBufferView。数据通道 (DataChannel) 接口表示一个在两个节点之间的双向的数据通道,该通道可以设置成可靠传输或非可靠传输。

var pc = new RTCPeerConnection(); // 创建 RTCPeerConnection 对象
var dc = pc.createDataChannel("dc", options); //创建 RTCDataChannel对象 参数1: 标签名称 参数2: 配置项

options 配置项

  • ordered: 消息的传递是否有序
  • maxPacketLifeTime:重传消息失败的最长时间
  • maxRetransmits:重传消息失败的最大次数
  • protocol:用户自定义的子协议,默认为空
  • negotiated:如果为 true,则会删除另一方数据通道的自动设置。这也意味着你可以通过自己的方式在另一侧创建具有相同 ID 的数据通道
  • id:当 negotiated 为 true 时,允许你提供自己的 ID 与 channel 进行绑定

除了上面介绍的三个对象,在 WebRTC 中还有两个重要过程,就是媒体协商和网络协商。

4. 媒体协商和网络协商的交换通道

1. 信令

信令是在两个设备之间发送控制信息以确定通信协议、信道、媒体编解码器和格式以及数据传输方法以及任何所需的路由信息的过程。

2. 信令服务器

客户端协商媒体信息 (SDP) 和网络信息 (candidate) 交换,就需要一个服务器,我们称之为信令服务器,真实应用中还会处理房间管理 、人员进出房间等功能。

5. 媒体协商

我们首先要知道的是,不同浏览器对于音视频的编解码能力是不同的。比如: Peer-A 端支持 H264、VP8 等多种编码格式,而 Peer-B 端支持 H264、VP9 等格式。为了保证双方都可以正确的编解码,最简单的办法即取它们所都支持格式的交集-H264。

在 WebRTC 中,有一个专门的协议,称为Session Description Protocol(SDP),可以用于描述上述这类信息。因此参与音视频通讯的双方想要了解对方支持的媒体格式,必须要交换 SDP 信息。而交换 SDP 的过程,通常称之为媒体协商

5.1 媒体协商流程

这里以在两个前端浏览器建立通讯来进行说明,我们暂且称“发起端”和“应答端”。

(1)首先双方连接信令通道,(一般由业务决定如何实现),并能交换信令。

(2)发起端调用 RTCPeerConnection.createOffer 创建一个offer,并调用 setLocalDescription 设置本地SDP。

(3)然后通过信令服务器 将含有 SDP 的 offer 设置给应答端。

(4)应答端拿到此 offer 以后调用 setRemoteDescription 将此 SDP 信息保存。

(5)应答端调用 RTCPeerConnection.createAnswer 创建一个 answer,并调用 setLocalDescription 设置本地SDP。

(6)通过信令服务器将含有 SDP 的 answer 发送给发起端。

(7)发起端调用 setRemoteDescription 将此 SDP 信息保存。

简单概括就是:发起端和应答端通过 creatOffer 和 createAnswer 创建 offer/answerSDP,然后通过信令服务互换,最后调用 setLocalDescription/setRemoteDescription 进行设置本地和远端的 SDP 以完成协商。

在双方都创建 RTCPeerConnection 之后,它们就可以开始进行媒体协商了。

5.2 SDP信息内容

v=0 o=- 3409821183230872764 2 IN IP4 127.0.0.1
 ... 
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 
...
a=rtpmap:111 opus/48000/2 
a=rtpmap:103 ISAC/16000 
a=rtpmap:104 ISAC/32000 
...

如上面的 SDP 片段所示,该 SDP 中描述了一路音频流,即m=audio,该音频支持的 Payload ( 即数据负载 ) 类型包括 111、103、104 等等。

在该 SDP 片段中又进一步对 111、103、104 等 Payload 类型做了更详细的描述,如 a=rtpmap:111

opus/48000/2 表示 Payload 类型为 111 的数据是 OPUS 编码的音频数据,并且它的采样率是 48000,使用双 声道。以此类推,你也就可以知道 a=rtpmap:104 ISAC/32000 的含义是音频数据使用 ISAC 编码,采样频率 是 32000,使用单声道。

6. 网络协商

6.1 ICE

当媒体协商完成后,WebRTC 就开始建立网络连接,其过程称为 ICE(Interactive Connectivity Establishment)交互式连接建立。

ICE 不是一种协议,整合了 STUN 和 TURN 两种协议(用于 NAT 穿透)的框架。

ICE 是在各端调用 setLocalDescription() 后就开始了,其操作过程如下:

  1. 收集 Candidate
  2. 交换 Candidate
  3. 按优先级尝试连接

在说完上述一些生僻的概念,我来逐一解释下涉及到的东西

6.1.1 什么是 Candidate?
比如想用 socket 连接某台服务器,一定要知道这台服务器的一些基本信息,如服务器的 IP 地址、端口号以及使用的传输协议。只有知道了这些信息,才能与这台服务器建立连接。而 Candidate 正是 WebRTC 用来描述它可以连接的远端的基本信息,因此 Candidate 是至少包括 IP 地址、端口号、协议的一个信息集。

6.1.2 收集 Candidate
在 WebRTC 中有三种类型的 ICE 候选者(Candidate):

主机候选者:表示网卡自己的 IP 地址及端口。通过设备网卡获取,优先级最高。在 WebRTC 底层首先会尝试本地局域网内建立连接。


反射候选者:表示经过 NAT 之后的外网 IP 地址和端口,由 ICE(STUN)服务器获取,根据服务器的返回情况,来综合判断并知道自身在公网中的地址。其优先级低于主机候选者,当 WebRTC 尝试本地连接不通时,会尝试通过反射候选者获得的 IP 地址和端口进行连接。


中继候选者:表示的是中继(TURN)服务器的转发 IP 地址与端口,由 ICE 中继服务器提供。优先级最低,前两个都不行则会按该种方式。
在新建RTCPeerConnection时可在构造函数指定 ICE 服务器地址,没有指定的话则意味着这个连接只能在内网进行。

每次 WebRTC 找到/收集一个可用的 Candidate,都会触发一次icecandidate事件,为了将收集到的 Candidate 交换给对端,需要给onicecandidate方法设置一个回调函数,函数里面调用addIceCandidate方法来将候选者添加到通信中。

如下代码,通过该回调函数就可以获得 WebRTC 底层收集到的所有 Candidate 了。同时,还可以在该函数中将收集到的 Candidate 发送给对端。

peer.onicecandidate = (event) => {
  if (event.candidate) {
    // ...
  }
}

// 接收到信令服务器发送过来的候选信息后调用,为本机添加 ICE 代理
peer.addIceCandidate(candidate)

6.1.3 交换 Candidate
WebRTC 收集好 Candidate 后,会通过信令系统将它们发送给对端。对端接收到这些 Candidate 后,会与本地的 Candidate 形成 CandidatePair(即连接候选者对)。有了 CandidatePair,WebRTC 就可以开始尝试建立连接了。这里需要注意的是,Candidate 的交换不是等所有 Candidate 收集好后才进行的,而是边收集边交换。

CandidatePair,候选者对,即一个本地 Candidate,一个远端 Candidate
当 WebRTC 形成 CandidatePair 后,便开始尝试进行连接。一旦 WebRTC 发现其中有一个可以连通的 CandidatePair 时,它就不再进行后面的连接尝试了,但发现新的 Candidate 时仍然可以继续进行交换。

彼此要了解对方的网络情况,这样才有可能找到一条相互通讯的链路

先说结论:(1)获取外网IP地址映射;(2)通过信令服务器(signal server)交换"网络信息"

理想的网络情况是每个浏览器的电脑都是私有公网IP,可以直接进行点对点连接。

实际情况是:我们的电脑和电脑之前或大或小都是在某个局域网中,需要NAT(网络地址转换)打洞,显示情况如下图:

为了实现客户端的点到点连接(数据不需经过服务器转发),RTCPeerConnection做了很多工作。首先需要解决的问题是局域网穿透。

6.1.NAT穿墙打洞

要建立一个连接需要知道对方的IP地址和端口号,在局域网里面一台路由器可能会连接着很多台设备,例如家庭路由器接入宽带的时候宽带服务商会分配一个公网的IP地址,所有连到这个路由器的设备都共用这个公网IP地址。如果两台设备都用了同一个端口号创建套接字去连接服务,这个时候就会冲突,因为对外的IP是一样的。因此路由器需要重写IP地址/端口号进行区分,如下图所示:

有两台设备分别用了相同的端口号建立连接,被路由器转换成不同的端口,对外网表现为相同IP地址不同端口号,当服务器给这两个端口号发送数据的时候,路由器再根据地址转换映射表把数据转发给相应的主机。

所以当你在本地监听端口号为55020,但是对外的端口号并不是这个,对方用55020这个端口号是连不到你的。这个时候有两种解决方法,第一种是在路由器设置一下端口映射,如下图所示:

上图的配置是把所有发往8123端口的数据包到转到192.168.123.20这台设备上。

但是我们不能要求每个用户都这么配他们的路由器,因此就有了穿墙打洞,基本方法是先由服务器与其中一方(Peer)建立连接,这个时候路由器就会建立一个端口号内网和外网的映射关系并保存起来,如上面的外网1091就可以打到电脑的55020的应用上,这样就打了一个洞,这个时候服务器把1091端口加上IP地址告诉另一方(Peer),让它用这个打好洞的地址进行连接。这就是建立P2P连接穿墙打洞的原理,最早起源于网络游戏,因为打网络游戏经常要组网,WebRTC对NAT打洞进行了标准化。

这个的有效性受制于用户的网络拓扑结构,因为如果路由器的映射关系既取决于内网的IP + 端口号,也取决于服务器的IP加端口号,这个时候就打不了洞了,因为服务器打的那个洞不能给另外一个外网的应用程序使用(会建立不同的映射关系)。相反如果地址映射表只取决于内网机器的IP和端口号那么是可行的。打不了洞的情况下WebRTC也提供了解决方法,即用一个服务器转发多媒体数据。

6.2. STUN

STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重 NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的 Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。该协议由RFC 5389定 义。

在遇到上述情况的时候,我们可以建立一个STUN服务器,这个服务器做什么用的呢?主要是给无法在公网环境下的 视频通话设备分配公网IP用的。这样两台电脑就可以在公网IP中进行通话。

使用一句话说明STUN做的事情就是:告诉我你的公网IP地址+端口是什么。搭建STUN服务器很简单,媒体流传输是 按照P2P的方式。

那么问题来了,STUN并不是每次都能成功的为需要NAT的通话设备分配IP地址的,P2P在传输媒体流时,使用的本地带宽,在多人视频通话的过程中,通话质量的好坏往往需要根据使用者本地的带宽确定。那么怎么办?TURN可以 很好的解决这个问题。

6.3. TURN

TURN的全称为Traversal Using Relays around NAT,是STUN/RFC5389的一个拓展,主要添加了Relay功能。如果终端在NAT之后, 那么在特定的情景下,有可能使得终端无法和其对等端(peer)进行直接的通信,这时就需要公网的服务器作为一个中继, 对来往的数据进行转发。这个转发的协议就被定义TURN。

在上图的基础上,再架设几台TURN服务器:

在STUN分配公网IP失败后,可以通过TURN服务器请求公网IP地址作为中继地址。这种方式的带宽由服务器端承担,在多人视频聊天的时候,本地带宽压力较小,并且,根据Google的说明,TURN协议可以使用在所有的环境中。 (单向数据200kbps 一对一通话)

四、WebRTC存在的问题

WebRTC 是一个非常优秀的项目,但如果直接拿来使用也存在以下问题。

1、WebRTC 使用的是对点对传输,虽然节约了服务器资源的开销,但实际使用时也带来了传输质量的问题,比如跨国以及跨运营商网络之间的传输质量往往很难保证,虽然 webRTC 有优秀的端对端质量控制算法,但在错综复杂的网络条件下,表现也很难让人满意。

2、WebRTC 在移动端的表现也很难让人满意。早期由于缺少对于 H.264 编解码器的支持,使得移动端很长一段时间只能使用 VP8 软件编解码,导致在中低端手机上的表现较差,加上安卓自身碎片化的属性,如果不针对不同机型做适配,很难有统一的用户体验。

3、WebRTC 是为 1 对 1 通信场景设计的,如果要实现多人的场景,还是需要借助服务端方案。即使当前有很多开源的 webRTC 服务器实现,一个流媒体中转服务器或者混流服务器的部署以及维护也是非常复杂的。

4、在 Web 端需要面临不同浏览器之间的兼容性问题。虽然使用 AdapterJS 可以解决不同浏览器之间的接口适配问题,但除此之外依然要面临不同浏览器行为不一致的问题。可以说如果 WebRTC 如果直接拿过来商用的话,几乎是不太可能的,当下普遍的解决方案是自研,根据自身的业务场景进行二次定制开发,或者更简单一点使用第三方 SDK。

猜你喜欢

转载自blog.csdn.net/qq_44476091/article/details/126505032