webrtc QOS笔记四 Nack机制浅析
nack源码浅析
Video Nack
机制概述
- nack的机制非常简洁,收到非连续的packet seq 会将丢包的seq插入自身nack_list缓存, 之后立即发送一次那组丢包的seq重传请求, 之后如果超时仍然没有收到重传回来的seq, 就通过定时任务继续发送.
nack 三个缓存list
- nack_list_ : 用于记录已丢包的信息,seq 即为list key
- keyframe_list_ : 记录关键帧序列号,可用于后面清理比关键帧老的过旧nack,
- recovered_list_ : 用于记录从RTX或FEC恢复过来的包,
nack 两种发送方式:
1.kSeqNumOnly : 开启nack模块后,nack会检查接收到packet的序列号,如果序列号连续性中断即认为是丢包了,如下例子,上一次最新收到的包序列号为38,当前新收到的序列号为41,那么[39,40]就判定为是丢掉了,会立刻发送这组[39,40]的nack重传请求
newest_seq_num_:36 seq_num:37 is_keyframe:0 is_recovered: 0 newest_seq_num_:37 seq_num:38 is_keyframe:0 is_recovered: 0 newest_seq_num_:38 seq_num:41 is_keyframe:0 is_recovered: 0 newest_seq_num_:41 seq_num:42 is_keyframe:0 is_recovered: 0 newest_seq_num_:42 seq_num:43 is_keyframe:0 is_recovered: 0
2.kTimeOnly : nack 模块创建后会启动一个定时任务,默认周期kUpdateInterval(20ms), 这个周期任务会调用GetNackBatch(kTimeOnly)从nack_list里面获取满足发送条件的seq,批量发送nack重传请求.
repeating_task_ = RepeatingTaskHandle::DelayedStart( TaskQueueBase::Current(), kUpdateInterval, [this]() { std::vector<uint16_t> nack_batch = GetNackBatch(kTimeOnly); if (!nack_batch.empty()) { nack_sender_->SendNack(nack_batch, false); } });
3.kSeqNumOnly模式是在接收packet的时候触发一次,并且只发送一次,即第一次,之后如果仍然没有收到重传回来的包就通过kTimeOnly定时任务方式继续请求重传.
nack模块源码简析
nack模块
- nack模块位于在RTX和FEC 和 jitter buffer之间,经过Call模块将RTP包分发到RtpVideoStreamReceiver模块,当模块RtpVideoStreamReceiver每次对rtp包进行处理的时候都会调用NackModule::OnReceivedPacket()主动驱动NackModule模块.
nack list
- insert :
- insert : AddPacketsToNack()会判断包的连续性,相应的丢包序列如果不在recover list里面就会插入
- erase :
- 1.序列号距离当前收到的序列号过旧的包kMaxPacketAge(10000)
- 2.nack_list 大小 + 即将插入的nack 序列数量如果超过kMaxNackPackets(1000) 就会清理掉关键帧之前的nack,循环直至size 小于1000 或者 已经到了最新关键帧
- 3.如果经步骤2 nack_list大小仍然超过了nackkMaxNackPackets(1000) 会全部清理掉,并重新请求关键帧
- 4.收到乱序的包, 可能是抖动过来的 或者 后面恢复过来的包.
- 5.发送超过10次仍然没有收到重传回来的包.
keyFrame list & recovered list
- insert :
- 只需要判断是否是关键帧或恢复过来的包即可插入
- erase:
- 三个缓存都相同的删除点,清理序列号距离当前收到的序列号过旧的包kMaxPacketAge(10000) 例如[6,7,...,100007],6即被清理.
nack 发送的策略
前面提的两种发送处理在NackModule2::GetNackBatch()里面,一处在创建模块的时候就会启动的定时任务里定时调用,一处在NackModule2:: OnReceivedPacket()检查完包连续性后就会立即调用.
NackModule2::GetNackBatch(kSeqNumOnly) : kSeqNumOnly 根据序列号判断是否发送nack
仅在第一次发送的时会用到序列号方式,延迟发送时间为kDefaultSendNackDelayMs(0ms), 所以基本是入nack_lisk后就立即发送了(可以作为优化点之一后面提), 发送后会更新nack_list[seq].send_at_time = now(), 供后续定时任务判断是否超时NackModule2::GetNackBatch(kTimeOnly) :kTimeOnly 根据时间判断是否发送nack,在没有打开补偿配置的情况下间隔为一个rtt时间,rtt会动态更新(默认频率1000ms), 初始值为kDefaultRttMs(100ms), 再次发送的时间 resend_delay 默认为一个rtt 时间,即一个rtt时间后没有收到重传回来的nack,就继续发送, 实验阶段增加了补偿配置,可以动态延长resend_delay 延迟, 可以作为改进方案之一, 后面有提.
nack 模块的几个重要常量
- nack 模块的几个重要常量
const int kMaxPacketAge = 10000; //三个缓存list包序列的生存长度 const int kMaxNackPackets = 1000; // nack_list 存储 packets 的最大大小 const int kDefaultRttMs = 100; // 默认rtt 时间 const int kMaxNackRetries = 10; // 最大重试次数 const int kDefaultSendNackDelayMs = 0; // 延迟发送nack时间 static constexpr TimeDelta kUpdateInterval = TimeDelta::Millis(20); // 定时发送nack任务的周期
改进参考
经过调研和一小部分数据测试,发现nack有几个可能的优化点
配置一个合适的发送延迟
收到正常顺序外的包,原生机制默认是直接就返送nack的, 当前版本支持了NACK延时发送机制,通过控制NACK延时发送的时间间隔,避免固定延时网络下无必要的重传请求。比如,如果kDefaultSendNackDelayMs=20ms,如果因为网络的固有延时,造成某些数据包迟到了10ms,而此时没有NACK延时发送机制的话,这些包都会被认为丢了,从而对这些包请求重传。但是如果有20ms的NACK延时发送,这些包就不会被计算为丢失,从而避免了没有必要的重传请求,避免了资源浪费
[023:217](nack_module2.cc:182): OnReceivedPacket:seq_num|newest_seq_num_|is_keyframe|is_recovered|loss_ratio|recover_ratio: 25402|25399|0|0|36.21%|0.00%|0|0|903
[023:218](nack_module2.cc:182): OnReceivedPacket:seq_num|newest_seq_num_|is_keyframe|is_recovered|loss_ratio|recover_ratio: 25401|25402|0|0|36.24%|0.00%|0|0|905
[023:219](rtp_video_stream_receiver2.cc:745): RtpVideoStreamReceiver2::RequestPacketRetransmit:SendNack:sn:size():2:|25400|25401|
重发补偿
改进2和上面情况其实类似,
如下日志, 序列33156 刚发送完第二次nack请求(69:466ms), 仅30ms之差收到了重传包(069:496ms),之后又收到了一次重传包(69:843ms),浪费网络资源.
可以配置重传补偿,每次重传时间增加%25,原生机制自带,需要打开配置。
[069:326](video_receive_stream2.cc:581):VideoReceiveStream2:OnRttUpdate|avg_rtt_ms|max_rtt_ms: 377ms|407ms
[069:466](rtp_video_stream_receiver2.cc:742):RtpVideoStreamReceiver2::RequestPacketRetransmit:SendNack:sn:size():1:|33156|
[069:496](rtx_receive_stream.cc:70):RtxReceiveStream::OnRtpPacket:recovered:sq:33156
[069:497](nack_module2.cc:181):OnReceivedPacket:seq_num|newest_seq_num_|is_keyframe|is_recovered|loss_ratio|recover_ratio|dup_recover_ratio:33156|33167|0|1|29.90%|98.80%|30.37%
----
[069:843](rtx_receive_stream.cc:70):RtxReceiveStream::OnRtpPacket:recovered:sq:33156
----
//测试发现在 200ms delay %30丢包下 这类重复重传包的情况也有高达%30左右比率
//这个case尝试增加这个改进后,重传比率降低到了 %21左右
----
[327:764](nack_module2.cc:182):OnReceivedPacket:seq_num|newest_seq_num_|is_keyframe|is_recovered|loss_ratio|recover_ratio|dup_recover_ratio: 23891|23894|0|1|30.93%|97.13%|%21.88
Audio Nack
Audio Nack 默认不打开,可通过配置feedback参数打开
codec.AddFeedbackParam(
FeedbackParam(kRtcpFbParamNack, kParamValueEmpty));
具体实现在NackTracker中, 机制大同小异
可改进点
:
固定丢包场景,高丢包等场景,可重置RTT校验策略,增强nack重传效果,配合控制neteq buffer 低水位高度,实验测试可以做到80-90%抗接收丢包.
SRS Nack
机制与webrtc 大同小异, 调用栈如下:
默认nack_list 大小 audio 66 video 666 定时默认20ms
if (is_audio) {
rtp_queue_ = new SrsRtpRingBuffer(100);
nack_receiver_ = new SrsRtpNackForReceiver(rtp_queue_, 100 * 2 / 3);
} else {
rtp_queue_ = new SrsRtpRingBuffer(1000);
nack_receiver_ = new SrsRtpNackForReceiver(rtp_queue_, 1000 * 2 / 3);
}
----
SrsRtcConnectionNackTimer::SrsRtcConnectionNackTimer(SrsRtcConnection* p) : p_(p)
{
_srs_hybrid->timer20ms()->subscribe(this);
}
默认初始常量
SrsNackOption::SrsNackOption()
{
max_count = 15;
max_alive_time = 1000 * SRS_UTIME_MILLISECONDS;
first_nack_interval = 10 * SRS_UTIME_MILLISECONDS;
nack_interval = 50 * SRS_UTIME_MILLISECONDS;
max_nack_interval = 500 * SRS_UTIME_MILLISECONDS;
min_nack_interval = 20 * SRS_UTIME_MILLISECONDS;
nack_check_interval = 20 * SRS_UTIME_MILLISECONDS;
}