14、丢包与抗丢包:NACK与RTX、FEC前向纠错、PLI关键帧请求

做WebRTC开发,最头疼的问题是什么?

我个人觉得,丢包绝对排前三。

你想想看,视频通话正聊着,突然画面卡住、马赛克、甚至直接黑屏。用户第一反应就是「这什么破软件」。其实很多时候,问题出在网络丢包上。

今天我们就来聊聊WebRTC里对抗丢包的几把刷子:NACK、RTX、FEC、PLI。这些技术我几乎每个项目都跟它们打过交道,踩过坑,也填过坑。

丢包是怎么发生的?

先说个基础概念。网络丢包说白了就是数据包在传输过程中「走丢了」。原因很多:路由器缓存满了、网络拥塞、无线信号不稳定……

在实时通信里,丢包的影响特别明显。我遇到过最夸张的一次,某客户在偏远矿区做视频巡检,丢包率飙到30%。画面基本就是幻灯片,声音断断续续。

丢包带来的问题主要有三个:

  • 视频花屏/马赛克:丢失了关键帧数据,解码器拿不到完整信息
  • 画面卡顿:接收端等不到数据包,只能重复显示上一帧
  • 音画不同步:音频包和视频包丢失比例不同,时间戳对不上

那WebRTC是怎么解决这些问题的?核心就是下面这几种抗丢包机制。

NACK:最朴实的重传方案

NACK,全称Negative ACKnowledgment。名字挺绕,意思很简单——「我没收到,再发一次」。

接收端发现某个包丢了,就给发送端发个NACK消息。发送端收到后,把对应的包重新发一遍。

NACK的工作流程:

  1. 接收端检测到序列号不连续(比如收到了1、2、4,缺了3)
  2. 接收端构造NACK消息,包含丢失包的序列号
  3. 发送端收到NACK,从历史缓存中找到对应包
  4. 发送端重新发送该包

这里有个关键点:发送端必须保留一段时间内的历史包。我习惯把这个缓存叫做「重传缓冲区」,大小一般设500ms到1000ms。太短了来不及重传,太长了内存吃不消。

// WebRTC内部NACK处理的简化逻辑
// 接收端检测到丢包
if (expected_seq_no != received_seq_no) {
    uint16_t lost_seq = expected_seq_no;
    while (lost_seq != received_seq_no) {
        nack_list.push_back(lost_seq);
        lost_seq++;
    }
    SendNackToSender(nack_list);
}

// 发送端收到NACK后重传
void OnReceivedNack(const std::vector<uint16_t>& nack_list) {
    for (auto seq : nack_list) {
        auto packet = retransmission_buffer_.FindPacket(seq);
        if (packet) {
            SendPacket(packet);
        }
    }
}

我的经验:NACK不是万能的。如果RTT(往返时延)太大,重传包到了也来不及解码。我曾经在跨国链路上测试,RTT 300ms,NACK重传基本没用。这时候就得靠FEC了。

RTX:专门为重传设计的通道

RTX是Retransmission的缩写。它和NACK是搭档关系——NACK负责「通知」,RTX负责「执行」。

WebRTC里,RTX是一个独立的SSRC(同步源)流。什么意思呢?就是原始视频流走一个SSRC,重传的包走另一个SSRC。这样接收端可以区分哪些是原始包,哪些是重传包。

特性 原始流 RTX流
SSRC 主SSRC RTX SSRC(关联主SSRC)
序列号 原始序列号 RTX专用序列号
负载 原始RTP包 原始RTP包(完整复制)
用途 正常传输 重传丢失包

RTX的好处是:接收端可以独立处理重传包,不会干扰原始流的排序和抖动缓冲。而且RTX流可以单独配置FEC,进一步保护重传数据。

注意:RTX会增加带宽消耗。每个重传包都是原始包的完整拷贝。如果丢包率很高,重传流量可能占满整个带宽。我曾经见过一个极端案例:丢包率20%,重传流量占了总带宽的40%,导致正常视频质量严重下降。

FEC前向纠错:未雨绸缪

NACK是「事后补救」,FEC是「事前预防」。

FEC的原理很简单:发送端在发送原始数据包的同时,额外发送一些冗余包。接收端即使丢了一部分包,也能通过冗余包恢复出原始数据。

WebRTC里最常用的是ULP FEC(Uneven Level Protection)。它基于异或(XOR)运算:

// FEC编码示例:保护一组K个媒体包
// 生成R个冗余包
// 每个冗余包 = 部分媒体包的异或结果

// 假设有4个媒体包:P1, P2, P3, P4
// 生成2个FEC包:
// F1 = P1 XOR P2
// F2 = P3 XOR P4

// 如果P2丢失,接收端可以用P1和F1恢复:
// P2 = P1 XOR F1

FEC的冗余度可以调节。冗余越多,抗丢包能力越强,但带宽消耗也越大。我一般建议这样配置:

  • 低丢包率(<5%):FEC冗余度10%-20%,主要保护关键帧
  • 中丢包率(5%-15%):FEC冗余度30%-50%,保护所有帧
  • 高丢包率(>15%):FEC+NACK联合使用,冗余度50%以上

FEC vs NACK:怎么选?

FEC适合低延迟场景(比如视频会议),因为它不需要等待重传。但FEC会持续消耗带宽,即使没有丢包。

NACK适合带宽敏感场景,只在丢包时才消耗额外带宽。但NACK需要等待一个RTT,延迟更高。

我个人的做法是:两者都用。FEC处理小规模丢包,NACK兜底处理大规模丢包。

PLI关键帧请求:最后的杀手锏

PLI,Picture Loss Indication。当接收端发现画面彻底坏了(比如关键帧丢了),就会发送PLI请求。

发送端收到PLI后,会立即编码一个关键帧(I帧)发送出去。接收端收到关键帧后,解码器重新开始工作,画面恢复正常。

PLI的使用场景:

  • 关键帧(IDR)丢失,后续所有P帧都无法解码
  • 连续丢包导致解码器状态混乱
  • 接收端刚加入通话,需要快速获取完整画面

我曾经踩过的坑:PLI不能频繁发送。如果网络持续丢包,接收端反复请求关键帧,发送端不断编码大尺寸关键帧,带宽瞬间爆炸。这叫「PLI风暴」。我见过一个案例,PLI请求频率达到每秒5次,视频码率从1Mbps飙升到8Mbps,整个通话直接崩溃。

解决方案:限制PLI发送频率,比如每秒最多1次。同时配合NACK和FEC,尽量用它们恢复画面,PLI作为最后手段。

四种技术的协同工作

在实际的WebRTC引擎里,这四种技术是协同工作的。我画了一张图来说明它们的关系:

WebRTC抗丢包机制协同工作图 发送端 编码器 + 重传缓冲区 接收端 解码器 + 丢包检测 网络传输 可能发生丢包 RTP包 + FEC冗余 部分包到达 NACK请求(丢失包序列号) RTX重传(通过独立SSRC) PLI关键帧请求(画面彻底损坏时) 图例: 正常数据流 NACK请求 RTX重传 PLI请求

从图中可以看到:

  1. 正常情况:发送端发送RTP包,同时携带FEC冗余。接收端正常解码。
  2. 小规模丢包:接收端通过FEC恢复丢失包,无需额外请求。
  3. FEC恢复失败:接收端发送NACK,发送端通过RTX通道重传。
  4. 画面彻底损坏:接收端发送PLI,发送端编码关键帧。

我的建议:在实际项目中,不要把所有抗丢包机制都开到最大。要根据网络状况动态调整。比如WiFi环境下丢包率低,可以降低FEC冗余;4G网络波动大,可以增加NACK的灵敏度。

WebRTC内置了拥塞控制和码率自适应,但抗丢包参数的调优还是需要根据具体场景来。我一般会在客户端暴露几个配置项,让运维人员可以根据实际网络情况微调。

总结一下

四种抗丢包技术各有千秋:

  • NACK:按需重传,节省带宽,但有延迟
  • RTX:独立通道重传,不干扰原始流
  • FEC:主动冗余,零延迟恢复,但持续消耗带宽
  • PLI:终极手段,恢复画面但代价大

实际工程中,没有银弹。我见过只用FEC的项目,也见过只用NACK的项目。但做得好的WebRTC产品,一定是把这四种技术组合起来,根据网络状况动态切换。

嗯,关于抗丢包就先聊这么多。下一节我们聊聊更进阶的话题——如何在弱网环境下保持流畅的通话体验。


公众号:蓝海资料掘金营,微信deep3321