WebRTC 的一些解释
最近工作上在使用 WebRTC,在公司内部做了技术分享。这里把内容进行脱敏,整理公开。
快速进入 WebRTC
媒体协商 概要
- 每方确定自己的编解码、网络等信息(媒体)
- 每方将自己的信息给到对方(协商)
信令 概要
webrtc 建联前,在各方之间传输【协商数据】
- offer、answer、candidate
webrtc 建联后,在各方之间传输【控制信息】
- mute、leave、join、…
多样化应用场景
webrtc 通道多样:
- 每条 webrtc 通道连接两个端
- 每个端可以有 n 个 webrtc 通道
- 服务器可以成为其中一个端
对【端】的影响:
- 上行 和 下行带宽
- 视频编解码对 CPU/GPU 的消耗
2 人参会,两个【客户端】直接打通,最舒适
4 人参会,每个【客户端】打开多个通道,上下行剧增,硬件消耗剧增
8 人参会,【服务端】作为中转通道,相比 4 人场景,下行剧增,上行剧减,硬件消耗增加
50 人参会,【服务端】作为终极合流通道。相比 8 人场景,客户端回退到 2 人消耗,并把所有消耗转移到【服务端】
Tips:当【服务端】作为 webrtc 的一个通道后,玩法就非常多样了。比如【说话人大屏】、【服务端滤镜】等
流媒体的质量约束
清晰度 vs 流畅度 vs 码率
分辨率影响清晰度,越大越好,流尺寸也会越大。
补充:同样分辨率在不同的显示屏上,也会有清晰度的差异。详见 分辨率解释,介绍了【光源】、【虚拟像素】、【像素软件化】、【高清屏】对图片的影响。
帧率影响流畅度。视频由 帧 画面组成,同一秒内 帧 画面越多,肉眼感知越流畅,流尺寸也会越大。
电影:24fps,电视机:30 fps,电脑 & 手机:60 fps - 120 fps
码率:每秒传输的比特数据量。单位:[number] bps
码率 = 分辨率 × 比特深度 × 帧率 × 压缩比
比特深度:描述一个 px 所需要的 bit。通常 RGB 为 3 色彩通道,每个色彩需要 8 个 bit,即 24 bit。
压缩比:H.264/H.265 压缩效率极高,根据视频画面的【前后帧之间的 px 差异】,极大降低视频流尺寸。示例:(场景设定:分辨率:1080p,色彩通道:3,帧率:30,编码:H.264)
码率 = 1920 × 1080 × 24 × 30 x 1/100 = 14.93 Mbps = 14930Kbps = 1866.25KBps = 1.87 MBps
即:每秒需要 1.87 M 的流量
在 webrtc 中,分辨率和帧率都是由开发人员控制的(主动调用 sdk api 将 stream 给到 sdk)。其中:
- stream 本身具有尺寸,即 分辨率。
- 调用 sdk api 的频次,即 帧率。
webrtc 控制码率有两种行为:
- 主动控制:当发现网络状态不佳,webrtc 会通过内部算法自行调整码率,包括自行调整码率,甚至直接丢弃数据包。
- 被动约束:开发人员预设期望码率范围,但【主动控制】优先。
编解码
H.264/5 等编码。很重要,但还不会。这里留坑。
媒体协商的多样性
SDP 是什么
SDP - Session Description Protocol(会话描述协议)。用于在两个通信终端之间描述细节。
强制依赖【协议描述规则】进行解析
a - b 之间约定了 n 条通信规则,就有 n 种 SDP 通信数据。
SDP - offer/answer
SDP Format:
1 | 示例//=============会话描述==================== |
SDP Raw Example:
1 | v=0 |
SDP Raw Example Desc:
1 | //==会话描述== |
SDP - Candidate
candidate: 一个可能的网络连接点的候选项。网络节点负责最终的音视频流传输的数据通道,很重要。
sdpMid/sdpMLineIndex: 用来标记 SDP 中的媒体流索引。即每一个 candidate 候选,都属于某一个媒体流通道,如 音频流、视频流等。
1 | { |
下面示例,是使用 Cloudflare TURN 服务采集到的一个网络发现节点:
candidate:4109260943 1 udp 8331263 104.30.136.76 36634 typ relay raddr 59.87.240.206 rport 63859 generation 0 ufrag 2D6J network-id 1 network-cost 10
tips:
- TCP or UDP:webrtc 使用 SRTP 来传输音视频流,该应用层协议可同时在 TCP 和 UDP 上工作。其中,默认使用 UDP。只有 UDP 不可用后会回退到 TCP。
- N 个 candidate 都是候选并被 webrtc sdk 存储。当 webrtc 使用过程中【中断】了,会自行寻找其他的 candidate 作为候选,对开发者透明(自动重连)。
Candidate Raw Example:
1 | [ |
Candidate lines:
1 | candidate:2524315334 1 udp 2122260223 10.0.3.39 59309 typ host generation 0 ufrag 2D6J network-id 1 network-cost 10 |
网络发现 - STUN
以上媒体协商的过程中,SDP 的很多信息都可以由开发者控制,比如编码类型。但是 candidate 网络节点的收集会比较复杂。
数据包如何进入公网
NAT 路由映射表:
NAT 的多样性
路由器具有特定的类型:完全锥形、地址限制锥形、端口限制锥形、对称 NAT
映射表规则:
- 映射端口:
- 对称 NAT → {source_ip, source_port, dest_ip, dest_port} 有一个变化,将新建映射端口
- 端口限制锥形 / 地址限制锥形 → {source_ip, source_port} 有一个变化,将新建映射端口
- 回流数据包:
- 对称 NAT / 端口限制锥形:被【映射表】映射过的 {dest_ip, dest_port} 可以回流
- 地址限制锥形:被【映射表】映射过的 {dest_ip} 可以回流
P2P 打洞
对于 p2p 打洞失败的场景,就需要 TURN 做中继服务,转发音视频流。
中继服务 - TURN
协商回退
在 ipv6 越多的场景下,上面提到的 ipv4 NAT 问题可以有效解决。但 STUN 网络发现依旧不可缺少。local、多 ipv6、防火墙等场景依旧需要网络择优。
ipv6 场景下,不需要打洞就能直连,最具有性价比。以下场景可以专门做【协商回退】处理:
- server 端 ipv6 和 ipv4 共存,那么 client 端只要有 ipv6 就明确使用 ipv6
1
2
3
4
5
6
7
8pc.onicecandidate = event => {
if (event.candidate) {
if (event.candidate.address.indexOf(':') !== -1) {
// IPv6候选者,可能优先处理
handleIPv6Candidate(event.candidate);
}
}
}; - 回退(ipv6 → ipv4 → turn 中继)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22pc.oniceconnectionstatechange = () => {
switch(this.pc.iceConnectionState) {
case 'checking':
// 设置连接超时
connectionTimer = setTimeout(() => {
this.handleConnectionTimeout();
}, this.connectionTimeout);
break;
}
};
async handleConnectionTimeout() {
console.log('连接超时,尝试回退策略');
if (this.preferredProtocol === 'IPv6') {
console.log('切换到IPv4');
this.preferredProtocol = 'IPv4';
await this.restartConnection();
} else {
console.log('尝试TURN服务器');
await this.switchToTURN();
}
}
信令服务的演变
- 自建 IM - socket /websocket。
- 最复杂,最可控。可以把媒体协商、会控等一系列功能做体系化的整合。
- 私有独立服务使用该场景较多,如某个独立的 app 等。
- SSE 长链接推送 / HTTP 轮询,
- firebase realtime database - db + im
- RTDB 指南
- WHIP & WHEP:正在逐渐形成标准,普遍适用于各大推流平台。(适用于中央服务器,因为服务器具有固定的 sdp - candidate)
- 通过 restful api 控制媒体协商和会话生命周期
- 可以通过 header、body 增加授权、编码定义、期望格式和分辨率等等参数
- 平台场景使用较多,如直播平台、云厂商。
1
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
32
33
34
35
36
37
38
39Example:
https://webrtcpush.tlivewebrtcpush.com/webrtc/v2/whip
// WHIP 服务
const server = http.createServer((req, res) => {
// 处理 WHIP 客户端请求
switch (req.method) {
case "POST":
// 生成 SDP Answer
const sdpAnswer = generateSdpAnswer(sdpOffer);
res.statusCode = 201;
res.setHeader("Location", `/whip/sessions/${randomUUID()}`);// 用于 delete
res.end(sdpAnswer);
case "PATCH":
// 处理 ICE 候选
let iceCandidate = req.text();
handleIceCandidate(iceCandidate);
case "DELETE":
// 处理会话删除
deleteSession(req.sessionId);
case "OPTIONS":
// 处理能力协商
res.setHeader("Allow", "POST, PATCH, DELETE, OPTIONS");
res.setHeader("Accept", "application/sdp");
}
res.end();
});
function generateSdpAnswer(sdpOffer) {
return `v=0 o=- 123456789 123456789 IN IP4 0.0.0.0 s=- t=0 0 m=video 9 UDP/TLS/RTP/SAVPF 96 c=IN IP4 0.0.0.0 a=rtpmap:96 VP8/90000 a=setup:active a=mid:0 `;
}
function handleIceCandidate(iceCandidate) {
console.log("Received ICE candidate:", iceCandidate);
}
function deleteSession(sessionId) {
console.log("Deleting session:", sessionId);
}
RTC DataChannel
当 webrtc 的两个端媒体协商完成后,可通过 webrtc 在两个端创建 全双工 的通信通道,即 RTC Data Channel。
应用场景:
- 聊天、简单的会控
- 会议白版、云游戏
- 飞书完成了高效的协同编辑 - 妙享 Magic Share
- 会议中,A 进行文档共享(非投屏)
- 其他人员默认跟随 A 的操作进行文档阅读(根队)
- 其他人员可自行操作、编辑文档(离队)
- 核心算法还是云文档协作,但通过 DataChannel 非常高效的完成会议结果的沉淀。
RTC SFU - MCU 架构
在不同的参会规模场景下,有多种流传输方案。
P2P 打洞场景最特殊,因为是终端直连,服务器无法监控通话质量。这种场景也最安全,视频流不会被服务器侧拦截和分析。
- telegram 在 webrtc 自身的 dtls 流加密基础上,增加了 MTProto 端到端 加密。可以避免被中间服务分析流数据。
MCU 方案,server 需要对大量的【视频流、音频流】进行合流,增加了延时。可控性也最高,可以做各种场景的滤镜、变声、流组合特效等。
- mcu 方案,不支持端到端加密。
现实场景下,除了视频聊天这种 1-1 场景对 webrtc 是强依赖外,其他流媒体服务如直播,都是多协议共存。
- 推流:webrtc、rtmp、基于 webrtc 的私有协议 (优化延迟)
- 拉流:webrtc、hls、rtmp、flv
- 辅助:直播拉流场景,CDN 必不可少
WebRTC 源码宝藏
WebRTC 中 3A (AEC、AGC、ANC) 音频算法最顶级处理音频的算法,可以直接拿来用。
对于网络方向,网络带宽的评估、平滑处理、网络协议的实现在 WebRTC 中是应有尽有。