NACK 与 RTX 重传 · 丢包了就重发
UDP 会丢包,WebRTC 用 NACK+重传补救:接收端发现序号缺失就发 NACK,发送端从历史重发。这一章讲清 NackTracker 怎么决定请求哪些包、RTX 为什么用独立的流承载重传,以及重传的适用边界——RTT 太大时重传就来不及了。
NACK 与 RTX 重传 · 丢包了就重发
UDP 会丢包,而视频对丢包尤其敏感(丢一个包可能让好几帧解不出)。WebRTC 对抗丢包的第一招,是最直接的:发现丢了就重发。这套机制叫 NACK + 重传,重传常走一条叫 RTX 的独立流。这一章拆开它。
NACK:接收端喊「重发」
序号让接收端能发现丢包——收到序号 100、102,那 101 就是丢了。接收端于是发一个 NACK(Negative ACKnowledgement,否定确认)告诉发送端「101 我没收到,重发」。发送端从 RtpPacketHistory 历史里取出 101 号包,重发。
决定「请求重传哪些包」的,是接收端的 NackTracker(modules/rtp_rtcp/source/nack_tracker.cc 一类):
RTX:重传走独立的流
重传的包怎么发?最朴素的办法是原样重发(同序号同 SSRC)。但 WebRTC 更常用 RTX(RTP Retransmission,RFC 4588)——让重传走一条独立的 RTP 流:
RTX 给重传包配了独立的 SSRC、payload type 和序号空间——重传不再冒充原包,而是「另开一路」。 一个原始流(SSRC=A),配一个 RTX 流(SSRC=B)专发它的重传。重传 101 号包时,在 RTX 流里它有自己的新序号(比如 RTX 序号 5),包体里再带上「我其实是原始流的 101 号」。为什么这么绕?
- 不打乱原始流的序号连续性:原始流的序号保持严格 +1,丢包检测、抖动计算才不被重传包干扰。
- 重传流可独立统计:能清楚知道「重传占了多少带宽」,给拥塞控制参考。
- 兼做带宽探测:
g3doc/rtp.md说的 RTX 探测就复用这条流——重发个旧包既探带宽、又万一有用。
代价是要多协商一个 payload type 和 SSRC。这是实时传输里「多一层间接、换取干净与灵活」的又一例。RTX 是否启用由 SDP 协商(a=rtpmap 里的 rtx)。
重传的边界:RTT 太大就来不及
重传不是万能的,它有个硬边界:
重传要一来一回一个 RTT,如果 RTT 大于「这段媒体还能等多久」,重传回来时媒体早该播了,纯属浪费。 举例:视频缓冲区只能容忍 100ms 延迟,而 RTT 是 300ms——你发 NACK、对端重传、包回来,已经 300ms 过去,这帧的播放时刻早过了,重传的包只能丢弃。所以重传适合低 RTT 场景(同城、好网络);高 RTT(跨国、卫星)下,重传越来越不划算,得靠不需要往返的 FEC 前向纠错来补。
这引出实时传输一个核心权衡:重传(NACK/RTX)省带宽但要往返、FEC 免往返但费带宽。WebRTC 常两者结合——低 RTT 主要靠重传,高 RTT / 高丢包补以 FEC。下一章就讲 FEC。
小结与下一章
NACK + 重传是对抗丢包的第一招:接收端靠序号发现缺包、发 NACK,发送端从历史重发。NackTracker 有策略(区分丢包 / 乱序、控节奏、看 RTT)。重传常走 RTX 独立流(独立 SSRC/PT/序号),不打乱原始流、可独立统计、兼做探测。重传的硬边界是 RTT——太大就来不及,得靠 FEC。
那个「不需要往返」的补丁,就是前向纠错 FEC。下一章看它怎么用冗余包在丢包发生的当下就恢复。👉 FEC 前向纠错。
参考资料
- [1] libwebrtc 源码.
modules/rtp_rtcp/source/nack_tracker.h、rtp_rtcp/source/rtp_packet_history.h(重传取包)。 - [2] libwebrtc 源码.
g3doc/rtp.md(RTX 流与探测)。 - [3] IETF RFC 4585「NACK」、RFC 4588「RTP Retransmission (RTX)」。