3. 丢包与重传机制:NACK与PLI/FIR请求、RTCP反馈机制、FEC前向纠错

丢包,是RTP部署里最让人头疼的问题之一。你想想看,视频卡成PPT,音频断断续续,用户直接摔手机。我这些年排查过的丢包问题,少说也有上百个了。说白了,丢包不可怕,可怕的是不知道怎么应对。

这一章,我们就来聊聊RTP世界里对抗丢包的几把“武器”。我会结合我自己的实战经验,把NACK、PLI/FIR、RTCP反馈,还有FEC这些技术掰开揉碎了讲清楚。

3.1 丢包了,谁先知道?—— RTCP反馈机制

在讲具体怎么重传之前,得先搞清楚一个问题:谁负责发现丢包?

答案是RTCP。RTP只管往外扔数据包,它自己是不管丢不丢的。丢包检测这件事,全靠RTCP的反馈报文。

RTCP里有两个关键角色:

  • 发送端报告(SR):发送端定期发,告诉接收端“我发了多少包,时间戳是多少”。
  • 接收端报告(RR):接收端收到SR后,回复一个RR,里面包含了丢包率、累计丢包数、抖动、往返时延(RTT)等信息。

我记得有一次,一个客户说他们的视频会议系统偶尔会花屏,但网络监控显示丢包率不到0.1%。我一看RR里的抖动值,好家伙,峰值到了80ms。这就是典型的突发丢包,平均丢包率不高,但瞬间丢一堆包,画面直接就崩了。

核心观点:RTCP的RR报文是丢包检测的“眼睛”。不要只看平均丢包率,要关注抖动和突发丢包模式。

3.2 丢了就补:NACK与PLI/FIR请求

发现丢包之后,怎么补?这里分两种情况:

3.2.1 NACK(否定确认)—— 精准补包

NACK,全称Negative Acknowledgment。说白了就是接收端告诉发送端:“哥们,序号X、Y、Z这几个包我没收到,赶紧再发一遍。”

NACK的流程很简单:

  1. 接收端检测到RTP序列号不连续。
  2. 接收端发送RTCP NACK报文,里面带上丢失包的序列号列表。
  3. 发送端收到NACK后,从发送缓冲区里取出对应的包,重新发送。

这里有个坑,我踩过。发送端不能无限制地重传。如果网络本来就差,重传只会加剧拥塞。所以,重传次数和超时时间必须有限制。我个人习惯是:最多重传3次,每次间隔递增(比如第一次等20ms,第二次等40ms,第三次等80ms)。如果还丢,那就放弃,交给FEC或者上层应用去处理。

实战技巧:NACK请求里可以带一个“延迟”字段。如果接收端发现丢包,但知道后面还有包没到,可以等一小会儿再发NACK,避免因为包乱序而误触发重传。这个在WebRTC里叫“NACK聚合”。

3.2.2 PLI/FIR —— 关键帧请求

NACK只能补单个包。但如果丢包太严重,或者关键帧(I帧)丢了,那补几个包也没用。这时候就需要PLI(Picture Loss Indication)或FIR(Full Intra Request)了。

  • PLI:接收端告诉发送端:“我画面丢了,你发个关键帧让我刷新一下。”发送端收到后,会在下一个编码周期强制生成一个I帧。
  • FIR:和PLI类似,但更“强硬”。它通常用于MCU(多点控制单元)场景,要求发送端立即生成关键帧,不能延迟。

我曾经在一个视频监控项目里遇到过一个问题:摄像头每30秒才发一个I帧,中间全是P帧。一旦网络抖动丢了一个P帧,画面就花了,要等30秒才能恢复。用户投诉说“监控画面像幻灯片”。后来我让摄像头每2秒发一个PLI请求,强制刷新,问题就解决了。当然,代价是码率涨了20%。

注意:频繁请求PLI/FIR会导致码率飙升,因为I帧的数据量通常是P帧的5-10倍。在带宽有限的场景下,要设置PLI请求的冷却时间(比如至少间隔1秒)。

3.3 未雨绸缪:FEC前向纠错

重传是“事后补救”,FEC则是“事前预防”。FEC的原理很简单:发送端在发送原始数据包的同时,额外发送一些冗余包。接收端即使丢了一部分包,也能通过冗余包把原始数据恢复出来。

FEC的好处是低延迟——不需要等待重传,直接本地恢复。坏处是浪费带宽——冗余包本身也占带宽。

3.3.1 ULPFEC(Uneven Level Protection FEC)

ULPFEC是RTP标准里最早定义的FEC方案之一。它的核心思想是:对不同重要性的数据,提供不同级别的保护。

比如,一个视频帧里,I帧比P帧重要,P帧比B帧重要。ULPFEC可以给I帧分配更多的冗余包,给B帧分配更少的冗余包。这样在带宽有限的情况下,能最大化保护关键数据。

ULPFEC的报文格式比较“古老”,它把FEC数据直接封装在RTP扩展头里。我早期做IPTV项目时用过它,效果还行,但有个问题:扩展性差。一旦需要调整保护级别,就得重新协商SDP,很麻烦。

3.3.2 FlexFEC(Flexible FEC)

FlexFEC是后来WebRTC社区推的方案,也是我现在最常用的。它比ULPFEC灵活得多:

  • 保护粒度可调:可以按包保护,也可以按帧保护,甚至可以按媒体流保护。
  • 编码方式可换:支持XOR(异或)、Reed-Solomon(里德-所罗门)等多种编码。
  • 与重传协同:FlexFEC可以和NACK配合使用。比如,先尝试FEC恢复,如果恢复失败,再走NACK重传。

下面这张图,是我自己总结的FlexFEC工作流程:

FlexFEC工作流程图 FlexFEC 工作流程 发送端 原始RTP包 (K个) FEC编码 (生成N-K个冗余包) 网络传输 可能发生丢包 (丢包率 p%) 接收端 收到的包 (M个, M≥K) FEC解码恢复原始K个包 恢复成功? ✅ 成功:直接播放 ❌ 失败:触发NACK重传 说明: FlexFEC 支持与 NACK 协同工作。先尝试 FEC 恢复,如果恢复失败(收到的包 M < K), 再触发 NACK 重传。这样可以兼顾低延迟和可靠性。

3.4 如何选择:重传 vs FEC?

很多新手会问:“到底用NACK还是FEC?”我的答案是:看场景。

场景 推荐方案 原因
低延迟实时通信(如视频通话) FEC为主 + NACK为辅 FEC恢复无等待,延迟低;NACK作为兜底
高带宽直播 NACK为主 带宽充足,重传成本低;FEC会浪费带宽
弱网环境(丢包率 > 10%) FEC + NACK 双保险 单靠重传会雪崩,FEC能分担压力
单向流媒体(如监控录像) FEC 没有反馈通道,无法使用NACK

我的经验:在WebRTC里,我一般配置20%的FEC冗余(即每5个原始包生成1个冗余包)。如果丢包率超过5%,再动态增加FEC比例,同时开启NACK。这个策略在大多数场景下都能兼顾延迟和带宽。

3.5 避坑指南:我曾经踩过的三个坑

最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。

  • 坑一:NACK风暴。有一次,接收端检测到丢包后,同时发了100个NACK请求。发送端瞬间被淹没,CPU飙升,反而导致更多丢包。解决方案:限制NACK请求的并发数,比如每10ms最多发10个NACK。
  • 坑二:FEC与编码器不匹配。FlexFEC的冗余包和原始包必须使用相同的SSRC和序列号空间。我见过有人把FEC包的SSRC设成了另一个值,结果接收端无法关联,FEC完全失效。
  • 坑三:PLI请求过于频繁。一个客户把PLI请求间隔设成了100ms,结果发送端每100ms就生成一个I帧,码率直接翻了5倍,网络瞬间瘫痪。后来我强制要求PLI间隔至少1秒,才稳定下来。

嗯,丢包与重传这块内容,其实还有很多细节。比如FEC的编码矩阵怎么选,NACK的RTT估算怎么做,这些我们后面章节再展开。今天先把框架搭好,你回去可以对照自己的系统,看看用的是哪种方案,有没有踩到我说的这些坑。