• 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;
    }
    
  • 相关阅读:
    【Java】学生管理系统-登录、注册、CRUD(附完整代码)
    Spring基础知识总结(纯文字版)
    软件设计师:程序设计语言
    哈希切割+布隆过滤器
    计算机Android毕业设计论文基于Uniapp+SSM实现的作业管理app
    为什么Redis使用单线程 性能会优于多线程?
    grunt构建
    python读取pdf表格并合并为excel
    第十章《日期与时间》第1节:Date类的使用
    Fastadmin/Tp5.0连接多个数据库
  • 原文地址:https://www.cnblogs.com/ailumiyana/p/17285830.html