一、基本原理
WebRTC是基于udp协议来进行传输音视频数据的,所以基于udp的特性,RTC采用了2种方式来优化丢包问题
- fec,前向纠错,在每个数据包中添加一些关于前一个信息的信息,以防丢失。在接收端,需要重新构建它们,如果fec为5%,那么在丢包小于5%的情况下,都可以通过fec进行恢复,组成完整的帧,但是需要发送额外的包,会占用更大的带宽,具体实现不做讨论。
- NACK机制,和TCP的ACK机制正好相反,NACK是用来确认丢包的发送协议,当接收方检测到有丢包时,它会发送NACK类型的RTCP包给发送方,发送方会重发这些数据。
NACK 模块是 WebRTC 对抗弱网的核心 QoS 技术之一,有两种发送模式,一种是基于时间序列的发送模式,一种是基于包序列号的发送模式。
二、具体实现
当接收方检测到有丢包时,它会发送NACK类型的RTCP包给发送方。针对现实中网络的复杂程度,我们思考以下几个问题:
- 如何判断丢包?也就是如何选择发送NACK的时机?因为UDP是无连接状态的,不能保证数据的连续性,比如我们先收到了序号1的包, 第二次收到了序号3的包,那么此时是否可以认为序号2的包已经丢失,需要发送NACK报告丢包情况,但很有可能下一时刻2号包就到了。
- 是否需要一直发送NACK包?假如我们发送NACK后,可能因为网络原因等等,一直没有收到接受方发送的重发包,那么还要一直继续发送NACK吗,会不会导致服务链路拥塞
- 如果丢包数量过多,超过了一定的数量,是否需要放弃之前的丢包数据,不再进行发送NACK?
RTC内部也考虑到了这些问题,目前有一些实施的策略来保证,记住几个关键的数字如下。
const int kMaxNackRetries = 10;
const int kProcessIntervalMs = 20;
const int kDefaultRttMs = 100;
const int kMaxNackPackets = 1000;
const int kMaxPacketAge = 10000;
- NACK 模块对同一包号的最大请求次数是10次,超过这个最大次数限制,会把该包号移出 nack_list,放弃对该包的重传请求。
- NACK 模块每隔 20 毫秒批量处理 nack_list,获取一批请求包号存储到 nack_batch,生成 nack 包并发送。不过,nack_list 的处理周期并不是固定的 20ms ,而是基于 20ms 动态变化
- NACK 模块默认 rtt 时间,如果距离上次 nack 发送时间不到一个 rtt 时间,那么不会发送 nack 请求,注意,100ms 只是 rtt 的默认值,在实际应用中,rtt 应该要根据网络状况动态计算,计算方式有很多种,比如对于接收端来说,可以通过发送 xr 包来计算 rtt。
- nack_list 的最大长度,即本次发送的 nack 包至多可以对 1000 个丢失的包进行重传请求。
- nack_list 中包号的距离不能超过 10000 个包号。即 nack_list 中的包号始终保持 [cur_seq_num - 10000, cur_seq_num] 这样的跨度,以保证 nack 请求列表中不会有太老旧的包号
关于第四点,nack_list 的最大长度,这里拉出来单独理解下,如果丢失的包数量超过 1000,会循环清空 nack_list 中关键帧之前的包,直到其长度小于 1000,但是并不是清除到刚好到1000的数量,也就是说,放弃对关键帧首包之前的包的重传请求,直接而快速的以关键帧首包之后的包号作为重传请求的开始。
我们知道在一个GOP内,解码时,后面的帧都是参考前面的帧进行解码的,如果一个GOP内,前面的帧被清掉了,后面的也没有重传的必要。
举个例子,假如我们接收方,收到的包序号是 1/701/1201,并且都是关键帧的包,那按照上面的算法,我们丢失包是700+500 = 1200个,此时触发了大于1000的条件,那么需要清空超过的包体,按照上面关键帧的算法,那么这里会将701之前的包都会清除掉保证重传的意义。因为如果按照只清除超过的包体算法,只会清除1-201的包,但是如果这样,201-700的包体,重传了也没有意义,因为无法进行解码。
-
源码分析
-
class NackModule : public Module { public: .............. int OnReceivedPacket(uint16_t seq_num, bool is_keyframe); int OnReceivedPacket(uint16_t seq_num, bool is_keyframe, bool is_recovered); void ClearUpTo(uint16_t seq_num); void UpdateRtt(int64_t rtt_ms); void Clear(); // Module implementation int64_t TimeUntilNextProcess() override; void Process() override; private: struct NackInfo { NackInfo(); NackInfo(uint16_t seq_num, uint16_t send_at_seq_num, int64_t created_at_time); uint16_t seq_num; uint16_t send_at_seq_num; int64_t created_at_time; int64_t sent_at_time; int retries; }; std::map<uint16_t, NackInfo, DescendingSeqNumComp<uint16_t>> nack_list_ RTC_GUARDED_BY(crit_); std::set<uint16_t, DescendingSeqNumComp<uint16_t>> keyframe_list_ RTC_GUARDED_BY(crit_); std::set<uint16_t, DescendingSeqNumComp<uint16_t>> recovered_list_ RTC_GUARDED_BY(crit_); video_coding::Histogram reordering_histogram_ RTC_GUARDED_BY(crit_); bool initialized_ RTC_GUARDED_BY(crit_); int64_t rtt_ms_ RTC_GUARDED_BY(crit_); uint16_t newest_seq_num_ RTC_GUARDED_BY(crit_); // Only touched on the process thread. int64_t next_process_time_ms_; // Adds a delay before send nack on packet received. const int64_t send_nack_delay_ms_; const absl::optional<BackoffSettings> backoff_settings_; }; } // namespace webrtc #endif // MODULES_VIDEO_CODING_DEPRECATED_NACK_MODULE_H_ - nack_list_,丢包的数组,如果判断符合NACK条件,添加到数组之中
- keyframe_list_,关键帧数组
- recovered_list_,丢包重传恢复的数组
- newest_seq_num_,当前最新的包的序号,用来判断包是否连续等等
- NackInfo ,非常重要的一个结构体,上面我们说到NACK 有两种发送模式,基于时间和基于包序号的,如果sent_at_time 为-1,那么这是一个基于序列号发送的 nack,而且要在当前接收的最新包号 newest_seq_num_ 大于等于 send_at_seq_num 时才会发送。sent_at_time如果有值,那么这是一个基于时间序列发送的 nack,要将这个参数结合当前 rtt 来决定是否发送重传请求。
OnReceivedPacket
通过该方法,可以拿到当前接收的所有RTP包的序列号,进行丢包检测

int NackModule2::OnReceivedPacket(uint16_t seq_num,
bool is_keyframe,
bool is_recovered) {
RTC_DCHECK_RUN_ON(worker_thread_);
// TODO(philipel): When the packet includes information whether it is
// retransmitted or not, use that value instead. For
// now set it to true, which will cause the reordering
// statistics to never be updated.
bool is_retransmitted = true;
if (!initialized_) {
newest_seq_num_ = seq_num;
if (is_keyframe)
keyframe_list_.insert(seq_num);
initialized_ = true;
return 0;
}
// Since the |newest_seq_num_| is a packet we have actually received we know
// that packet has never been Nacked.
if (seq_num == newest_seq_num_)
return 0;
if (AheadOf(newest_seq_num_, seq_num)) {
// An out of order packet has been received.
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;
}
// Keep track of new keyframes.
if (is_keyframe)
keyframe_list_.insert(seq_num);
// And remove old ones so we don't accumulate keyframes.
auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != keyframe_list_.begin())
keyframe_list_.erase(keyframe_list_.begin(), it);
if (is_recovered) {
recovered_list_.insert(seq_num);
// Remove old ones so we don't accumulate recovered packets.
auto it = recovered_list_.lower_bound(seq_num - kMaxPacketAge);
if (it != recovered_list_.begin())
recovered_list_.erase(recovered_list_.begin(), it);
// Do not send nack for packets recovered by FEC or RTX.
return 0;
}
AddPacketsToNack(newest_seq_num_ + 1, seq_num);
newest_seq_num_ = seq_num;
// Are there any nacks that are waiting for this seq_num.
std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);
if (!nack_batch.empty()) {
// This batch of NACKs is triggered externally; the initiator can
// batch them with other feedback messages.
nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);
}
return 0;
}
- 如果seq_num == newest_seq_num_,那说明是连续的包,不需要处理
- 接下里判断,如果AheadOf(newest_seq_num_, seq_num),也就是收到的包序号比当前的最新的序号要小,那这里有两种情况
- 因为包的乱序导致之前的包晚一点才到
- 经过NACK后重传后的包到达
- 通过nack_list_.find(seq_num)看看能否找到,如果能找到,则说明是重传的包,那么需要将这个包进行移除,反之则是乱序到达的包。
- 判断包的连续性,如果当前包号不连续,则将中间断掉的包号加入到 nack 请求列表,并更新 newest_seq_num_
- 一旦发现 nack_list 的大小已经超过 1000,那么就要根据关键帧序列号来调整其大小,也就是上面到的策略4
- 最后,批量获取 nack_list 中的包序列号到数组 nack_batch 中,生成并发送 nack 包。
三、实现效果
没有启动重传时,设置10%丢包 ,可以明显看到推流端与拉流端之间的画面差异。
启动重传时,设置30%丢包 ,可以看到推流端与拉流端之间的画面并无明显差异。
本文介绍了WebRTC中用于应对丢包问题的NACK机制。NACK是一种反馈机制,当接收方检测到丢包时,会发送NACK请求让发送方重传。文章详细讨论了NACK的工作原理、实现细节,包括如何判断丢包、如何处理NACK请求以及相关的限制策略,如最大请求次数、处理周期等。
1480

被折叠的 条评论
为什么被折叠?



