WebRTC 概要介绍
# 什么是 WebRTC?
面向网络的实时通信:
借助 WebRTC,您可以为应用添加基于开放标准运行的实时通信功能。它支持在对等设备之间发送视频、语音和通用数据,使开发者能够构建强大的语音和视频通信解决方案。这项技术适用于所有现代浏览器以及所有主要平台的原生客户端。WebRTC 采用的技术是开放网络标准,以常规 JavaScript API 的形式在所有主流浏览器中提供。对于原生客户端(例如 Android 和 iOS 应用),可以使用具备相同功能的库。
特点:
- 音视频处理+即时通讯的开源库
- 2010 年 Google 将其开源
- 优秀的多媒体框架,跨平台
# WebRTC 可以做什么?
将WebRTC加入浏览器,使得浏览器的功能更加强大。WebRTC(Web Real-Time Communication)项目的最终目的主要是让Web开发者能够基于浏览器(Chrome\FireFox...)轻易快捷开发出丰富的实时多媒体应用,而无需下载安装任何插件,Web开发者也无需关注多媒体的数字信号处理过程,只需编写简单的Javascript程序即可实现,W3C等组织正在制定Javascript 标准API,目前是WebRTC 1.0版本即Draft状态;另外WebRTC还希望能够建立一个多互联网浏览器间健壮的实时通信的平台,形成开发者与浏览器厂商良好的生态环境。同时,Google也希望和致力于让WebRTC的技术成为HTML5标准之一,可见Google布局之深远。
WebRTC 有许多不同的用例,从使用摄像头或麦克风的基本 Web 应用,到更高级的视频通话应用和屏幕共享。
- 音视频实时互动
- 游戏、即时通讯、文件传输等等
- 传输、音视频处理(回音消除、降噪等)
WebRTC 的应用场景:
愿景:各浏览器之间可以快速开发可以实时互动的音视频的应用场景
# 难点
- 过多的协议,WebRTC太庞大、烦杂,门槛高。
- 客户端与服务端分离,WebRTC只有客户端,没有服务端,需要自己根据业务实现。
- 相关资料少。
- 网上代码错误太多。
# WebRTC与FFmpeg
- WebRTC与FFmpeg是音视频领域的两个佼佼者,两个侧重点不同。
- FFmpeg侧重于多媒体文件的编辑,音视频的编解码等。
- WebRTC侧重于处理网络抖动、丢包、评估以及音频处理,回音降噪等等。
# 应用流程
WebRTC 应用通常会经过常见的应用流程。访问媒体设备,打开对等连接,发现对等设备,并开始流式传输。
# 新功能
# 原理与架构
WebRTC实现了基于网页的视频会议,标准是 WHATWG 协议,目的是通过浏览器提供简单的javascript就可以达到实时通讯(Real-Time Communications (RTC))能力。
WebRTC整体架构主要分为两部分:
- 第一部分为绿色区域,为webrtc库所提供的核心功能。
- 第二部分为紫色区域,是浏览器提供的javascript的API层,也就是说浏览器对webrtc的核心层的C++ API做了一层封装,封装成为了javascript接口。
- 第三部分为箭头区域,是很多的上层应用。
- 调用顺序就是从上到下。
# 核心层
分为如下4层:
- 第一层为C++ API,是WebRTC库提供给浏览器javascript层的核心功能API接口(比如:连接、P2P进行连接、传输质量、设备管理....)。
- 第二层为Session层,上下文管理层,音频、视频、非音视频的数据传输,都通过session层处理,实现相关逻辑。
- 第三层包括音频引擎、视频引擎、传输模块。
- 第四层与硬件相关,包括音视频的采集、网络IO (可重载的,可以使用自己的方案)
注意:在webrtc中没有对视频进行渲染处理,所以需要我们在应用中自己实现。
# 引擎层
引擎层包括音频引擎、视频引擎、传输模块。将这3个模块分隔开来,逻辑更加清晰。另外音视频的同步不是在引擎层实现。
音频引擎:包含一系列音频多媒体处理的框架,包括从视频采集卡到网络传输端等整个解决方案。VoiceEngine是WebRTC极具价值的技术之一,是Google收购GIPS公司后开源的。
# 音频引擎
# 编解码器
- iSAC(Internet Speech Audio Codec):针对VoIP和音频流的宽带和超宽带音频编解码器,是WebRTC音频引擎的默认的编解码器。
参数
- 采样频率:16khz,24khz,32khz;(默认为16khz)
- 自适应速率为10kbit/s ~ 52kbit/s
- 自适应包大小:30~60ms
- 算法延时:frame + 3ms
- iLBC(Internet Low Bitrate Codec):VoIP音频流的窄带语音编解码器。
参数
- 采样频率:8khz;
- 20ms帧比特率为15.2kbps
- 30ms帧比特率为13.33kbps
- 标准由IETF RFC3951和RFC3952定义
# 防止抖动与丢失
NetEQ for Voice:针对音频软件实现的语音信号处理元件。
NetEQ算法是自适应抖动控制算法以及语音包丢失隐藏算法。使其能够快速且高解析度地适应不断变化的网络环境,确保音质优美且缓冲延迟最小。 是GIPS公司独步天下的技术,能够有效的处理由于网络抖动和语音包丢失时候对语音质量产生的影响。
NetEQ 也是WebRTC中一个极具价值的技术,对于提高VoIP质量有明显效果,加以AEC\NR\AGC
等模块集成使用,效果更好。
# 回音消除、降噪
- Acoustic Echo Canceler (AEC) :回声消除器是一个基于软件的信号处理元件,能实时的去除mic采集到的回声。
- Noise Reduction (NR):噪声抑制也是一个基于软件的信号处理元件,用于消除与相关VoIP的某些类型的背景噪声(嘶嘶声,风扇噪音等等……)。
# 视频引擎
视频引擎包含一系列视频处理的整体框架,从摄像头采集视频到视频信息网络传输再到视频显示整个完整过程的解决方案。
- 编解码器:视频图像编解码器,是WebRTC视频引擎的默认的编解码器。VP8适合实时通信应用场景,因为它主要是针对低延时而设计的编解码器。
扩展
VPx编解码器是Google收购ON2公司后开源的,VPx现在是WebM项目的一部分,而WebM项目是Google致力于推动的HTML5标准之一
- 视频抖动缓冲器:Video Jitter Buffer,可以降低由于视频抖动和视频信息包丢失带来的不良影响。
- 图像质量增强模块:Image enhancements,对网络摄像头采集到的图像进行处理,包括明暗度检测、颜色增强、降噪处理等功能,用来提升视频质量。
# 传输层
底层使用UDP,上层使用RTP。所有的音视频的接收与发送,都是通过传输模块实现的。此外在传输层实现了网络链路的检测,进行网络带宽的估计,从而对(音视频、非音视频)数据的传输进行控制。
扩展
- 由于浏览器需要安全传输,所以使用了
SRTP
协议,为了进行控制,使用了RTCP
; - 为了处理多个流复用同一个通道,实现了
Multiplexing
。 - 最下面实现了P2P相关的协议,比如
STUN
+TRUN
+ICE
。 - 虽然UDP很适合实时通讯,但是也有需要使用TCP的场景:连通性TCP要优于UDP,假如国内外通信,可能某些区域不允许通过UDP进行实时传输。为了保证连通率,优先选择UDP,如果UDP无法通信,则选择TCP,以此来保证连通率。当然,也存在部分情况,TCP依旧不通,比如通过企业内部网访问,网关拒绝访问外网,这时可以使用Https。这时不太保证实时性了。
# WebRTC API
WebRTC 标准概括介绍了两种不同的技术:媒体捕获设备和点对点连接。
媒体捕获设备包括摄像机和麦克风,还包括屏幕捕获设备。对于摄像头和麦克风,我们使用 navigator.mediaDevices.getUserMedia()
来捕获 MediaStreams。对于屏幕录制,我们改为使用 navigator.mediaDevices.getDisplayMedia()
。
点对点连接由 RTCPeerConnection
接口处理。这是在 WebRTC 中两个对等方之间建立和控制连接的中心点。
# 媒体设备使用入门
针对 Web 开发时,WebRTC 标准提供了用于访问连接到计算机或智能手机的相机和麦克风的 API。这些设备通常称为媒体设备,可以通过实现 MediaDevices
接口的 navigator.mediaDevices
对象使用 JavaScript 进行访问。通过此对象,我们可以枚举所有已连接的设备,监听设备的变化(设备连接或断开连接时)以及打开设备以检索媒体流(见下文)。
其最常见的方式是通过 getUserMedia()
函数,该函数会返回一个解析为匹配媒体设备的 MediaStream
的 promise。此函数采用单个 MediaStreamConstraints
对象,用于指定我们的要求。例如,要简单地打开默认麦克风和摄像头,请执行以下操作。
const openMediaDevices = async (constraints) => {
return await navigator.mediaDevices.getUserMedia(constraints);
}
try {
const stream = openMediaDevices({'video':true,'audio':true});
console.log('Got MediaStream:', stream);
} catch(error) {
console.error('Error accessing media devices.', error);
}
2
3
4
5
6
7
8
9
10
调用 getUserMedia()
将触发权限请求。如果用户接受该权限,系统会使用包含一个视频和一个音轨的 MediaStream 解析该 promise。如果权限遭拒,系统会抛出 PermissionDeniedError
。如果没有连接任何匹配的设备,则会抛出 NotFoundError
。
# 查询媒体设备
在更复杂的应用中,我们很可能需要检查所有连接的摄像头和麦克风,并向用户提供相应的反馈。这可以通过调用 enumerateDevices()
函数来实现。这将返回一个 promise,它可以解析为描述每个已知媒体设备的 MediaDevicesInfo
数组。我们可以用它来呈现界面,让用户选择他们喜欢的那个。每个 MediaDevicesInfo
都包含一个名为 kind
的属性,其值为 audioinput
、audiooutput
或 videoinput
,指示它是哪种类型的媒体设备。
async function getConnectedDevices(type) {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === type)
}
const videoCameras = getConnectedDevices('videoinput');
console.log('Cameras found:', videoCameras);
2
3
4
5
6
7
# 监听设备更改
大多数计算机都支持在运行时插入各种设备。它可能是通过 USB 连接的摄像头、蓝牙耳机或一组外部扬声器。为了正确支持这一点,Web 应用应监听媒体设备的变化。这可以通过为 devicechange
事件的 navigator.mediaDevices
添加监听器来实现。
// Updates the select element with the provided set of cameras
function updateCameraList(cameras) {
const listElement = document.querySelector('select#availableCameras');
listElement.innerHTML = '';
cameras.map(camera => {
const cameraOption = document.createElement('option');
cameraOption.label = camera.label;
cameraOption.value = camera.deviceId;
return cameraOption;
}).forEach(cameraOption => listElement.add(cameraOption));
}
// Fetch an array of devices of a certain type
async function getConnectedDevices(type) {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === type)
}
// Get the initial set of cameras connected
const videoCameras = getConnectedDevices('videoinput');
updateCameraList(videoCameras);
// Listen for changes to media devices and update the list accordingly
navigator.mediaDevices.addEventListener('devicechange', event => {
const newCameraList = getConnectedDevices('video');
updateCameraList(newCameraList);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 媒体限制
如果约束对象必须实现 MediaStreamConstraints
接口并将其作为参数传递给 getUserMedia()
,我们就可以打开符合特定要求的媒体设备。此要求可以非常宽泛(音频和/或视频),也可以非常具体(最低相机分辨率或确切设备 ID)。建议使用 getUserMedia()
API 的应用先检查现有设备,然后使用 deviceId
限制条件指定与设备完全匹配的限制条件。如果可能,设备还会根据限制条件进行配置。我们可以对麦克风启用回声消除功能,也可以从摄像头设置视频的特定或最小宽度和高度。
async function getConnectedDevices(type) {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === type)
}
// Open camera with at least minWidth and minHeight capabilities
async function openCamera(cameraId, minWidth, minHeight) {
const constraints = {
'audio': {'echoCancellation': true},
'video': {
'deviceId': cameraId,
'width': {'min': minWidth},
'height': {'min': minHeight}
}
}
return await navigator.mediaDevices.getUserMedia(constraints);
}
const cameras = getConnectedDevices('videoinput');
if (cameras && cameras.length > 0) {
// Open first available video camera with a resolution of 1280x720 pixels
const stream = openCamera(cameras[0].deviceId, 1280, 720);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 本地播放
媒体设备打开后,如果有 MediaStream,我们可以将其分配给视频或音频元素,以在本地播放流。
async function playVideoFromCamera() {
try {
const constraints = {'video': true, 'audio': true};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
const videoElement = document.querySelector('video#localVideo');
videoElement.srcObject = stream;
} catch(error) {
console.error('Error opening video camera.', error);
}
}
2
3
4
5
6
7
8
9
10
与 getUserMedia()
一起使用的典型视频元素所需的 HTML 通常具有 autoplay
和 playsinline
属性。autoplay
属性将使分配给元素的新数据流自动播放。playsinline
属性允许视频在特定移动浏览器中内嵌播放,而不仅仅是全屏播放。此外,我们还建议对直播使用 controls="false",除非用户应能够暂停这些直播。
<html>
<head><title>Local video playback</video></head>
<body>
<video id="localVideo" autoplay playsinline controls="false"/>
</body>
</html>
2
3
4
5
6
# 媒体捕获和约束
WebRTC 的媒体部分介绍了如何使用能够捕捉视频和音频的硬件(例如相机和麦克风),以及媒体流的工作原理。此外,还介绍了显示媒体,这是应用可执行屏幕捕获的方式。
# 媒体设备
您可以通过 navigator.mediaDevices
对象访问和管理浏览器支持的所有摄像头和麦克风。应用可以检索已连接设备的最新列表并监听变化,因为许多相机和微型麦克风可通过 USB 连接,并且可以在应用生命周期内连接和断开连接。由于媒体设备的状态可能会随时发生变化,因此建议应用注册设备更改,以便正确处理更改。
# 约束条件
访问媒体设备时,建议您提供尽可能详细的限制条件。虽然可以通过简单的约束条件打开默认摄像头和麦克风,但其提供的媒体流可能明显优于应用的最佳流。
具体的约束条件在 MediaTrackConstraint
对象中定义,一个针对音频,另一个针对视频。此对象中的特性类型为 ConstraintLong
、ConstraintBoolean
、ConstraintDouble
或 ConstraintDOMString
。这些对象可以是特定值(例如数字、布尔值或字符串)、范围(具有最小值和最大值的 LongRange 或 DoubleRange)或具有 ideal
或 exact
定义的对象。对于特定值,浏览器将尝试选择尽可能接近的值。对于某个范围,将使用该范围内的最佳值。指定 exact
后,系统将仅返回与约束条件完全匹配的媒体流。
// Camera with a resolution as close to 640x480 as possible
{
"video": {
"width": 640,
"height": 480
}
}
// Camera with a resolution in the range 640x480 to 1024x768
{
"video": {
"width": {
"min": 640,
"max": 1024
},
"height": {
"min": 480,
"max": 768
}
}
}
// Camera with the exact resolution of 1024x768
{
"video": {
"width": {
"exact": 1024
},
"height": {
"exact": 768
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
为了确定某个媒体流的特定轨道的实际配置,我们可以调用 MediaStreamTrack.getSettings()
,它会返回当前应用的 MediaTrackSettings
。
此外,也可以通过对媒体轨道上调用 applyConstraints()
来更新已打开的媒体设备上的轨道约束条件。这样,应用无需重新关闭现有音频流,即可重新配置媒体设备。
# 显示媒体
想要能够截取和录制屏幕的应用必须使用 Display Media API
。函数 getDisplayMedia()
(属于 navigator.mediaDevices
的一部分)与getUserMedia()
类似,用于打开显示内容(或部分内容,如窗口)。返回的 MediaStream
与使用 getUserMedia()
时相同。
getDisplayMedia()
的约束条件与常规视频或音频输入资源的限制不同。
{
video: {
cursor: 'always' | 'motion' | 'never',
displaySurface: 'application' | 'browser' | 'monitor' | 'window'
}
}
2
3
4
5
6
上述代码片段展示了屏幕录制的特殊限制的工作原理。请注意,并非所有支持显示媒体支持的浏览器都支持这些属性。
# 数据流和轨道
MediaStream
表示媒体内容流,由音频和视频轨道 (MediaStreamTrack
) 组成。您可以通过调用 MediaStream.getTracks()
从 MediaStream
检索所有轨道,该方法会返回一组 MediaStreamTrack
对象。
# 媒体流跟踪
MediaStreamTrack
具有的 kind
属性为 audio
或 video
,用于表示其表示的媒体类型。您可以通过切换其 enabled
属性将各个轨道静音。轨道具有布尔属性 remote
,它会指示它来自 RTCPeerConnection
而来自远程对等设备。
# 开始使用对等连接
点对点连接是 WebRTC 规范的一部分,该规范旨在对点一台计算机上的两台应用进行连接,以使用点对点协议进行通信。对等设备之间的通信可以是视频、音频或任意二进制数据(适用于支持 RTCDataChannel API 的客户端)。为了发现两个对等端如何连接,两个客户端都需要提供 ICE Server 配置。这是 STUN 或 TURN 服务器,其作用是向每个客户端提供 ICE 候选对象,然后这些客户端将被传输到远程对等方。这种转移 ICE 候选对象的方式通常称为信号。
# 信令
WebRTC 规范包含用于与 ICE(互联网连接建立)服务器通信的 API,但信令组件并不属于该组件。需要发出信号才能让两个对等网络共享它们之间的连接方式。这通常可以通过基于 HTTP 的常规 Web API(即 REST 服务或其他 RPC 机制)解决,在此过程中,网络应用可在发起对等连接之前中继必要的信息。
以下代码段展示了如何使用虚构信号服务异步发送和接收消息。必要时,本指南的其余示例将使用该方法。
// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
// New message from remote client received
});
// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');
2
3
4
5
6
7
8
9
信令可以通过许多不同的方式实现,WebRTC 规范不偏好任何特定的解决方案。
# 启动对等连接
每个对等连接都由一个 RTCPeerConnection
对象处理。此类的构造函数接受单个 RTCConfiguration
对象作为其参数。此对象定义对等连接的设置方式,应包含关于要使用的 ICE 服务器的信息。
创建 RTCPeerConnection
后,我们需要创建 SDP offer 或 answer,具体取决于我们是发起通话的对等方还是接收方的对等方。SDP offer 或 answer 一经创建,就必须通过其他信道发送给远程对等方。将 SDP 对象传递给远程对等设备的过程称为信号,不在 WebRTC 规范的涵盖范围内。
为了从调用方启动对等连接设置,我们创建一个 RTCPeerConnection
对象,然后调用 createOffer()
以创建 RTCSessionDescription
对象。此会话说明设置为使用 setLocalDescription()
的本地说明,然后通过我们的信令通道发送到接收端。我们还针对针对信令渠道设置了监听器,接收方收到所提供会话说明后得回答会被监听到。
async function makeCall() {
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.answer) {
const remoteDesc = new RTCSessionDescription(message.answer);
await peerConnection.setRemoteDescription(remoteDesc);
}
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingChannel.send({'offer': offer});
}
2
3
4
5
6
7
8
9
10
11
12
13
在接收端,创建 RTCPeerConnection
实例,创建 offer 并通过信令通道发送到远程。在等待 remote answer 的消息监听中,我们使用 setRemoteDescription()
设置收到的 answer。在远端,我们调用 createAnswer()
为收到的 offer 创建答案。系统会使用 setLocalDescription()
将此答案设置为本地说明,然后通过我们的信令服务器将其发送至发起调用的一方。
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.offer) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signalingChannel.send({'answer': answer});
}
});
2
3
4
5
6
7
8
9
两个对等方同时设置了本地和远程会话说明之后,他们就会了解远程对等方的功能。这并不意味着对等设备之间的连接已准备就绪。为此,我们需要在每个对等端收集 ICE 候选项,并通过信令通道传输给另一个对等方。
# ICE Candidate
两个对等方必须用 WebRTC 交换连接信息,然后才能使用 WebRTC 进行通信。由于网络条件可能因多种因素而发生变化,因此通常使用外部服务来发现连接到对等网络的潜在候选对象。此服务称为 ICE,使用的是 STUN 或 TURN 服务器。STUN 代表用于 NAT 的会话遍历实用程序,通常在大多数 WebRTC 应用中间接使用。
TURN(Traversal using Relay NAT)是一种整合了 STUN 协议的更高级解决方案,大多数基于 WebRTC 的商业服务都使用 TURN 服务器在对等方之间建立连接。WebRTC API 直接支持 STUN 和 TURN,在更完整的术语“互联网连接建立”下收集。创建 WebRTC 连接时,我们通常会在 RTCPeerConnection 对象的配置中提供一个或多个 ICE 服务器。
# Trickle ICE
创建 RTCPeerConnection
对象后,底层框架会使用提供的 ICE 服务器收集连接建立的候选对象(ICE 候选对象)。RTCPeerConnection
上的事件 icegatheringstatechange
会指示 ICE 收集的状态为(new、gathering 或 complete)。
虽然对等设备可以等待 ICE 收集完成,但通常要高效地使用“滚动冰”技术,并在发现每个 ICE 候选设备后将其传输到远程对等设备。这将大大缩短对等连接的设置时间,并允许视频通话以更低的延迟开始。
要收集 ICE 候选对象,只需为 icecandidate
事件添加监听器即可。针对该监听器发出的 RTCPeerConnectionIceEvent
将包含 candidate
属性,该属性表示应发送到远程对等端的新候选成员(请参阅信号)。
// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate) {
signalingChannel.send({'new-ice-candidate': event.candidate});
}
});
// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
if (message.iceCandidate) {
try {
await peerConnection.addIceCandidate(message.iceCandidate);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 已建立连接
收到 ICE 候选对象后,我们的对等连接状态最终会变为已连接状态。为了检测这一点,我们在 RTCPeerConnection
中添加一个监听器,用于监听 connectionstatechange
事件。
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});
2
3
4
5
6
# 远程数据流
RTCPeerConnection
连接到远程对等设备后,就可以在它们之间流式传输音频和视频。此时,我们会将从 getUserMedia()
收到的数据流连接到 RTCPeerConnection
。媒体流包含至少一个媒体轨道,当我们想将媒体传输到远程对等设备时,它们会分别添加到 RTCPeerConnection
中。
const localStream = await getUserMedia({vide: true, audio: true});
const peerConnection = new RTCPeerConnection(iceConfig);
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
2
3
4
5
轨道可以在连接到远程对等方之前添加到 RTCPeerConnection
,因此最好尽早执行此设置,而不是等待连接完成。
# 添加远程轨道
为了接收由另一个对等方添加的远程轨道,我们会在本地 RTCPeerConnection
上注册一个监听器,用于监听 track
事件。RTCTrackEvent
包含一个 MediaStream
对象数组,这些对象与对等项的相应本地数据流具有相同的 MediaStream.id
值。在我们的示例中,每个轨道仅与单个数据流相关联。
请注意,尽管 MediaStream ID 在对等端的两端均匹配,但 MediaStreamTrack ID 通常并非如此。
const remoteVideo = document.querySelector('#remoteVideo');
peerConnection.addEventListener('track', async (event) => {
const [remoteStream] = event.streams;
remoteVideo.srcObject = remoteStream;
});
2
3
4
5
6
# 数据通道
WebRTC 标准还涵盖用于通过 RTCPeerConnection
发送任意数据的 API。可通过对 RTCPeerConnection
对象调用 createDataChannel()
来完成此操作,该方法会返回 RTCDataChannel
对象。
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();
2
远程对等端可以通过监听 RTCPeerConnection
对象的 datachannel
事件来接收数据通道。收到的事件是 RTCDataChannelEvent
类型,包含一个 channel
属性,该属性表示在对等方之间连接的 RTCDataChannel
。
const peerConnection = new RTCPeerConnection(configuration);
peerConnection.addEventListener('datachannel', event => {
const dataChannel = event.channel;
});
2
3
4
# 打开和关闭事件
在使用数据通道发送数据之前,客户端需要等到数据通道打开后才能使用它。具体方法是监听 open
事件。同样,当任意一侧关闭频道时,也会发生 close
事件。
const messageBox = document.querySelector('#messageBox');
const sendButton = document.querySelector('#sendButton');
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();
// Enable textarea and button when opened
dataChannel.addEventListener('open', event => {
messageBox.disabled = false;
messageBox.focus();
sendButton.disabled = false;
});
// Disable input when closed
dataChannel.addEventListener('close', event => {
messageBox.disabled = false;
sendButton.disabled = false;
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 信息
如需在 RTCDataChannel
上发送消息,请使用要发送的数据调用 send()
函数。此函数的 data
参数可以是字符串、Blob、ArrayBuffer 或 ArrayBufferView。
const messageBox = document.querySelector('#messageBox');
const sendButton = document.querySelector('#sendButton');
// Send a simple text message when we click the button
sendButton.addEventListener('click', event => {
const message = messageBox.textContent;
dataChannel.send(message);
})
2
3
4
5
6
7
8
远程对等端将通过监听 message
事件来接收 RTCDataChannel
上发送的消息。
const incomingMessages = document.querySelector('#incomingMessages');
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();
// Append new messages to the box of incoming messages
dataChannel.addEventListener('message', event => {
const message = event.data;
incomingMessages.textContent += message + '\n';
});
2
3
4
5
6
7
8
9
10
# TURN 服务器
对于大多数 WebRTC 应用,服务器都需要在对等设备之间中继流量,因为在客户端之间通常无法实现直接套接字(除非这些应用在同一本地网络中)。解决此问题的常见方法是使用 TURN 服务器。术语表示使用中继 NAT 的遍历,是一种中继网络流量的协议。
目前有几种针对 TURN 服务器的选择:在线托管应用(如开源 COTURN 项目)和作为云提供的服务。
有了 TURN 服务器可在线使用后,您只需具备正确的 RTCConfiguration
以供客户端应用使用。以下代码段展示了 RTCPeerConnection
的示例配置,其中 TURN 服务器的主机名为 my-turn-server.mycompany.com
,端口为 19403。配置对象还支持 username
和 credentials
属性,以确保对服务器访问的安全。连接到 TURN 服务器时需要这些证书。
const iceConfiguration = {
iceServers: [
{
urls: 'turn:my-turn-server.mycompany.com:19403',
username: 'optional-username',
credentials: 'auth-token'
}
]
}
const peerConnection = new RTCPeerConnection(iceConfiguration);
2
3
4
5
6
7
8
9
10
11
# 测试 WebRTC 应用
为 WebRTC 应用编写自动化测试时,可以对浏览器启用一些有用的配置,以便更轻松地进行开发和测试。
# Chrome
在 Chrome 上运行自动化测试时,以下参数在启动时非常有用:
--allow-file-access-from-files
- 允许访问 file:// 网址的 API--disable-translate
- 停用翻译弹出窗口--use-fake-ui-for-media-stream
- 提供虚假媒体流。在 CI 服务器上运行时非常有用。--use-file-for-fake-audio-capture=<filename>
- 提供用于捕获音频的文件。--use-file-for-fake-video-capture=<filename>
- 提供在捕获视频时使用的文件。--headless
- 在无头模式下运行。在 CI 服务器上运行时非常有用。--mute-audio
- 将音频输出静音。
# Firefox
在 Firefox 上运行自动化测试时,您需要提供一组偏好设置键,这些键将用于已启动的实例。以下是用于 WebRTC 自动化测试示例的配置:
"prefs": {
"browser.cache.disk.enable": false,
"browser.cache.disk.capacity": 0,
"browser.cache.disk.smart_size.enabled": false,
"browser.cache.disk.smart_size.first_run": false,
"browser.sessionstore.resume_from_crash": false,
"browser.startup.page": 0,
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true,
"device.storage.enabled": false,
"media.gstreamer.enabled": false,
"browser.startup.homepage": "about:blank",
"browser.startup.firstrunSkipsHomepage": false,
"extensions.update.enabled": false,
"app.update.enabled": false,
"network.http.use-cache": false,
"browser.shell.checkDefaultBrowser": false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# WebRTC运行机制
# 轨与流
- 轨:比如一路音频,就是一路轨;一路视频,也是一路轨。两条轨之间是不相交的,单独存放!!两路音频也是两路轨,也是不相交的。
- 流:媒体流,内部包含了很多轨,两者属于层级关系。
# 重要类
MediaStream
:同前面的讲解。RTCPeerConnection
:是整个WebRTC中最重要的类(大而全),包含了很多的功能。对于应用层非常方便,在应用层只要创建了 PeerConnection,然后将流放入 PeerConnection 中即可,其他的逻辑全部由 peerConnection 内部实现RTCDataChannel
:对于非音视频的数据,都通过dataChannel进行传输。其中RTCDataChannel是通过RTCPeerConnection获取的。
# PeerConnection调用过程
PeerConnection中包含两个线程,Worker线程和Signaling线程,可以创建 PeerConnectionFactory
,之后 PeerConnectionFactory
可以创建 PeerConnection
和 LocalMediaStream
和 LocalVideo
/AudioTrack
,通过 AddTrack
将我们创建的多个 Track
轨加入到 MediaStream
流中,通过 AddStream
可以将多个 MediaStream
流(与多方通信,每一方(每一个参与单位)都是对应一个Stream)加入同一个 PeerConnection
中(复用)。
# 调用时序图
- 首先应用层 Application(注意这里Application本身就是一个
PeerConnectionObserver
),创建出一个PeerConnectionFactory
(通过CreatePeerConnectionFactory
); PeerConnectionFactory
触发CreatePeerConnection
、CreateLocalMediaStream
、CreateLocalVideoTrack
、CreateLocalAudioTrack
创建了PeerConnection
、MediaStream
等等实例;- 然后通过
AddTrack
,把各种轨(track)加到流(LocalMediaStream
)中去,然后通过AddStream
,把流加到PeerConnection
中; - 流加到
PeerConnection
之后,会通过CommitStreamChanges
提交流的变化;当流发生变化的时候,会触发OnSignalingMessage
事件,创造出一个offer
【SDP描述信息】; - 有了
offer
【SDP描述信息】之后,就会通过应用层Application,通过信令,发送到远端【Send offer to the remote peer】;
扩展
【SDP描述信息】内容:有哪些音视频数据,音视频数据的格式分别是什么,传输地址是什么等。
- 远端收到数据后,则根据
offerSDP
,回复一个answerSDP
【Get answer from the remote peer】,交给本地信令; - 信令收到远端的
answerSDP
之后,会把信息传给本地PeerConnection
【ProcessSignalingMessage】,本地PeerConnection
就会拿到对方的媒体流信息、传输端口、传输地址;至此,远端和本地就打通连接,可以相互传媒体数据; - 远端数据来的时候,
PeerConnection
还会将远端的流添加到Application中去;【OnAddStream(注意区分AddStream)】
# 声明
- 本文部分内容来源于WebRTC学习(一)WebRTC了解 - 山上有风景 (opens new window),特此感谢。