WebRTC 中的 NACK(Negative Acknowledgment)机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象,尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响,从而保持通信的连续性和质量。
1. 总体架构
WebRTC NACK 总体架构如下图所示。
1)发送端发送 RTP 报文时会缓存一份到 RtpPacketHistory,收到 NACK 请求的时候从 RtpPacketHistory 获取对应缓存报文发送出去。RtpPacketHistory 收到 TransportFeedback 会将接收端确认的报文从缓存中移除。
2)接收端收到的所有报文都从 NackRequester 过一遍(只需要序列号),丢失了哪个报文门清。由于丢包和乱序无法分辨,NackRequest 在定时器驱动下发送 NACK 请求(极端丢包情况会发送关键帧请求)。
2. 发送端
2.1. 调用流程
发送端的调用流程由三条子流程组成:
1)发送出去的报文会被缓存到 RtpPacketHistory,用来响应 NACK 请求。
2)收到 TransportFeedback 将对应报文从 RtpPacketHistory 中移除。
3)收到 NACK 请求从 RtpPacketHistory 获取缓存报文发送出去。
2.2. RtpPacketHistory
RtpSenderEgress 负责报文发送,发送完后将报文缓存到 RtpPacketHistory。ModuleRtpRtcpImpl2 处理所有 RTCP 报文,NACK 请求交给 RTPSender 处理,RTPSender 从 RtpPacketHistory 获取请求重传的报文然后发送出去。
2.2.1. 重传条件
RtpSenderEgress 只会将满足条件的报文缓存到 RtpPacketHistory。正常的视频帧需要重传,但 FEC 报文不重传。另外,对于 simulcast 或 SVC,需要根据重传策略来决定,判断逻辑比较复杂,这里暂不分析。
void RtpSenderEgress::CompleteSendPacket(const Packet& compound_packet,
bool last_in_batch) {
...
if (is_media && packet->allow_retransmission()) {
packet_history_->PutRtpPacket(std::make_unique<RtpPacketToSend>(*packet), now);
} else if (packet->retransmitted_sequence_number()) {
packet_history_->MarkPacketAsSent(*packet->retransmitted_sequence_number());
}
...
}
2.2.2. 队列长度
缓存队列长度非常重要,太长的话,会引入较大延迟,太短的话,会导致重传 miss。因此,队列长度的设置需要在延迟和 miss 之间取得一个较好的平衡。
WebRTC 从时间和数量两个维度来对队列长度进行限制,其中,kMaxCapacity 是一个硬性数量限制,不管缓存的报文是否新鲜,都不能超过这个限制。
// packet_duration = max(1 second, 3x RTT).
static constexpr TimeDelta kMinPacketDuration = TimeDelta::Seconds(1);
static constexpr int kMinPacketDurationRtt = 3;
// With kStoreAndCull, always remove packets after 3x max(1000ms, 3x rtt).
static constexpr int kPacketCullingDelayFactor = 3;
// number_to_store_ = min(kMaxCapacity, kMinSendSidePacketHistorySize)
static constex