NACK、PLI、FEC 到底怎么配合:丢包之后,实时音视频系统到底怎么补救
前言:为什么实时音视频里最难受的不是“丢包会不会发生”,而是“丢了以后到底怎么救”
只要开始做实时音视频,几乎很快就会接受一个现实:
丢包不是例外,而是常态。
尤其一旦进入这些环境,丢包几乎迟早要来:
- Wi-Fi 抖动
- 4G / 5G 波动
- 公网跨地区链路
- 弱网移动场景
- 多人会议并发
- 家庭网络被别的设备抢占
所以真正有价值的问题,从来不是:
系统能不能做到完全不丢包?
而更像是:
一旦丢了,系统到底怎么尽量把体验救回来?
这时候你就会碰到几组非常关键的机制:
- NACK
- PLI
- FEC
很多文章会把它们分别解释一下:
- NACK 是重传请求
- PLI 是关键帧请求
- FEC 是前向纠错
这些定义都对,但如果只停在这里,其实还是抓不到工程里的那条主线。
因为真实系统里,这三样东西不是孤零零站着的,而是:
围绕“丢包之后怎么补救”这个核心问题,分别站在不同时间尺度、不同代价模型上的三种手段。
说白了就是:
- 有的适合“赶紧补回来”
- 有的适合“补不回来了,那就尽快重建参考链路”
- 有的适合“别等丢了再说,提前带一点冗余过去”
所以这篇文章的目标,不只是背定义,而是想把它们讲成一个真正的工程决策问题。
重点讲清:
- 为什么光有 jitter buffer 还不够
- NACK、PLI、FEC 各自到底在补什么洞
- 它们为什么不能互相完全替代
- 接收端怎么检测丢包、怎么发 NACK、什么时候该发 PLI
- 发送端为什么要有重传缓存
- FEC 到底是在拿什么换什么
- 如果你自己实现一套简化版丢包恢复链路,代码骨架应该怎么写
而且这篇会继续按你强调的方式来:
核心代码示例会继续给。
我会放:
- 结构图
- 时序图
- 恢复流程图
- 接收端丢包检测代码
- NACK 构造与发送代码
- 发送端重传缓存代码
- PLI 触发逻辑代码
- 一个简化版 FEC 恢复骨架
我希望这篇读完之后,你对“丢包恢复”不再只是记住三个名词,而是真能看到系统遇到坏包之后,是怎么一点点自救的。
一、先给一句最重要的话:丢包恢复不是单一技术,而是一组围绕时延、恢复概率和画面连续性做权衡的策略组合
如果让我先压一句最重要的话,我会这样说:
NACK、PLI、FEC 不是三种孤立功能,而是实时音视频系统围绕“时延预算、恢复成功率、画面连续性”做出来的一组补救策略组合。
这句话特别重要。
因为只要你开始真正做系统,很快就会发现,丢包恢复从来不是一句“补回来”那么简单。
你真正要面对的是这些互相打架的约束:
- 我能不能来得及补
- 补回来还有没有意义
- 现在是只坏了几包,还是整条参考链已经断了
- 我是该请求重传,还是该直接要关键帧
- 我值不值得一开始就多发一点冗余
这也是为什么真实系统不会只靠一种方法吃天下。
它通常是多种手段一起上,只是侧重点不同。
二、先看总图:丢包恢复在整条实时链路里处于什么位置
先上总图。
flowchart LR
A[发送端编码] --> B[RTP发送]
B --> C[网络传输]
C --> D[接收端发现丢包]
D --> E1[NACK请求重传]
D --> E2[PLI请求关键帧]
D --> E3[FEC尝试恢复]
E1 --> F[发送端重传缓存]
F --> G[重发缺失包]
E2 --> H[发送端尽快产生关键帧]
E3 --> I[接收端局部恢复]
G --> J[解码链路恢复]
H --> J
I --> J
这张图里最核心的一点是:
丢包恢复并不是只发生在接收端,也不是只发生在发送端,而是一整条链路的协同行为。
你可以先粗略理解成:
- 接收端负责发现问题
- 反馈机制负责发出求救
- 发送端负责提供补救材料
- 解码链路负责看现在还能不能继续活下去
三、为什么光有 jitter buffer 还不够
很多人看完 jitter buffer,会下意识觉得:
那我等一等、排一排,不就行了吗?
但问题是,jitter buffer 解决的主要是:
- 到得早晚不均
- 顺序乱一点
- 某些包只是稍微迟到
它真正不擅长解决的是:
包彻底没了。
如果缺的包一直不来,jitter buffer 最终还是得面对:
- 等太久,延迟爆炸
- 不等,解码链路可能断掉
也就是说,它更像“秩序修复器”,不是“凭空造包器”。
所以一旦包真的缺失,就必须进入更主动的补救层:
- NACK
- PLI
- FEC
四、NACK 到底在解决什么问题
4.1 NACK 的核心直觉
NACK(Negative ACKnowledgement)你可以先把它理解成:
接收端发现某些包没收到,于是告诉发送端:这些包我缺了,你如果还来得及,就补给我。
它特别适合什么场景?
- 丢的是少量包
- 发送端手里还缓存着这些包
- 当前往返时延没有大到“补回来也没意义”
所以 NACK 的本质,是一种:
来得及的、针对性的事后补救。
4.2 它为什么值钱
因为它很节省。
相比一开始就多发很多冗余,NACK 的思路是:
- 先正常发
- 真丢了再补
这在丢包不重、RTT 不夸张的场景里,通常性价比很高。
五、PLI 到底在解决什么问题
5.1 PLI 不是补单个包,而是在说“参考链路可能已经坏了”
PLI(Picture Loss Indication)不是在说:
- 某一个 RTP 包丢了
而更像是在说:
我这边的视频参考状态可能已经坏掉了,请你尽快发一个关键帧,让我重新建立解码基线。
这就和 NACK 很不一样。
NACK 关心的是:
- 某几个包能不能补回来
PLI 关心的是:
- 就算补几个包,也未必救得回来了,赶紧重开一局吧
5.2 它适合什么情况
比如:
- 丢失的是关键参考数据
- 解码器已经明显花屏 / 无法继续正确参考
- NACK 来不及或者连续失败
- 新加入的接收端需要尽快拿到完整可解码画面
所以 PLI 更像是一种:
请求发送端主动刷新参考链路的恢复手段。
六、FEC 到底在解决什么问题
6.1 FEC 的核心直觉
FEC(Forward Error Correction)可以理解成:
发送端在发送原始媒体数据时,顺手带上一些可用于恢复丢失数据的冗余信息。
它的思路和 NACK 完全不同。
NACK 是:
- 先丢了再说
- 事后请求补发
FEC 是:
- 我先预判可能会丢
- 所以提前带一些修复材料过去
6.2 它为什么值钱
因为它有一个特别大的优点:
不依赖往返重传时延。
也就是说,如果你在特别低延迟、RTT 又比较敏感的场景里,等 NACK 往返可能已经晚了。
这时候 FEC 的提前冗余就会特别有用。
6.3 它的代价是什么
当然不是白来的。
FEC 在拿这些东西换恢复能力:
- 更多带宽开销
- 更多打包和恢复复杂度
- 冗余不是无限强,丢太多照样救不回来
所以它不是银弹,而是一种:
用额外码率换更强实时恢复能力 的手段。
七、一张图看懂 NACK、PLI、FEC 三者的时间尺度差别
flowchart TD
A[包丢失] --> B{能否快速发现并且来得及补?}
B -- 能 --> C[NACK请求重传]
B -- 不能 --> D{参考链是否已损坏?}
D -- 是 --> E[PLI请求关键帧]
D -- 否 --> F{是否已有FEC冗余可恢复?}
F -- 是 --> G[FEC本地恢复]
F -- 否 --> E
这张图不是绝对流程,但很适合建立直觉:
- NACK 偏“快速补洞”
- PLI 偏“重建参考基线”
- FEC 偏“提前带保险”
它们不是一个维度上的替代关系。
八、第一段核心代码:接收端怎么检测 RTP 丢包
代码先上。
1 | |
这段代码的意义
它干的事很朴素,但特别关键:
通过 RTP sequence number 发现“中间少了谁”。
这就是很多恢复动作的起点。
如果连“少了哪个包”都不知道,后面根本谈不上 NACK。
九、第二段核心代码:接收端怎么组织 NACK 请求
检测到丢包之后,下一步通常不是马上狂发反馈,而是先组织成更像样的请求。
1 | |
这里最该看什么
最该看的是这几个工程动作:
- 丢包不是只记一次就完
- NACK 通常会有重试次数限制
- 一旦补到了,要把它从等待恢复集合里移掉
这就已经不是“发个请求”那么简单,而是一个小型恢复状态机了。
十、第三段核心代码:NACK 通过 RTCP 发送时,系统逻辑长什么样
下面给一个简化版“接收端主循环”骨架。
1 | |
这段代码说明什么
说明 NACK 不是独立在系统外面的。
它通常会和这些模块一起工作:
- RTP 接收
- 丢包检测
- jitter buffer
- RTCP 反馈发送
也就是说,恢复动作是实时链路里的一等公民,不是补丁。
十一、第四段核心代码:发送端为什么必须有重传缓存
NACK 要想有意义,发送端不能“听到你缺包了,但我早删了”。
所以发送端通常要保留一段时间的可重传历史。
1 | |
这里的关键工程含义
为什么不能无限缓存
因为:
- 内存不是无限的
- 太老的包即使补回去,也未必还有意义
- 实时系统不值得为了极晚的恢复一直背历史包袱
所以通常要有一个:
按时延预算决定的重传缓存窗口。
十二、第五段核心代码:发送端收到 NACK 之后怎么重传
1 | |
这段代码最关键的点
你要看到的是:
NACK 的价值完全建立在发送端“还留着旧包”且“还能快速再发一次”之上。
这也是为什么高 RTT 场景下,NACK 有时就会开始变得不划算。
因为等你补回来,解码时机可能已经过了。
十三、第六段核心代码:什么时候该从 NACK 升级到 PLI
这是特别有工程味的一段。
因为系统不可能永远执着于“再补一次试试”。
如果参考链路已经坏了,继续 NACK 可能只是浪费时间。
1 | |
这里体现的工程判断
什么情况适合发 PLI
- 某些关键包一直补不到
- 解码器已经明确报告参考损坏
- 花屏 / 马赛克已经持续
- 新用户刚加入但缺少可解码关键基线
也就是说,PLI 更像一句:
别补零件了,直接给我一块新的底板。
十四、第七段核心代码:PLI 发送与关键帧触发流程怎么组织
1 | |
这段代码很短,但意义很大
它说明:
- 接收端不是自己生成关键帧
- 它只能通过反馈告诉发送端:赶紧刷新参考链
- 真正产生关键帧的是发送端编码器
这就是控制面和媒体面的分工。
十五、第八段核心代码:FEC 的最小直觉实现骨架
FEC 真要完整展开会很深,但这篇里我先给你一个最小直觉版,重点是让你理解它怎么工作。
假设我们做一种非常简化的异或保护:
- 每 4 个媒体包
- 生成 1 个 parity 包
- 如果恰好丢 1 个,可以用其他包 + parity 包恢复
15.1 发送端生成简化 FEC
1 | |
15.2 接收端尝试恢复
1 | |
这段 FEC 代码最该看懂什么
不是异或细节本身,而是它背后的思路:
- 冗余不是等丢了才要
- 是在发送时提前埋进去
- 这样接收端有机会不等重传就本地恢复
当然真实 FEC 会复杂很多,但这个骨架已经足够把核心直觉立起来。
十六、一张时序图看懂 NACK、PLI、FEC 三种恢复路径的区别
sequenceDiagram
participant S as Sender
participant N as Network
participant R as Receiver
S->>N: media packets
S->>N: optional FEC packets
N->>R: packets with some loss
alt FEC can recover
R->>R: local recovery from redundancy
else Small loss and still in time
R->>S: RTCP NACK
S->>R: retransmission packet
else Reference chain broken
R->>S: RTCP PLI
S->>R: next key frame
end
这张图可以帮你直接抓住三条恢复路径:
- FEC:本地先救
- NACK:请求补洞
- PLI:请求重建基线
十七、为什么三者不能互相完全替代
这个问题非常重要。
17.1 为什么不能只靠 NACK
因为:
- RTT 大时补回来太慢
- 连续丢包时补洞效率会变差
- 某些关键参考一旦错过,后面即使补回来也未必赶得上解码时序
17.2 为什么不能只靠 PLI
因为:
- 关键帧通常更大
- 频繁请求关键帧会显著拉高码率和瞬时压力
- 小范围丢包不值得动不动就“重开一局”
17.3 为什么不能只靠 FEC
因为:
- 冗余要额外吃带宽
- 冗余不是无限强
- 丢包模式复杂时未必恢复得了
- 对带宽本就紧张的场景,FEC 比例不能乱加
所以真实系统常见的思路往往是:
FEC 抵挡一部分即时小损失,NACK 补可来得及的缺口,PLI 负责在参考链断了之后快速重建。
十八、一个更像真实系统的恢复策略图
flowchart TD
A[接收端发现丢包] --> B{丢包规模小且时延允许?}
B -- 是 --> C[发NACK请求重传]
B -- 否 --> D{FEC能本地恢复?}
D -- 是 --> E[FEC恢复]
D -- 否 --> F{参考链是否已损坏?}
F -- 是 --> G[发PLI请求关键帧]
F -- 否 --> H[继续观察/局部跳过]
这张图不是某个标准规定,而是一个很实用的工程脑图。
它提醒你:
恢复策略的核心不是“谁最高级”,而是“现在最划算的补救动作是什么”。
十九、最后给一句更像工程师的话:丢包恢复不是追求“全部救回”,而是追求“在时延预算内把体验尽量救回来”
写到这里,最想落下的一句话其实是:
实时音视频里的丢包恢复,不是追求每个包都完美拿回来,而是在有限时延预算内,把可交流体验尽量拉回来。
这是特别工程化的理解。
因为如果你把目标错设成:
- 每个丢包都必须补到
那系统很容易走向:
- 过度等待
- 重传过多
- 关键帧过频
- 额外带宽被挤爆
- 延迟越来越高
而如果你把目标设成:
- 小损失尽量本地修或快速补
- 参考坏了尽快重建
- 带宽紧张时别过度加冗余
- 整体以体验连续性优先
那这三种机制就会各自回到更合理的位置。
所以真正成熟的实时系统,考虑的不是:
- “包有没有百分百救回”
而是:
- “用户现在还能不能顺畅交流”
这才是更像工程师的最终指标。
二十、总结
最后把这篇收成几句话。
1. NACK、PLI、FEC 是三种不同时间尺度和代价模型下的恢复手段
- NACK:适合快速补小洞
- PLI:适合重建已损坏的参考链
- FEC:适合提前带保险,减少对重传时延的依赖
2. 丢包恢复不是某个点的工作,而是发送端、接收端、反馈控制和解码链路一起协同
这也是为什么它天然是系统问题。
3. 核心代码最该看的是系统骨架
包括:
- 丢包检测
- NACK 生成
- 重传缓存
- PLI 触发
- FEC 编码与恢复
4. 三者不能互相完全替代
真正成熟的系统通常是组合使用,而不是押宝单一机制。
5. 最终目标不是把每个包都救回来,而是在时延预算内把体验尽量救回来
这才是实时音视频里真正合理的恢复目标。
如果你愿意,下一篇我建议直接接:
《关键帧为什么又贵又重要:I 帧、IDR、GOP 与实时视频恢复链路的真实关系》
这篇可以把你前面写过的 I/P/B、GOP、PLI、关键帧请求这些东西,再真正并成一条更强的工程主线,而且我也会继续放代码。