webrtc QOS筆記四 Nack機制淺析

2023-04-04 12:00:24

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;
}