视频丢一帧,画面可能短暂卡住;语音丢掉一个承载 20 ms 音频的包,则可能吞掉一个关键的辅音片段。接收端还必须按时向扬声器交付 PCM:原包即使最终到了,错过播放时刻也无法补回已经听到的声音。

所以讨论音频丢包恢复,先要问:缺失的音频在播放截止时间前,究竟还有没有机会拿到? 有机会,就争取拿到编码数据;没有机会,就生成一段连续的替代声音。本文以本地 WebRTC 源码提交 c8bca1867efcbc5f52adf58ad4463088ed257aa0(2026-10-22)为基线,说明这些选择如何落到 WebRTC 的音频链路中。

一、丢包造成的并不只是“少了一个 RTP 包”

假设每包承载 20 ms 音频,P2 没有按时到达:

flowchart LR P1["P1:0~20 ms<br/>按时到达"] --> P2["P2:20~40 ms<br/>暂时缺失"] --> P3["P3:40~60 ms<br/>按时到达"] P2 --> D{"P2 播放截止前<br/>能取得可用数据吗?"} D -->|"原包或及时重传"| A["正常解码 P2"] D -->|"P3 携带可用冗余"| B["FEC / RED 恢复 P2"] D -->|"都没有"| C["PLC 生成替代音频"]

序号缺口不等于永久丢包。 P2 可能只是乱序或延迟;如果它在截止前进入接收缓冲,仍可正常播放。真正影响听感的是截止时仍没有可用音频。短缺口会损伤辅音、爆破音或瞬态;连续缺口会让预测逐渐失真,出现机械音、音量衰减甚至静音。长包虽减少包头开销,丢一包时缺失的时间也更长。

接收端为了等待乱序包,可以提高抖动缓冲的目标延迟;代价是整条通话更慢。缓冲只能把“迟到但仍赶得上”的包变为可用,无法凭空恢复真正丢失的数据。

二、先看 WebRTC 的实际接收链路

flowchart LR S["发送端 Opus 编码<br/>可选择带内 FEC / RED"] --> R["RTP 网络<br/>乱序、延迟、丢失"] R --> N["接收端 NetEQ<br/>排序、缓冲、目标延迟"] N --> X{"取音频时<br/>该时间段有可用帧?"} X -->|有| Y["正常帧或冗余帧解码"] X -->|无| Z["生成隐藏音频<br/>Expand / 可选 codec PLC"] Y --> O["PCM → 播放"] Z --> O R -.->|"检测序号缺口;配置允许时发 NACK"| Q["RTCP 反馈 → 发送端补发"] Q -.->|"赶上截止时间才有用"| N

NetEQ 不只是一个固定长度的队列。它根据到达情况调整目标延迟,在播放端请求音频时选择正常解码、时间伸缩、丢包隐藏和恢复后的拼接。缓冲偏多时可以加速消耗,偏少时可以预先拉伸;缺帧时执行 Expand,下一段正常音频到来后可能执行 Merge,让接缝不那么突兀。这些操作由 NetEQ 决策逻辑 和 NetEQ 主实现 共同完成。

发送端也能根据网络状况调整 Opus 码率、包时长和 FEC,但音频网络适配器(ANA)的控制策略需要启用并配置。降低码率可能缓解拥塞,却会牺牲编码质量;缩短包时长能缩短单次丢失的声音缺口,却增加每秒的包数与包头开销。这些措施改变的是后续丢包风险和恢复条件,不会把已经丢失的原始音频找回来。Opus 编码器适配层包含相关控制入口。

这里有一个容易误解的区别:libopus 有 PLC,并不表示 WebRTC 默认总是调用 Opus 的 PLC。 在本文源码基线中,AudioDecoderOpusImpl::GeneratePlc 受 WebRTC-Audio-OpusGeneratePlc field trial 控制;未启用时它不产出音频,NetEQ 转用自己的 Expand。启用后,NetEQ 才可能向 Opus 解码器请求 PLC。依据见 Opus 解码器适配层 的 GeneratePlc 和 NetEQ 的 DoCodecPlc。

三、FEC:提前付带宽,换一次及时恢复的机会

Opus 带内 FEC:下一包带上一段语音的低码率冗余

Opus 在合适的编码模式和网络条件下,可以把前一段语音的低码率冗余放进后续包。这种冗余通常称为 SILK LBRR。它不是前一包的字节副本,恢复音质也不必然等同于原包。

flowchart LR A["P1:语音 A"] --> B["P2:语音 B<br/>+ A 的 LBRR"] --> C["P3:语音 C<br/>+ B 的 LBRR"] A -.->|"网络丢失 P1"| L["A 缺失"] B -->|"P2 在 A 截止前到达"| F["先用 P2 冗余解 A<br/>再正常解 B"] L --> F

图是“一包 20 ms、一包一段语音”的教学例子。要救 A,P2 必须到达、确实携带可用冗余,而且接收端要留出等待时间;不能把“打开 FEC”直接等同于“每次丢包都能恢复”。WebRTC 的 Opus 解码器会在发现 FEC 时,把冗余帧以较早的时间戳交给 NetEQ,再交入当前主帧;NetEQ 按播放时间和帧优先级选用。解析路径可见 ParsePayload 和 DecodeRedundantInternal。

发送端需要协商接收能力,并允许编码器生成 FEC。WebRTC 的 SetFec 允许 Opus 使用带内 FEC,损失估计也会传给编码器;最终是否写入冗余,仍取决于码率、模式和内容。SILK 或 Hybrid 可利用这一路径;CELT-only 不应被假定有同样的 LBRR。参见 RFC 7587 §3.3和 WebRTC Opus 编码器适配层。

RTP RED:复制以前的编码块

RED 是 RFC 2198 定义的冗余负载封装格式,不是一套纠错算法。本文源码中的 AudioEncoderCopyRed 通常把上一份已编码音频连同当前音频装入新包;默认保留一层冗余,配置可改变层数。若原包丢失而后续 RED 包及时到达,可以拿到原先的编码块。相比 Opus LBRR,这种复制可能更接近原包内容,但常常消耗更多带宽,并受包大小、时间戳跨度和协商结果限制。依据见 RED 编码器 和 NetEQ RED 拆包。

前一篇参考资料中讲的 ULPFEC、FlexFEC 属于 RTP 包级保护机制;不能因为 RED、ULPFEC 和 Opus FEC 都带有“冗余”,就认为音频通话会自动同时启用它们。 本文的音频实用路径重点是 Opus 带内 FEC 与协商后的音频 RED。

四、NACK:能拿回原包,但要赶上播放时刻

接收端看到序号缺口后,可通过 RTCP NACK 请求补发;发送端必须还保留原包,并且补发流量没有被限制。这里的耗时可以粗略写为:

恢复耗时 ≈ 发现缺口与反馈等待 + 反馈上行 + 发送端排队 + 补发下行

网络路径近似对称时,两段传播之和常用 RTT 估算,但排队与反馈等待还要另算。如果距播放截止只剩 30 ms,RTT 却有 100 ms,补发包即使最终到达,也难以改善这一刻的声音。RTT 小、缓冲较充足时,NACK 对孤立丢包更有价值;大 RTT 或连续丢包时,反复补发还可能挤占新音频的带宽。

本文基线的音频接收端确实有 NACK 路径:ChannelReceive::OnRtpPacket 调用 NackTracker 找缺口并发送反馈;发送端从历史缓存取包并排队补发。但该路径受协商与 NACK 历史配置控制,NackTracker 的筛选也不等于对每个包精确计算扬声器的播放截止时间。依据见 接收端、NackTracker 和 发送端补发。

NACK 与 FEC 不是必须排成“先等 NACK、再尝试 FEC”的串行流水线。NACK 反馈可以在接收包时发出,冗余帧和补发的原帧则可能先后进入 NetEQ;到播放时刻,谁及时且可用,谁才有机会被采用。

五、PLC:数据来不及,就尽量把声音接起来

PLC(Packet Loss Concealment)不会复原未知的原始语音,而是根据已解码的历史生成替代音频。独立使用 libopus 时,传入空包可触发 Opus PLC:SILK 侧使用语音预测、基音和激励状态;CELT 侧会依据历史波形与频谱状态处理短缺口,较长丢失转向噪声和能量衰减。具体策略因编码模式和连续丢失长度而变。RFC 6716 §4.4描述了 Opus 的丢包隐藏。

WebRTC 默认的 NetEQ Expand 同样从解码后的历史音频外推,并在连续缺口中逐渐衰减;恢复正常帧时通过 Merge 平滑衔接。它不需要额外传输数据,因而是截止前没有可用包时的兜底,但对爆破音、说话人突然转变、音乐瞬态和长时间连续丢包都没有“猜中原声”的保证。参见 Expand 与 Merge。

六、把方案放在同一张表里比较

机制获得的内容主要代价更合适的条件主要限制
NetEQ 抖动缓冲迟到、乱序的原包增加播放延迟抖动明显,仍能赶上截止不能修复真正丢失的包
Opus 带内 FEC前一段语音的低码率冗余持续带宽,等待后包中等丢包、后包通常能到、可容忍一段等待不保证每包有冗余;连续丢包效果差
音频 RED以前编码块的副本较高的带宽与包长可用带宽较充足,想提高原数据可用率后包也丢或超时就无效
NACK 重传原编码包反馈和补发流量、RTTRTT 较小、缓冲有余量、偶发丢包过期补发无法改善实时播放
PLC / Expand估计的替代 PCM无额外网络带宽低延迟要求高,或前面方法都未赶上无法还原原声,连续丢包听感下降

这张表比较的是机制,不是某种固定优先级。实际效果受协商、码率、包时长、RTT、抖动分布与播放目标延迟共同影响。FEC、RED 和 NACK 的开销都要算进总发送预算;链路已经拥塞时,盲目叠加冗余可能使新包也丢失。

七、按场景组合,而不是寻找一个万能开关

flowchart TD A["先测 RTT、抖动、独立/突发丢包<br/>以及播放目标延迟"] --> B{"主要是乱序、迟到?"} B -->|是| C["调整 NetEQ 延迟目标<br/>检查迟到包比例"] B -->|否| D{"RTT 和剩余播放预算<br/>允许重传吗?"} D -->|允许| E["协商音频 NACK<br/>观察补发是否真正被播放"] D -->|不允许| F["评估 Opus 带内 FEC<br/>或协商音频 RED"] C --> G["所有路径最终仍需 PLC 兜底"] E --> G F --> G
  • 低延迟语音通话、RTT 较大:先控制编码包时长与播放延迟;若带宽允许,评估 Opus 带内 FEC。剩余缺口交给 PLC。不要为了等遥远的重传而无限拉长缓冲。
  • 近距离网络、偶发孤立丢包:NACK 可能在截止前拿回原包;仍要观察补发的到达时间,而不只看 NACK 数量。
  • 连续突发丢包:依赖下一包的单层冗余经常一起失效。可以评估多层 RED 或其他保护设计,但会增加带宽与等待;同时应检查拥塞和发送码率,避免把网络问题仅当成解码问题。
  • 音乐或高质量音频:CELT-only 场景不能照搬 SILK LBRR 的收益预期。若音质比带宽更重要,可评估适当冗余与缓冲;瞬态被 PLC 预测错误时,主观听感仍可能明显下降。

验证时,除了网络丢包率,还要记录 NetEQ 的 concealedSamples、concealmentEvents、fecPacketsReceived、fecPacketsDiscarded、缓冲延迟、NACK 数量,以及补发包是否在截止前真正被解码播放。WebRTC 接收统计映射包含这些接收侧指标。收到冗余包不等于成功用它恢复声音,发出 NACK 也不等于补发赶上了播放。测试应分别注入孤立丢包、连续丢包和纯延迟抖动,并在相同总码率与端到端延迟约束下比较听感;否则多花的带宽或多等的时间会被误认为恢复算法更好。

归根结底,WebRTC 的音频恢复是一场与播放截止时间的赛跑:NetEQ 尽量给原包、冗余和补发创造机会;来不及的部分由 PLC 保住连续性。恢复率、实时性和带宽必须一起看。