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的流程很简单:
- 接收端检测到RTP序列号不连续。
- 接收端发送RTCP NACK报文,里面带上丢失包的序列号列表。
- 发送端收到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工作流程:
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估算怎么做,这些我们后面章节再展开。今天先把框架搭好,你回去可以对照自己的系统,看看用的是哪种方案,有没有踩到我说的这些坑。