做实时音视频的人,迟早会碰到一对经常放在一起讲的概念:NACK 和 RTX。NACK 是接收方向发送方反馈“哪个包丢了”,RTX 则是发送方针对丢包进行的重传动作。音视频学习(九十四)这篇,我不想只停留在“丢包就重传”这种层面,而是把从丢包检测、RTCP 反馈、重传包格式到实际协商参数这条链路完整拆开。适合正在做 WebRTC 通话、自研 RTP 传输层,或者刚接触实时音视频但被各种 RFC 名词绕晕的开发者。
先说一个很多人容易忽略的判断:NACK 和 RTX 并不是同一层的东西,它们的配合远没有“接收端报一下、发送端发一下”那么简单。真正要理解它们,得先回答三个问题:什么时候该判断丢包?用什么报文告诉对端?收到 NACK 之后,重传的到底是“原始 RTP 包”,还是另一个经过包装的包?
1. 先搞清场景:为什么实时传输不能像 TCP 一样做重传
1.1 TCP 式重传的问题
在实时音视频里,RTP 通常跑在 UDP 上,这是为了绕开 TCP 的队头阻塞和拥塞控制带来的不确定延迟。但 UDP 没有丢包重传机制,所以在应用层,我们要自己补一套“准可靠”的传输策略。
最朴素的想法是:丢了包,就把原始包再发一次。听起来可行,实际做起来却有一个很现实的问题——实时性。一个视频通话如果端到端延迟要求控制在 200ms 以内,那么发送方收到 NACK 后,只能在剩余时间窗口内完成重传。如果RTT 已经占了大半,重传到接收端也没有意义,因为播放器等不到那个时候。
还有一个更“致命”的问题:TCP 重传不会影响应用层对字节流的顺序理解,但在 RTP 里,同一个 SSRC 的序列号是接收方判断包是否连续、是否乱序、是否重复的唯一依据。如果发送方收到 NACK 后把同一个序列号的原始包直接重发一次,表面上没问题,但接收方已经把这个序列号标记为“期待到达”,如果数据包还在网络里漂着,后面又来了一个重发副本,接收方就得做额外的去重。如果处理不好,解码器拿到重复帧,视频画面会出现跳变。
1.2 为什么需要“否定确认”而不是“肯定确认”
TCP 用 ACK 确认每一个或连续一批字节,实时音视频场景为什么不照搬?
因为实时音视频的接收端状态是“连续播放”,它并不需要像文件传输一样保证每比特都可靠到达。接收方只要知道自己播放进度附近缺了哪些包就够了。用一个 ACK 列表把所有收到的包都汇报回去,带宽消耗太大,尤其是高码率视频场景,一秒几百个 RTP 包,不可能全部确认。
NACK 的思路是:只告诉发送方“我没收到什么”。在绝大多数网络正常的通话里,丢包率很低,NACK 的报文数量远比 ACK 少。而且接收端如果发现缺口可以通过后续到达的包补齐,它甚至可以不发 NACK。这种“带有宽容度的反馈”才是实时传输需要的。
所以,NACK 是接收方对“预期收到的包缺失”的反馈,是一种否定式确认。而 RTX 是发送方收到 NACK 之后,从重传缓冲区取出对应包再发一次的执行动作。两者共同把 UDP 传输从“尽力而为”提升到“准可靠”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 丢包检测怎么做的?序列号缺口与乱序的博弈
2.1 接收侧如何判定某个 RTP 包丢了
RTP 序列号是连续的,正常情况下一个媒体 SSRC 的接收序列应该一个接一个递增。接收端每收到一个 RTP 包,会和上一个收到的序列号比较,如果发现差值大于 1,就说明中间可能有包丢了。
但这里有一个经典陷阱:序列号有跳变不代表一定丢包。网络路径上出现乱序时,后面那个序号更大的包可能先到,之前那个序号较小的包还在路上。如果接收端一看到缺口就立刻上报 NACK,发送方马上重传,结果过了一会儿原始包也到了,接收端就收到了重复副本,白白浪费一次重传带宽。
所以,几乎所有的实现都会做一个“等待乱序包”的窗口。常见做法是:发现序列号缺口后,不立刻发 NACK,而是先启动一个很短的定时器,或者等待若干毫秒,看看缺失的包能不能在乱序窗口内到达。如果等待超时仍未到达,再判定为丢失。
在 WebRTC 的开源实现和一些自研实时传输引擎里,这个等待时间不会太长,一般控制在 10ms 到 30ms 这个量级。等待太短,会把乱序误判为丢包;等待太长,即使重传回来,播放器也可能已经等不及了。
2.2 一个 NACK 能报多少丢失包
RTCP 的 NACK 报文属于 RTPFB(RTP Feedback)类型。它的格式不复杂,但非常巧妙:每个 NACK 条目由 PID 和 BLP 两个字段组成。PID 表示第一个丢失包的 RTP 序列号,BLP 是 16 bit 的位掩码,每一位代表
