機制概述
nack 三個快取list
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模組原始碼簡析
前面提的兩種傳送處理在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 延遲, 可以作為改進方案之一, 後面有提.
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 預設不開啟,可通過設定feedback引數開啟
codec.AddFeedbackParam(
FeedbackParam(kRtcpFbParamNack, kParamValueEmpty));
具體實現在NackTracker中, 機制大同小異
可改進點 :
固定丟包場景,高丟包等場景,可重置RTT校驗策略,增強nack重傳效果,配合控制neteq buffer 低水位高度,實驗測試可以做到80-90%抗接收丟包.
機制與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;
}