WebRTC I: Latency, Codecs & Packet-Loss Resilience
从你动一下到对方屏幕亮起,整段延迟 是流水线上各段耗时之和:
人能感到”实时”的红线约 200 ms(one-way)。每段都要榨干到几十毫秒以内才合格。
| 段 | 典型耗时 | 说明 |
|---|---|---|
| 采集 | 16–33 ms | 一帧周期(60→16ms / 30fps→33ms),相机驱动缓冲 |
| 编码 | 5–30 ms | 取决于编码器、分辨率、CPU |
| 网络(RTT/2) | 20–150 ms | 单程传播 + 排队,跨境大 |
| 抖动缓冲 | 20–100 ms | 故意加的,吸收网络抖动(B09) |
| 解码 | 5–30 ms | 与编码对称 |
| 渲染 | 16–33 ms | 等 vsync 周期 |
总和稍不留神就超 200 ms,所以每一段都在”抠时间”。
人耳对延迟的容忍随场景变化:
电话网(PSTN)历史上瞄准 150 ms one-way(ITU-T G.114 建议)。WebRTC 把同样的目标带到互联网,用 UDP + 自适应码率 + 抖动缓冲达成。
延迟不是均匀分布的,而是集中在某几段。优化要找瓶颈段:
TCP 保证可靠、有序,靠的是重传 + 等待。一旦某个包丢失,发送方继续发,但接收方应用层看不到后续包——它们在 TCP 缓冲区里排队等丢失的那个补回来。这种”前面一个卡住后面全部”的现象叫队头阻塞(Head-of-Line blocking, HoL)。
丢包 后,即使 已正确到达,应用层也要等 1 个 RTT(重传往返)才能看到 :
发送: P1 ✗(丢) P2 P3 P4 ...
接收缓冲: [等P1'] ← P2,P3 已到但应用看不到
1 RTT 后: P1' 到达 → 应用一次性看到 P1' P2 P3 P4
对文件传输这是好事(顺序正确)。对实时视频这是灾难——这 1 个 RTT(跨境可能 200 ms)期间所有”准时到达”的新帧都被一个过期的丢失帧卡住。
视频帧的价值随时间衰减。第 时刻产生的帧,到 就基本没用了(播放早已过了那个时刻)。重传回来的旧帧:
但它的存在让后续新帧全部延迟到达——用一帧没用的旧帧拖累无数新帧,得不偿失。
UDP 不保证可靠、不保证顺序,每个包独立投递。丢一个包,后续包照常到达应用层:
发送: P1 ✗(丢) P2 P3 P4 ...
接收应用: [P2 P3 P4 ...] ← 立刻可用,P1 缺失造成一帧花屏
画面上出现短暂的”花屏”或”卡顿”(解码缺帧),但延迟不累积——后续帧正常播放。对实时通信,宁可偶尔花一下,也不能让延迟滚雪球。
UDP 完全不重传也不行(关键帧丢了就彻底花屏)。WebRTC 在应用层(RTP 之上)实现按需重传:
这种”按延迟预算决策”的重传策略,比 TCP 的”无条件重传”灵活得多。
有意思的是,HTTP/3 的 QUIC 协议也用 UDP——因为 TCP 的 HoL 在多路复用 HTTP 里也是问题。QUIC 在 UDP 上重建了类似 TCP 的可靠传输,但每条流独立,一个流丢包不阻塞其他流。WebRTC over QUIC(正在标准化)会进一步降低延迟。
裸 UDP 只给你一个 payload 和源/目的端口,不知道包的先后、是什么编码、什么时候播。RTP(Real-time Transport Protocol, RFC 3550)在 UDP 之上加一个 12 字节(最小)头部,盖三个关键”章”:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
每发一个 RTP 包序号 +1(绕回)。接收端用它:
初始值随机,避免不同会话的包混淆。
这是 RTP 最重要的字段——给媒体装上时间轴。它不是”墙钟时间”(wall clock),而是采样时钟的计数:
同一帧的所有 RTP 包共享相同 timestamp(一帧切片成多个包),不同帧 timestamp 递增。接收端用 timestamp 决定何时播放和音视频同步。
标识这个包的负载编码格式:
| PT | 编码 | 媒体 |
|---|---|---|
| 0 | PCMU (G.711 μ-law) | 音频 |
| 111 | Opus | 音频 |
| 96+ | 动态类型(H.264/VP8/AV1) | 视频 |
PT < 96 是静态分配(RFC 3551 固定),≥ 96 是动态分配(SDP 协商时绑定)。
每个 RTP 流有个随机 32 位 SSRC(Synchronization Source)。一路通话里有视频流、音频流各自一个 SSRC,接收端靠它分流和同步。
只靠 RTP timestamp 没法跨流同步——视频用 90kHz、音频用 48kHz,时钟基准不同。RTCP Sender Report (SR) 携带 (NTP timestamp, RTP timestamp) 配对:
SR 包: (墙钟时间 T_wall, 该时刻对应的 RTP timestamp T_rtp)
接收端收到 SR 后,可以把任意 RTP timestamp 换算成墙钟时间:
有了墙钟时间,视频流和音频流就能对齐——这就是 lip sync(唇形同步)的实现。
WebRTC 用 GCC(Google Congestion Control,IETF draft-ietf-rmcat-gcc)估带宽。和 TCP 的丢包驱动(loss-based)拥塞控制(如 CUBIC)不同,GCC 主要看延迟变化——在丢包发生前就预判拥塞。
接收端记录每两个相邻包的到达时间差 和发送时间差 (从 RTP timestamp 推),定义延迟梯度:
把一段时间的包组成”块”(典型 60ms 内的包),算块级 ODT 作为信号。
原始 ODT 噪声大(网络抖动),GCC 对最近的 ODT 序列做线性回归,求斜率 :
阈值 自适应(基于噪声估计),避免误判。这是 GCC “提前预警”的关键——在丢包发生前看到排队增长。
GCC 维护一个 AIMD(Additive Increase, Multiplicative Decrease)风格的状态机:
| 状态 | 触发 | 动作 |
|---|---|---|
| Increase | ,过载解除 | 码率按 缓慢加(探测带宽) |
| Hold | 不确定 | 保持当前码率 |
| Decrease | 码率乘 0.85(快速降,避免丢包) |
增得慢、降得快——和 TCP CUBIC 同样的设计哲学,避免震荡。
接收端:测 ODT → 趋势检测 → 估可用带宽 Â_t
↓ 通过 RTCP TWCC(Transport-wide CC)反馈每包到达时间
发送端:融合接收端估计 + 自身基于丢包的估计
↓
编码器:setTargetBitrate(Â_t)
↓
下一批包以新码率发出
TWCC(Transport-wide Congestion Control)是 WebRTC 当前的反馈机制:发送端给每个包打序号,接收端周期性(每 100ms)回报”哪些包何时到达”,发送端据此算 ODT。比旧的 REMB(直接报带宽估计值)更细粒度。
GCC 决定降码率时,编码器通过跳帧、降量化参数(QP)、降分辨率实现:
具体策略是编码器内部的码率控制(rate control),见 B13 的量化环节。
丢包不可避免(UDP),WebRTC 用三种机制按代价分层恢复:
| 机制 | 代价 | 延迟 | 适用 |
|---|---|---|---|
| NACK | 1 RTT 重传 | +RTT | 还来得及的丢包 |
| FEC | 多发冗余 | 0(即时恢复) | 高频小丢包 |
| PLI | 重发关键帧 | +RTT + 编码 | 彻底失步 |
接收端发现 seq 缺失(如收到 5、6、8),向发送端发 NACK:“包 7 没收到,请重传”。
发送: P5 P6 P7(丢) P8 P9 ...
接收: P5 P6 [缺7] P8 P9 → 发 NACK(7)
发送收到 NACK → 重发 P7
接收: ... P7' → 补上
关键判据:剩余播放时间。接收端的抖动缓冲(B09)会给每个包留一段缓冲时间 。如果 ,重传来得及,发 NACK;否则放弃,等下一帧。
NACK 是 RTP/RTCP 的反馈消息(RFC 4585),可以多次重发(同一个 NACK 重复发几次防 NACK 本身丢失)。
不靠重传,提前多发一点冗余数据,接收端用冗余就地算回丢失的包,0 额外延迟。
最简单的 FEC 是 XOR 编码:发 个源包 后,发一个冗余包 。任一包丢失,用 和其余 个异或还原:
代价:多发 1 个包(带宽 +)。WebRTC 用更灵活的 Reed-Solomon 或 UlpFEC(RFC 5109)做分层冗余。
FEC 适合网络稳定但偶发小丢包的场景(无线网络常见 1-5% 随机丢包)。但 FEC 的冗余是”未丢也付”——好网络下浪费带宽,所以 WebRTC 根据当前丢包率动态调 FEC 强度。
当关键帧(IDR)或它的依赖帧丢失,后续 P 帧全部失参考,解码器彻底失步,画面持续花屏。NACK/FEC 都救不了(太多了)。
这时接收端发 PLI:“我整个画面崩了,请立刻发一个全新的 IDR 关键帧”。
发送端收到 PLI 后强制编码一个 IDR(即使码率控制觉得不该发),解码器用它重置参考帧状态,画面恢复。
代价:IDR 帧很大(比 P 帧大 10×),瞬时码率飙升可能加剧拥塞,形成”丢包→PLI→大帧→更堵”的恶性循环。所以 PLI 是最后手段。
WebRTC 的丢包恢复是自适应分层:
策略由丢包率、RTT、抖动缓冲深度共同决定。Opus 音频还内置了自己的 FEC(见 B18),独立于视频层的 FEC。
网络包到达间隔不均匀——有的紧挨着来、有的隔很久。这种到达时间的不一致叫抖动(jitter)。如果来一个播一个,播放会一卡一卡(来慢的瞬间没数据播)。
抖动缓冲(jitter buffer)在接收端故意囤一段时间的数据,再以恒定速率喂给解码器,吸收抖动。
包在网络里走的延迟 不是常数,而是 ,其中 是抖动项。来源:
抖动让 RTP timestamp(发送节奏)和到达节奏不同步:
发送: |--包1--|--包2--|--包3--|--包4--| 均匀 20ms
到达: |包1|----包2、3挤一起----|--包4--| 忽快忽慢
播放器维护一个队列,包到达先入队,按 RTP timestamp 顺序出队播放:
包到达 → [JitterBuffer 队列] → 以恒定 20ms 间隔出队 → 解码 → 播放
关键参数是播放延迟 ——包到达后等多久才播。等的时间 = 缓冲深度。如果 是最大抖动,要 才不卡顿。
缓冲越大越流畅(吸收更大抖动),但端到端延迟越大(B04 的 增大)。缓冲越小越实时,但抖动超过缓冲就卡顿。
这是 WebRTC 最核心的工程权衡之一:
固定缓冲深度既不灵活也浪费——网络好时囤太久没必要,网络差时又不够。WebRTC(如 Google 的 NetEq for audio)用自适应缓冲:
调整时为避免”突然变速”导致音调变化,音频用 时间拉伸(WSOLA、PIC 等算法)微调音频长度——轻微拉伸或压缩几毫秒,听感自然,实现平滑过渡。
WebRTC 音频用 NetEq(Google 的专利算法),不只是缓冲,还做:
一路未压缩 1080p 视频:
家宽通常 10–100 Mbps,连这一路的零头都传不动。所以必须压缩——而压缩能成功,是因为视频里有海量冗余。
一帧画面里相邻像素往往相似——蓝天、白墙、纯色背景,大块区域像素值几乎相同。JPEG 这类帧内压缩就是去这个冗余(DCT 把低频能量集中到少数系数,详见 B13)。一帧能压 10–50 倍。
相邻帧之间往往变化很小——背景不动、只有人在动。第 帧和第 帧的差异远小于整帧数据。视频编码器不存整帧,只存差异(残差 residual),这是视频比图片能压更多倍的核心。
数学上,相邻帧的互信息 接近帧自身的熵 ,即 ——给定前一帧,当前帧的”新信息”很少。
光存”差异”还不够,因为画面在动——同一个物体在两帧里位置不同,直接相减残差仍很大。运动补偿(motion compensation)先估计物体的位移,再算位移后的差异:
这是 H.26x / VPx / AV1 共同的核心思路,带来了 100–1000 倍的压缩比。
视频压缩是有损的(lossy)。编码器在码率 和失真 之间权衡——这是信息论的率失真理论(rate-distortion theory):
更狠量化(B13)→ 更低码率但更糊( 增大)。编码器的任务就是在 和 之间找最佳折中点,对每个宏块单独决策。
视频码流不是一串同质的帧,而是三种类型的混合,区别在参考关系:
B 帧的双向参考要求编码器先有未来帧才编码当前帧,这导致两个问题:
I B B P B B P 需要至少 2 帧的”前瞻”(lookahead),在 30fps 下是 66 ms——直接吃掉 B04 的延迟预算。I P B B,显示顺序 I B B P)。这要求 RTP 按解码顺序发包,接收端解码后再重排序显示——增加缓冲和复杂度。对实时通信,省下的那点带宽不值得用 66+ ms 延迟换。所以 WebRTC 的编码器(即使支持 B 帧)默认禁用,只用 I P P P P P ... 的结构:
I P P P P P P P P P I P P P ... ← 每隔一段来一个 IDR 重置
IBBPBBP 结构),压缩效率最高。这是同一编码器(如 x264)在不同场景下的配置差异——不是协议限制,是延迟预算决定的工程选择。
I P P P ... P I P P ... 两个 I 帧之间的距离叫 GOP(Group of Pictures)长度:
WebRTC 通常用动态 GOP——正常时长时间不主动发 I(节省带宽),只在收到 PLI 或场景切换时才发。
H.264/VP8/AV1 内部把一帧画面编码成比特的流程高度一致:
帧 → 1.分块 → 2.预测(运动估计) → 3.残差 → 4.变换 → 5.量化 → 6.熵编码 → 比特流
把画面切成小方块。H.264 用固定 16×16 的宏块(macroblock);H.265/AV1 用更大的 CTU(Coding Tree Unit,最大 64×64)并自适应细分(四叉树)。
帧 (1920×1080) → 16×16 宏块 → 8160 个块 (120×68)
每个块独立处理——这是为什么不同区域可以有不同压缩强度(静态大块、动态小块)。
每个块预测一个估计 ,分两种:
运动估计是计算最重的环节——对每个块在参考帧的搜索窗口里枚举所有位移,算 SAD(Sum of Absolute Differences):
SAD 最小的位移就是运动向量。1080p 一帧的 ME 计算量极大,硬件编码器靠专用 ASIC 加速。
预测值 和原值 的差:
如果预测准(静态场景),,几乎不传数据。这是压缩的主要来源。
对残差做 DCT(离散余弦变换,H.264)或整数近似变换:
DCT 把图像能量集中到左上角少数低频系数——大部分系数接近 0,便于压缩。直觉:自然图像的低频成分(大面积平滑)能量高,高频成分(细节边缘)能量低。
把变换系数除以量化步长 取整:
这一步是有损的源头——大量小系数被量化成 0(直接丢弃)。 越大压缩越狠、画质越糊。
由 QP(Quantization Parameter)控制,H.264 里 。QP 每加 6,量化步长翻倍;QP 每加 1,码率约降 12.5%(经验法则)。码率控制算法(见 B07 GCC 触发的调整)就是动态选 QP 达到目标码率。
把量化后的系数无损压成最短比特。H.264 用 CAVLC 或 CABAC(基于上下文的自适应二进制算术编码):
CABAC 比 CAVLC 压缩率高 5–15%,但计算更重——这是高 profile 的代价。
每一步编码器要决策(用什么预测模式、多大块、什么 QP),目标是在码率 和失真 间最优。H.264 用 RDO(Rate-Distortion Optimization):
拉格朗日乘子 由 QP 决定,平衡”画质损失”和”码率开销”。这是编码器性能差距的核心——x264 的 RDO 比简易编码器省 20%+ 码率。
熵编码产生的比特串本身没有边界——一帧的尾和下一帧的头混在一起。H.264 用 NAL(Network Abstraction Layer)把比特流切成一个个NAL 单元(NALU),每个单元是带类型的自描述块。
最常见的字节流格式(Annex-B,文件存储用)用起始码(start code)分隔 NALU:
00 00 00 01 | NAL头(1B) | NAL负载(RBSP) | 00 00 00 01 | NAL头(1B) | ...
起始码 00 00 00 01(4 字节)或 00 00 01(3 字节,少见)。它保证在码流里唯一——编码时若负载里出现 00 00 03 就插入一个 03 字节(emulation prevention),破坏假起始码,解码时再去掉。
WebRTC 走 RTP 时不用 Annex-B 起始码——每个 NALU 直接放进一个 RTP 包的 payload(或用 FU-A 分片大 NALU)。RTP 头部的长度字段已经标明边界,不需要起始码。这是为什么 WebRTC 的 H.264 payload 格式(RFC 6184)和文件的 Annex-B 不同:
文件: SC | NALU1 | SC | NALU2 | ...
RTP: [RTP头] [NALU1] 单包单 NALU
[RTP头] [FU-A指示] [NALU1分片1] [NALU1分片2] ... 大 NALU 分片
NALU 头 1 字节里的 type 字段(5 位)决定负载类型(H.264 表 7-1):
| Type | 名称 | 含义 |
|---|---|---|
| 1 | 非 IDR 片 | 普通 P 帧的片(slice) |
| 5 | IDR 片 | 关键帧的片(IDR = I 帧重置参考) |
| 6 | SEI | 补充增强信息(元数据) |
| 7 | SPS | Sequence Parameter Set:分辨率、帧率、编码配置 |
| 8 | PPS | Picture Parameter Set:量化矩阵、熵编码模式 |
Type 1 vs 5 的区别:都是”片”数据,但 type 5 是 IDR(强制重置),type 1 是依赖前面参考的普通片。
解码器要解码任何片(type 1 或 5),必须先有对应的 SPS 和 PPS:
没有 SPS/PPS,解码器不知道怎么解释后续比特。所以 WebRTC 协商建立连接后,发送端先发 SPS、PPS,再发 IDR,三者构成可解码的最小集合。PLI(B08)触发重发时也连带重发 SPS/PPS/IDR。
NAL 单元头部正好 1 字节,分成 3 个字段:
0 1 2 3
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+-+-+-+-+-+-+-+-+
| 比特 | 字段 | 长度 | 含义 |
|---|---|---|---|
| 7 | forbidden_zero_bit | 1 | 恒为 0。若为 1 表示该 NALU 有错(如传输错误) |
| 5–6 | nal_ref_idc (NRI) | 2 | 重要性:3=最高(参考价值大),0=可丢弃 |
| 0–4 | nal_unit_type | 5 | NAL 类型(见 B14 表) |
解码器读 type 字段(低 5 位)决定如何处理:
| 字节 (hex) | 二进制 (F NRI Type) | type | 含义 |
|---|---|---|---|
| 0x67 | 0 11 00111 | 7 | SPS,NRI=3(最重要) |
| 0x68 | 0 11 01000 | 8 | PPS,NRI=3 |
| 0x65 | 0 11 00101 | 5 | IDR 片,NRI=3 |
| 0x61 | 0 11 00001 | 1 | 非 IDR 片,NRI=3 |
| 0x41 | 0 10 00001 | 1 | 非 IDR 片,NRI=2 |
| 0x01 | 0 00 00001 | 1 | 非 IDR 片,NRI=0(最低) |
验证 0x67 = 0110 0111:bit 7 = 0(forbidden)、bits 5–6 = 11(NRI=3)、bits 0–4 = 00111(type=7)✓。
NRI 标记这个 NALU 的”参考价值”:
WebRTC 在拥塞时可以按 NRI 优先级丢包——先丢 NRI=0 的(影响小),保住 NRI=3 的(关键帧和参数集)。这就是不同 NALU 字节不同的原因:编码器事先标记好重要性,让传输层据此调度。
用位域(bit field)而不是整数标记类型,是为了省字节——NAL 头只占 1 字节,对大量小 NALU(每帧几十个)累积可观。H.265 把头扩到 2 字节是因为类型空间不够(新增更多 slice 类型、 temporal layer 等)。
大 NALU(如关键帧几十 KB)超过 MTU(约 1200 字节)时,WebRTC 用 FU-A(Fragmentation Unit type A)把它切成多个 RTP 包:
每个分片 RTP payload = [FU-A 指示字节(1B)] [FU-A 头(1B)] [NALU 分片数据]
FU-A 头: |S|E|R| Type|
S=1 首片, E=1 尾片, R 保留, Type 是原 NAL 类型
接收端按 S/E 标记重组:收齐所有分片后拼回完整 NALU。中间分片丢失则整个 NALU 作废(依赖它的帧失真)。
00 00 03 要去掉 03 还原 00 00,否则会把 03 误读进头部。但 NAL 头本身在最前面,不会被 prevention 影响——影响的是 RBSP(负载)部分。WebRTC 支持多种视频编码器,候选双方协商(SDP offer/answer)选一个:
| 编码器 | 发布 | 压缩效率 | 硬件支持 | 专利 |
|---|---|---|---|---|
| H.264 (AVC) | 2003 | 基准 | 几乎所有设备 | 收费(MPEG-LA) |
| VP8 | 2008 | ≈ H.264 | 较广 | 免费(Google) |
| H.265 (HEVC) | 2013 | ≈ H.264 的 2× | 主流设备 | 收费,专利混乱 |
| VP9 | 2013 | ≈ H.265 | Chrome/Android | 免费 |
| AV1 | 2018 | ≈ H.265 的 1.3×,比 VP9 强 30% | 普及中 | 免费(AOMedia) |
H.264 是事实标准——几乎所有设备(手机、PC、IoT)都有硬件编解码。WebRTC 把 H.264 作为强制支持的基线,确保任意两端能联通。即使效率不如新编码器,它的”通”价值无可替代。
专利上 H.264 受 MPEG-LA 收费,但浏览器厂商(通过授权)对终端用户免费。
Google 收购 On2 后开源 VP8(2010),作为 H.264 的免费替代。VP8 在 Chrome/Android 上硬件支持广,Firefox 也支持。
VP9 是 VP8 的下一代(YouTube 默认),压缩效率追上 H.265,且支持分层编码(SVC,见 B17)——这对多人会议很有用。
AV1 由 AOMedia(Google、Mozilla、Cisco、Amazon、Netflix、Microsoft 等)2018 年发布,目标”比 VP9 节省 30% 码率,比 H.265 也强”。特点:
WebRTC 把 AV1 作为可选编码器,双方都支持时优先用。
WebRTC 建立连接时通过 SDP(Session Description Protocol)交换能力:
Offer: a=rtpmap:96 H264/90000
a=rtpmap:97 VP8/90000
a=rtpmap:98 AV1/90000
Answer: a=rtpmap:96 H264/90000 ← 只选 H.264(不支持其他)
双方取交集,按优先级选最高的共同支持编码器。这就是为什么有时高清有时糊——协商结果是 H.264 还是 AV1 直接决定同样带宽下的画质。
H.265 压缩效率好但:
所以 WebRTC 主流是 H.264(兜底)+ VP8/VP9(Google 系)+ AV1(新王),H.265 几乎缺席。
SVC(Scalable Video Coding)让单路码流里嵌套多层,接收端按网络能力取不同子集:
每层依赖下层(不能丢底层用上层),所以丢上层保底层总能得到可解码的(低质量)画面。
VP9 / AV1 的典型时间分层(3 层,目标 30fps):
帧: T0 T2 T1 T2 T0 T2 T1 T2 T0
层: 0 2 1 2 0 2 1 2 0
IDR P P P IDR
fps: 3.75 7.5 7.5 7.5 ← 只收 T0
11.25 11.25 ← 收 T0+T1
30 ← 收 T0+T1+T2
关键性质:T2 帧只依赖 T0/T1,丢掉 T2 不影响 T0/T1 解码。服务器只转发 T0 给网络差的客户端,画面变成 3.75fps 但不卡顿不花屏。
传统会议:每个人要发 N 路码流给 N 个接收者(或者 SFU 服务器转码)。带宽和 CPU 都爆炸。
SVC 方案:每个人只发一路 SVC 码流(含所有层),SFU(Selective Forwarding Unit)服务器不转码,只按每个接收者的能力选择性转发:
发送端 A → [T0 T1 T2 全层] → SFU
├→ 给网络好的 B:转发 [T0 T1 T2](30fps 高清)
├→ 给网络差的 C:只转发 [T0](3.75fps 流畅)
└→ 给移动端 D:转发 [T0 T1](中等)
服务器只做”按需丢层”的转发,零编码成本——这就是大型视频会议(Zoom、Google Meet)能扛几十人的核心机制。
两种相关但不同的技术:
SVC 带宽更省(层间共享数据),但编码更复杂;Simulcast 编码简单(多路独立编码),但带宽 ×3。WebRTC 同时支持两者,VP9/AV1 偏 SVC,H.264 因不支持 SVC 通常用 Simulcast。
时间 SVC 容易(只是丢帧),空间 SVC(多分辨率嵌套)技术上难——下层用低分辨率预测,上层要用上采样,编码效率有损。所以实际产品多用 Simulcast 模拟空间可分层(发多路不同分辨率),SVC 主要用于时间分层。
Opus(RFC 6716,Xiph.Org + Skype/Microsoft 联合开发)是 IETF 标准化的音频编码器,几乎垄断实时音频。WebRTC 强制要求支持 Opus。它的优势集中在四点:
| 编码器 | 典型帧长 | 算法延迟 |
|---|---|---|
| MP3 | 26 ms | 50+ ms |
| AAC | 20 ms | 40+ ms |
| Opus | 2.5–60 ms 可选 | 5 ms(语音模式) |
Opus 的算法延迟 = 帧长 + 滤波器开销,最短可到 5 ms。这是通话可接受的关键——MP3/AAC 那种几十 ms 延迟在实时场景直接出局。
Opus 单一编码器覆盖 6 kbps 到 510 kbps:
一个编码器从电话音质到 CD 音质通吃,没有切换编码器的麻烦。
Opus 内部是两个编码器的智能切换:
编码器实时分析输入:人声为主用 SILK,音乐/复杂声音用 CELT,也可以两者混合(低频 SILK + 高频 CELT)。这是 Opus “语音音乐通吃”的技术根源。
Opus 自带 in-band FEC:当前帧预测下一帧可能丢,在当前帧里嵌入下一帧的低码率副本。下一帧真丢了,用上一帧里的副本恢复(音质略降但无断音)。
这与 B08 的 RTP 层 FEC 不同——Opus FEC 在编码器内部、对每个包自适应、带宽开销精确控制(只在丢包率高时启用)。接收端解码 Opus 时自动检测并使用 FEC。
DTX(Discontinuous Transmission):检测到静音时停止发包,只周期性发一个 SID(Silence Insertion Descriptor)帧描述背景噪声特性。接收端用 SID 合成”舒适的背景噪声”(comfort noise),听感自然。
通话里平均有 50% 时间某方在静音(对方说话时你不出声),DTX 直接省 50% 音频带宽。这就是为什么”我不说话时网络流量明显下降”。
useinbandfec=1。不启用时退化为 RTP 层 FEC 或纯 NACK。