webrtc源码走读(一)-QOS-NACK-概述
与NACK对应的是ACK,ACK是到达通知技术。以TCP为例,他可靠因为接收方在收到数据后会给发送方返回一个“已收到数据”的消息(ACK),告诉发送方“我已经收到了”,确保消息的可靠。NACK也是一种通知技术,只是触发通知的条件刚好的ACK相反,在未收到消息时,通知发送方“我未收到消息”,即通知未达。
NACK是在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端发送缓冲区的支持,RFC5104定义NACK数据包的格式。若在JB缓冲时间内接收端收到发送端重传的报文,就可以解决丢包问题。对应上图发送端的RTCP RTPFB
1.1 NACK实现
1.1.1、概念
与NACK对应的是ACK,ACK是到达通知技术。以TCP为例,他可靠因为接收方在收到数据后会给发送方返回一个“已收到数据”的消息(ACK),告诉发送方“我已经收到了”,确保消息的可靠。
NACK也是一种通知技术,只是触发通知的条件刚好的ACK相反,在未收到消息时,通知发送方“我未收到消息”,即通知未达。
在rfc4585协议中定义可重传未到达数据的类型有二种(RTPFB、PSFB)
(1)RTPFB(RTP 报文丢失重传)
- 含义:针对 “RTP 包本身丢了” 的情况,直接要求重传某一个 RTP 包。
- 场景:小明发的第 2 帧被封装在RTP 包 2里,这个包在网络中丢了 → 小红发送RTPFB 类型的 NACK,告诉小明 “RTP 包 2 丢了,重传它”。
(2)PSFB(指定净荷重传)
针对 “RTP 包没丢,但里面的视频数据丢了” 的情况,细分 3 种类型:
- ① PLI(Picture Loss Indication,视频帧丢失重传)
- 含义:一整个视频帧都丢了,要求重传整帧。
- 场景:小明发的第 2 帧是一个完整的视频画面(如 “小明挥手” 的一帧),但小红没收到 → 小红发送PLI 类型的 NACK,告诉小明 “整帧丢了,重传这一帧”。
- ② SLI(Slice Loss Indication,Slice 丢失重传)
- 含义:视频帧的一部分(Slice)丢了,只重传丢失的部分。
- 场景:小明发的第 2 帧被分成了 3 个 Slice(比如 “挥手的手”“身体”“背景”),其中 “手” 的 Slice 丢了 → 小红发送SLI 类型的 NACK,告诉小明 “只重传‘手’的 Slice”。
- ③ RPSI(Reference Picture Selection Indication,参考帧丢失重传)
- 含义:某一 “参考帧” 丢了,导致后续依赖它的帧无法解码,要求重传参考帧。
- 场景:小明发的第 2 帧是 “参考帧”(后续帧都依赖它解码),但丢了 → 小红发送RPSI 类型的 NACK,告诉小明 “参考帧丢了,重传它”。
WebRTC 的 SDP 会通过SDP 协议协商,确定用哪种 NACK 重传:
- 通常协商两种:
- ① RTPFB 类型的 NACK(默认,对应 “RTP 包丢了就重传包”)。
- ② PLI 类型的 NACK(对应 “整帧丢了就重传整帧”)。
1.1.2、触发场景
①包级重传(RTPFB 类型 NACK)的触发场景
核心逻辑:当单个 RTP 包丢失时触发,仅重传丢失的 RTP 包。
典型场景:
- 网络偶发丢包: 例如,小明给小红发送视频帧时,某一帧被拆分为 3 个 RTP 包(包 1、包 2、包 3)。其中包 2 在网络中丢失,但包 1 和包 3 正常到达。 小红的设备检测到“包 2 未收到”,会发送 RTPFB 类型的 NACK,要求小明重传包 2。
- 轻度网络波动: 网络短时拥塞导致少量 RTP 包丢失,但未影响整个视频帧的完整性。此时通过包级重传即可快速恢复数据,且开销小。
②帧级重传(PSFB 类型 PLI)的触发场景
核心逻辑:当一整个视频帧的关键数据丢失,导致该帧无法解码时触发,要求重传整个视频帧。
典型场景:
- 关键帧(I 帧)丢失: 视频编码中,I 帧是“完整画面帧”,后续 P 帧/B 帧依赖它解码。如果某一 I 帧的多个 RTP 包丢失,导致整帧无法解码,小红的设备会发送 PLI 类型的 NACK,要求小明重传整个 I 帧。
- 整帧 RTP 包大面积丢失: 例如,某一视频帧被拆分为 10 个 RTP 包,但超过一半的包丢失,即使重传个别包也无法恢复整帧,此时直接触发 PLI 要求重传整帧更高效。
- 解码链断裂: 若丢失的包属于“参考帧”(后续帧依赖它解码),且无法通过包级重传修复,会触发 PLI 重传整帧以恢复解码链。
1.1.3、WebRTC 如何决策
WebRTC 会根据丢包的严重程度和对解码的影响自动选择: - 优先尝试包级重传:如果丢失的是“非关键 RTP 包”,且重传后能恢复整帧,就用 RTPFB 类型 NACK。 - 升级为帧级重传:当包级重传无法解决问题(如关键帧丢失、整帧大量丢包),则触发 PLI 类型 NACK 重传整帧。 这种分层策略既保证了“小丢包快速修复”,又能在“严重丢包”时及时恢复画面,平衡了重传开销和画面流畅性。
1.1.4、报文示例:
# 包级重传协商
a=rtcp-fb:96 nack\r\n // 包级重传(RTPFB)
#PSFB 类型-帧级重传的 SDP 协商示例
a=rtcp-fb:96 nack pli\r\n // 帧级重传(PLI)
a=rtcp-fb:96 nack sli\r\n //SLI(Slice 丢失重传)
a=rtcp-fb:96 nack rpsi\r\n //RPSI(参考帧丢失重传)
# 音频媒体(98)的重传协商(音频一般少用 NACK 重传,此处为示例)
a=rtcp-fb:98 nack\r\n
a=rtcp-fb:98 nack pli\r\n
1.2、 RTCP 反馈消息定义

1.2.1 包头
这是 RTCP 反馈消息的“通用包头”,所有 NACK、PLI、SLI 等反馈都基于此结构:
| 字段 | 长度 | 含义说明 |
|---|---|---|
| V(版本) | 2bit | 固定为 2,标识 RTP/RTCP 协议版本。 |
| P(填充) | 1bit | 若为 1,表示报文末尾有填充字节(用于对齐,不影响数据);0 则无。 |
| FMT | 5bit | 反馈消息类型(区分是 NACK、PLI 还是其他类型)。 |
| PT(负载类型) | 8bit | 标识这是“反馈报文”,WebRTC 中通常为 205(RTPFB)或 206(PSFB)。 |
| length | 16bit | 报文总长度(以 32 位字为单位,包头本身占 2 个 32 位字,因此实际数据长度需计算)。 |
| SSRC of packet sender | 32bit | 发送反馈报文的设备标识(如接收端的 SSRC)。 |
| SSRC of media source | 32bit | 媒体源的标识(如发送端的视频 SSRC)。 |
| FCI(Feedback Control Information) | 变长 | 具体的反馈内容(如丢失的 RTP 序列号、帧丢失信息等)。 |
1.2.2 FMT
(5bit Feedback message type。可以通过 5bit 的 FMT 值,区分 “RTP 包级反馈”(RTPFB)和 “视频帧级反馈”(PSFB)的不同类型)
RTPFB 和 PSFB 两者是 RTCP 反馈的两大 “模式”,由报文里的 PT 字段(8bit)区分:
- 当 PT=205 时,是 RTPFB 模式(针对 RTP 数据包本身的反馈,比如包丢了);
- 当 PT=206 时,是 PSFB 模式(针对视频 “净荷数据” 的反馈,比如帧、Slice 丢了);

- 而 FMT 字段的取值,会 “跟着模式走”——RTPFB 有一套 FMT 定义,PSFB 有另一套。
RTPFB 模式下的 FMT 定义(处理“RTP 包级”问题)
RTPFB 聚焦于“RTP 数据包本身的丢失或异常”,目前仅定义了 1 个可用的 FMT 值,其他值暂未分配或预留:
| FMT 值 | 含义说明 | 核心用途 |
|---|---|---|
| 0 | unassigned(未分配) | 目前没有对应的反馈类型,暂时不用 |
| 1 | Generic NACK(通用 NACK) | 最常用,用于通知“某个/某些 RTP 包丢了”,比如之前例子中“RTP 序列号 176 丢了”,就用 FMT=1 |
| 2-30 | unassigned(未分配) | 协议预留,未来可能新增其他 RTP 包级反馈类型 |
| 31 | reserved(预留) | 用于未来扩展更多标识,当前不用 |
简单说:在 RTPFB 模式下,只要是“RTP 包丢了”,反馈报文的 FMT 就固定填 1,发送端看到 FMT=1 就知道“要重传指定的 RTP 包”。
PSFB 模式下的 FMT 定义(处理“视频帧级”问题)
PSFB 聚焦于“视频编码后的数据(净荷)丢失”,比如整帧、图像分片丢了,定义了 4 个关键 FMT 值,覆盖不同帧级问题:
| FMT 值 | 含义说明 | 核心用途 |
|---|---|---|
| 0 | unassigned(未分配) | 暂时无对应反馈类型 |
| 1 | Picture Loss Indication (PLI) | 整帧丢失,比如“整个 I 帧没收到”,发送 FMT=1 的反馈,要求重传完整视频帧 |
| 2 | Slice Loss Indication (SLI) | Slice 丢失,比如视频帧被分成 3 个 Slice(图像分片),其中 1 个丢了,用 FMT=2 反馈“要重传这个 Slice” |
| 3 | Reference Picture Selection Indication (RPSI) | 参考帧丢失,比如“依赖的 I 帧丢了,后续 P 帧解不了”,用 FMT=3 反馈“要重传这个参考帧” |
| 4-14 | unassigned(未分配) | 预留未来扩展 |
| 15 | Application layer FB (AFB) message | 应用层自定义反馈,比如 WebRTC 里的“带宽估计(REMB)”就用这个类型,非重传用途 |
| 16-30 | unassigned(未分配) | 预留未来扩展 |
| 31 | reserved(预留) | 用于未来扩展更多标识 |
简单说:在 PSFB 模式下,不同 FMT 值对应不同“帧级问题”——FMT=1 是整帧、FMT=2 是 Slice、FMT=3 是参考帧,发送端看到对应的 FMT 就知道“要重传哪类帧数据”。
WebRTC 中接收端要发起重传请求时,会先判断“丢的是 RTP 包还是视频帧”,再组合 PT 和 FMT 字段,如:
-
丢 RTP 包 → PT=205(RTPFB)+ FMT=1(Generic NACK);

-
丢整帧 → PT=206(PSFB)+ FMT=1(PLI);
-
丢 Slice → PT=206(PSFB)+ FMT=2(SLI);
-
丢参考帧 → PT=206(PSFB)+ FMT=3(RPSI);
发送端收到报文后,通过“PT 确定模式,FMT 确定具体问题”,就能精准执行对应的重传操作,这也是 WebRTC 实现“UDP 可靠传输”的关键逻辑之一。
1.2.3 FCI
变长 Feedback Control Information。
1、RTPFB

报文结构(FCI 部分)
- Packet Identifier (PID):丢失 RTP 包的序列号(标识具体丢了哪个包)。
- Bitmap of Lost Packets (BLP):16 位位图,每一位表示“从 PID 开始的下一个包是否丢失”(1=丢失,0=未丢失)。
Packet identifier(PID)即为丢失RTP数据包的序列号,Bitmap of Lost Packets(BLP)指示从PID开始接下来16个RTP数据包的丢失情况。一个NACK报文可以携带多个RTP序列号,NACK接收端对这些序列号逐个处理。如下示例:

示例解析
- PID = 176 → 表示“RTP 序列号为 176 的包丢失”。
- BLP = 0x6ae1(二进制需按小端解析为
1000 0111 0101 0110)→ 每一位对应“177、178…191”包的丢失情况:-
第 1 位(对应 177)= 1 → 177 包丢失;
-
第 6 位(对应 182)= 1 → 182 包丢失;
-
以此类推,最终丢失的包序列号为 177、182、183、184、186、188、190、191。
0x6ae1对应二进制:110101011100001倒过来看1000 0111 0101 0110。按照1bit是丢包,0bit是没有丢包解析,丢失报文序列号分别是:177 182 183 184 186 188 190 191与wireshark解析一致。
-
1.3、实现
以下是基于WebRTC源码对RTPFB(NACK)和PLI FB两种重传机制的实现详解:
1.3.1、 RTPFB(NACK)
RTPFB用于单个RTP包级别的丢包重传,核心依赖NackTracker类和抖动缓冲(JitterBuffer)的丢包检测逻辑。
1.3.1.1发送端重传流程
当接收端发送NACK请求后,发送端通过RtpPacketHistory维护已发送的RTP包缓存,触发重传的调用链如下:
PlatformThread::StartThread // 启动处理线程
-> PlatformThread::Run // 线程执行入口
-> ProcessThreadImpl::Run // 处理线程循环
-> ProcessThreadImpl::Process // 处理待执行任务
-> PacedSender::Process // 速率控制器处理待发送包
-> PacedSender::SendPacket // 发送包到网络
-> PacketRouter::TimeToSendPacket // 路由包到对应模块
-> ModuleRtpRtcpImpl::TimeToSendPacket // RTP/RTCP模块处理
-> RTPSender::TimeToSendPacket // RTP发送器处理
-> RtpPacketHistory::GetPacketAndSetSendTime // 从历史缓存中获取待重传包
-> RtpPacketHistory::GetPacket // 最终获取RTP包并标记发送时间
- 关键类:
RtpPacketHistory负责缓存已发送的RTP包,支持按序列号检索重传;PacedSender确保重传包的速率控制,避免网络拥塞。
1.3.1.2 接收端NACK触发机制
接收端通过收包驱动和定时驱动两种方式检测丢包并发送NACK:
-
收包驱动(实时检测):
DeliverPacket // 1. 网络层接收UDP数据包(从系统内核获取原始字节流)-> DeliverRtp // 2. 将原始数据包转换为RTP格式(解析UDP载荷为RTP包结构,提取版本、序列号等头部信息)-> RtpStreamReceiverController::OnRtpPacket // 3. 路由RTP包到对应的流控制器(根据SSRC区分不同媒体流,如视频流/音频流)-> RtpDemuxer::OnRtpPacket // 4. 媒体流解复用(根据RTP包的SSRC和负载类型,分发到对应的视频接收器)-> RtpVideoStreamReceiver::OnRtpPacket // 5. 视频流接收器接收RTP包(确认该RTP包属于当前视频流,进入视频处理链路)-> RtpVideoStreamReceiver::ReceivePacket // 6. 预处理RTP包(检查包完整性,过滤无效包,记录接收时间)-> RtpReceiverImpl::IncomingRtpPacket // 7. RTP接收器实现类处理包(更新接收统计,如丢包率、抖动值)-> RTPReceiverVideo::ParseRtpPacket // 8. 视频RTP包解析(提取视频载荷数据,验证时间戳、序列号连续性)-> RtpVideoStreamReceiver::OnReceivedPayloadData // 9. 处理视频载荷(将解析后的载荷数据传递给后续模块,如抖动缓冲)-> NackModule::OnReceivedPacket // 10. NACK模块检测丢包(对比已接收序列号与期望序列号,标记丢失的RTP包)-> VideoReceiveStream::SendNack // 11. 触发NACK发送(收集丢失的序列号,准备生成NACK请求)-> RtpVideoStreamReceiver::RequestPacketRetransmit // 12. 请求重传丢失包(将需要重传的序列号列表传递给RTCP模块) -> oduleRtpRtcpImpl::SendNack // 13. 发送NACK报文(构造RTCP NACK反馈报文,通过网络发送给发送端)NackModule::OnReceivedPacket:核心丢包检测逻辑,维护一个 “期望序列号窗口”,每收到一个 RTP 包就检查其序列号是否连续。例如,若当前收到序列号 100 的包,而上次收到 98,则判定 99 号包丢失,将其加入 NACK 列表。ModuleRtpRtcpImpl::SendNack:最终构造符合 RFC4585 标准的 RTCP NACK 报文(RTPFB 类型,FMT=1),包含丢失包的 PID(首个丢失序列号)和 BLP(后续 16 个包的丢失位图),确保发送端能精准重传。
-
定时驱动(周期性检测):
NackModule::Process- 核心逻辑:
NackModule定期(如每20ms)检查未确认的RTP包,若超时则触发NACK重传,避免收包驱动的遗漏。
- 核心逻辑:
1.3.2、PLI FB的实现逻辑
PLI用于视频帧级别的丢包重传,当整帧(如I帧)丢失导致解码中断时,触发“请求关键帧”的RTCP反馈。
3.2.1 触发条件
- 连续解码失败:解码器多次尝试解码失败,判断为整帧丢失。
- 长期无解码输入:抖动缓冲长期无有效帧输入,推测关键帧丢失。
3.2.2 源码调用链
VideoReceiveStream::Decode // 解码过程中检测到帧丢失
-> VideoReceiveStream::RequestKeyFrame // 触发关键帧请求
-> RTCRtpSenderVideo::SendPictureLossIndication // 构造PLI RTCP报文
-> RTCRtpSenderVideo::SendFeedback // 发送RTCP反馈
- 关键类:
VideoReceiveStream是视频接收流的核心控制器,RTCRtpSenderVideo负责生成并发送PLI的RTCP报文。
1.3.3、SDP协商与模块关联
在AssignPayloadTypesAndAddAssociatedRtxCodecs函数中,通过AddDefaultFeedbackParams将RTPFB和PLI的支持写入SDP:
// 简化示例逻辑
void AddDefaultFeedbackParams(SdpMediaSection* media_section) {// 添加RTPFB类型的NACK支持media_section->AddFeedbackParam(FeedbackParam("nack"));// 添加PLI类型的NACK支持media_section->AddFeedbackParam(FeedbackParam("nack", "pli"));// 可同时添加其他反馈类型(如goog-remb等)
}
- 该逻辑确保两端在SDP协商阶段就确认支持“包级NACK”和“帧级PLI重传”,为后续传输的重传机制铺路。
1.3.4、核心类与模块职责总结
| 模块/类 | 职责 |
|---|---|
NackTracker | 跟踪RTP包的接收状态,检测丢包并生成NACK请求。 |
RtpPacketHistory | 缓存发送端的RTP包,支持重传时的包检索。 |
VideoReceiveStream | 管理视频接收流的解码、丢帧检测及PLI请求触发。 |
RTCRtpSenderVideo | 生成并发送PLI等RTCP反馈报文。 |
AssignPayloadTypesAndAddAssociatedRtxCodecs | 处理SDP中Payload类型和反馈参数的协商,确保重传机制的协议级支持。 |
通过以上逻辑,WebRTC实现了“包级细粒度重传”和“帧级兜底重传”的双层保障,既保证了丢包恢复的效率,又在极端丢包场景下通过PLI请求关键帧维持解码连续性。
