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;
}
posted @ 2023-04-04 11:24  靑い空゛  阅读(393)  评论(0编辑  收藏  举报