认真看完这篇文章,相信你能收获一些东西
webrtc是基于udp协议来进行传输音视频数据的,所以基于udp的特性,rtc采用了2种方式来优化丢包问题
- fec,前向纠错,在每个数据包中,添加一些关于前一个信息的信息,以防丢失,您需要重新构建它们,如果fec为5%,那么在丢包小于5%的情况下,都可以通过fec进行恢复,组成完整的帧,但是需要发送额外的包,会占用更大的带宽,具体如何实现,可以自行研究下,不在本文的讨论范围之中。
- NACK机制,和TCP的ACK机制正好相反,NACK是用来确认丢包的发送协议,当接收方检测到有丢包时,它会发送NACK类型的RTCP包给发送方,发送方会重发这些数据。
NACK 模块是 WebRTC 对抗弱网的核心 QoS 技术之一,有两种发送模式,一种是基于时间序列的发送模式,一种是基于包序列号的发送模式。很明显,NACK机制,也是需要两端配合进行同时处理,我们分别来讨论下。
接收方
当接收方检测到有丢包时,它会发送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的包体,重传了也没有意义,因为无法进行解码。
源码分析
nack_module.h

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
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 包。
Process
该函数实现了基于时间周期(20ms)的 nack 发送模式,参考策略 2。具体的处理周期计算方法如下:
void DEPRECATED_NackModule::Process() {
if (nack_sender_) {
std::vector<uint16_t> nack_batch;
{
rtc::CritScope lock(&crit_);
nack_batch = GetNackBatch(kTimeOnly);
}
if (!nack_batch.empty()) {
// This batch of NACKs is triggered externally; there is no external
// initiator who can batch them with other feedback messages.
nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/false);
}
}
// Update the next_process_time_ms_ in intervals to achieve
// the targeted frequency over time. Also add multiple intervals
// in case of a skip in time as to not make uneccessary
// calls to Process in order to catch up.
int64_t now_ms = clock_->TimeInMilliseconds();
if (next_process_time_ms_ == -1) {
next_process_time_ms_ = now_ms + kProcessIntervalMs;
} else {
next_process_time_ms_ = next_process_time_ms_ + kProcessIntervalMs +
(now_ms - next_process_time_ms_) /
kProcessIntervalMs * kProcessIntervalMs;
}
}
复制代码
上面说到的策略,rtc内部并不是每隔 20 毫秒批量处理 nack_list,而是动态变化的,这里的算法主要是
int64_t now_ms = clock_->TimeInMilliseconds();
next_process_time_ms_ = next_process_time_ms_ + kProcessIntervalMs +
(now_ms - next_process_time_ms_) /
kProcessIntervalMs * kProcessIntervalMs;
复制代码
主要是引入了now_ms
根据当前处理的时间点进行叠加,这么做的原因是为了应对 cpu 繁忙时线程调度滞后的场景,追赶上正常的处理进度,这就是动态处理周期的意义所在。
GetNackBatch
std::vector<uint16_t> DEPRECATED_NackModule::GetNackBatch(
NackFilterOptions options) {
bool consider_seq_num = options != kTimeOnly;
bool consider_timestamp = options != kSeqNumOnly;
Timestamp now = clock_->CurrentTime();
std::vector<uint16_t> nack_batch;
auto it = nack_list_.begin();
while (it != nack_list_.end()) {
bool delay_timed_out =
now.ms() - it->second.created_at_time >= send_nack_delay_ms_;
bool nack_on_rtt_passed =
now.ms() - it->second.sent_at_time >= resend_delay.ms();
bool nack_on_seq_num_passed =
it->second.sent_at_time == -1 &&
AheadOrAt(newest_seq_num_, it->second.send_at_seq_num);
//判断是否满足基于序号或者基于时间序列的条件
if (delay_timed_out && ((consider_seq_num && nack_on_seq_num_passed) ||
(consider_timestamp && nack_on_rtt_passed))) {
nack_batch.emplace_back(it->second.seq_num);
++it->second.retries;
it->second.sent_at_time = now.ms();
if (it->second.retries >= kMaxNackRetries) {
RTC_LOG(LS_WARNING) << "Sequence number " << it->second.seq_num
<< " removed from NACK list due to max retries.";
it = nack_list_.erase(it);
} else {
++it;
}
continue;
}
++it;
}
return nack_batch;
}
复制代码
该函数传入 nack 过滤选项参数,根据时间或者序列号批量获取 nack_list 中的包序列号,并返回存储了这些包号的数组 nack_batch。
- 基于序列号的发送模式。consider_seq_num = true,且当前收到的最新包号已经等于或者超过该 nack_info 期望发送时的包号 send_at_seq_num。
- 基于时间序列的发送模式。consider_timestamp = true,且当前时间距上一次发送已经超过一个 rtt 时间
满足条件后,将该 nack_info 中请求重传的包号加入到 nack_batch 数组,重传请求次数 +1,更新 nack 发送时间,如果大于kMaxNackRetries
,那么则直接移除即可
文章比较长,大家可以针对源码来对比看下逻辑实现,下一章我们再分析发送端如何处理NACK模块。