WebRTC NACK 实现
Abstract |
WebRTC NACK 丢包重传机制 |
Authors |
Walter Fan |
Status |
v1.0 |
Updated |
2026-08-17 |
概述
NACK(Negative Acknowledgement)是 WebRTC 中最基本的丢包恢复机制。 当接收端检测到 RTP 包丢失时,通过 RTCP NACK 消息通知发送端重传丢失的包。
sequenceDiagram
participant S as 发送端
participant R as 接收端
S->>R: RTP seq=100
S--xR: RTP seq=101 丢失
S->>R: RTP seq=102
Note over R: 检测到 seq=101 缺失
R->>S: RTCP NACK seq=101
S->>R: RTP seq=101 重传
Note over R: 恢复完整
相比 FEC(前向纠错),NACK 的优势是不浪费带宽(只在丢包时重传), 劣势是增加至少一个 RTT 的延迟。
RTCP NACK 包格式
NACK 使用 RTCP 反馈消息(RFC 4585),格式如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P| FMT=1 | PT=205 | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of packet sender |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of media source |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PID (丢失包序号) | BLP (位掩码) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
PID:丢失包的序号
BLP:位掩码,指示 PID 之后的 16 个包中哪些也丢失了(1 个 NACK FCI 可报告最多 17 个丢包)
核心类
类 |
说明 |
|---|---|
|
接收端 NACK 请求生成器,维护 nack_list,检测丢包并决定何时发送 NACK |
|
NackRequester 的别名/演化版本 |
|
发送端包历史缓冲区,保存已发送的 RTP 包以备重传 |
|
发送 RTCP NACK 消息 |
|
解析收到的 RTCP NACK 消息 |
NackRequester 工作流程
flowchart TD
A[RTP 包到达] --> B["OnReceivedPacket seq_num"]
B --> C{seq_num vs newest}
C -->|相等| D[忽略 重复包]
C -->|小于 newest| E[乱序到达]
E --> F[从 nack_list 中移除]
F --> G[返回该包被 NACK 的次数]
C -->|大于 newest| H[新包]
H --> I["将 newest+1 到 seq_num 之间缺失包加入 nack_list"]
I --> J[更新 newest_seq_num]
J --> K["GetNackBatch 发送 NACK"]
关键源码:
int NackRequester::OnReceivedPacket(uint16_t seq_num,
bool is_keyframe,
bool is_recovered) {
RTC_DCHECK_RUN_ON(worker_thread_);
bool is_retransmitted = true;
if (!initialized_) {
newest_seq_num_ = seq_num;
if (is_keyframe)
keyframe_list_.insert(seq_num);
initialized_ = true;
return 0;
}
if (seq_num == newest_seq_num_)
return 0;
if (AheadOf(newest_seq_num_, seq_num)) {
// 乱序包到达 — 从 nack_list 中移除
auto nack_list_it = nack_list_.find(seq_num);
int nacks_sent_for_packet = 0;
if (nack_list_it != nack_list_.end()) {
nacks_sent_for_packet = nack_list_it->second.retries;
nack_list_.erase(nack_list_it);
}
if (!is_retransmitted)
UpdateReorderingStatistics(seq_num);
return nacks_sent_for_packet;
}
if (is_keyframe)
keyframe_list_.insert(seq_num);
// 清理过旧的关键帧记录
auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != keyframe_list_.begin())
keyframe_list_.erase(keyframe_list_.begin(), it);
// FEC/RTX 恢复的包不需要 NACK
if (is_recovered) {
recovered_list_.insert(seq_num);
auto it = recovered_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != recovered_list_.begin())
recovered_list_.erase(recovered_list_.begin(), it);
return 0;
}
// 将缺失的序号加入 nack_list
AddPacketsToNack(newest_seq_num_ + 1, seq_num);
newest_seq_num_ = seq_num;
// 立即发送一批 NACK
std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);
if (!nack_batch.empty()) {
nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);
}
return 0;
}
NACK 重试与限制
NackRequester 对每个丢失包的 NACK 请求有限制,避免无效重传浪费带宽:
参数 |
说明 |
|---|---|
|
单个包最大 NACK 重试次数(默认 10 次) |
|
包的最大年龄(序号差),超过后不再 NACK |
|
nack_list 最大长度,超过后清空并请求关键帧 |
NACK 间隔 |
基于 RTT 计算,避免在 RTT 内重复 NACK 同一个包 |
当 nack_list 过大时(连续大量丢包),说明网络状况极差,此时 NACK 重传已无意义, WebRTC 会转为请求关键帧(PLI/FIR),重新同步视频流。
发送端:RtpPacketHistory
发送端需要缓存已发送的 RTP 包,以便收到 NACK 后能重传:
class RtpPacketHistory {
public:
// 存储已发送的包
void PutRtpPacket(std::unique_ptr<RtpPacketToSend> packet,
Timestamp send_time);
// 根据序号取出包用于重传
std::unique_ptr<RtpPacketToSend> GetPacketAndMarkAsPending(
uint16_t sequence_number);
// 设置历史缓冲区大小(包数量)
void SetStorePacketsStatus(StorageMode mode,
uint16_t number_to_store);
};
重传的包通常通过 RTX(Retransmission)通道发送,使用独立的 SSRC 和 PT, 避免与原始流混淆。详见 WebRTC RTX Code。
NACK 与其他丢包恢复的协同
flowchart TD
A[丢包检测] --> B{FEC 能恢复?}
B -->|是| C[直接恢复,不发 NACK]
B -->|否| D[RTX 重传]
D --> E[通过 NACK 触发,独立 SSRC 发送]
D --> F{仍无法恢复?}
F -->|是| G["请求关键帧 PLI/FIR"]
WebRTC 的策略是优先使用 FEC 恢复(零延迟),FEC 无法恢复时使用 NACK 请求重传, 大面积丢包时退化为请求关键帧。
在 JavaScript 中观察 NACK
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'inbound-rtp') {
console.log('nackCount:', report.nackCount);
console.log('packetsLost:', report.packetsLost);
console.log('packetsReceived:', report.packetsReceived);
}
if (report.type === 'outbound-rtp') {
console.log('nackCount:', report.nackCount);
console.log('retransmittedPacketsSent:', report.retransmittedPacketsSent);
console.log('retransmittedBytesSent:', report.retransmittedBytesSent);
}
});
参考资料
RFC 4585: Extended RTP Profile for RTCP-Based Feedback (RTP/AVPF)