导语 | 上一篇文章我们详解了WebRTC中视频接收端NACK的实现,本文将为大家进一步详细解读WebRTC中视频接收端NACK的实现。文章中引用的WebRTC代码基于master,commit:f412945f05ce1ac372a7dad77d85498d23deaae源码分析。
概述
WebRTC接收端触发发送NACK报文有两处:
- 接收RTP报文,对序列号进行检测,发现有丢包,立即触发发送NACK报文;
- 定时检查nack_list_队列,发现丢包满足申请重传条件,立即触发发送NACK报文。
函数实现
1. 检测丢包触发
核心函数是NackModule2::OnReceivedPacket。
NackModule2::OnReceivedPacket函数在整个网络报文接收线程的调用栈的位置:
RtpVideoStreamReceiver::OnReceivedPayloadData调用NackModule2::OnReceivedPacket:
2. 定时检查触发
kTimeOnly模式默认周期调度时间是20ms。
-
static constexpr TimeDelta kUpdateInterval = TimeDelta::Millis(20);
3. 核心函数思想
NackModule2::AddPacketsToNack和NackModule2::GetNackBatch是NACK核心函数。
NackModule2::AddPacketsToNack:决定是否将该报文放入NACK队列。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
void NackModule2::AddPacketsToNack(uint16_t seq_num_start, uint16_t seq_num_end) { // Called on worker_thread_. // Remove old packets. auto it = nack_list_.lower_bound(seq_num_end - kMaxPacketAge); nack_list_.erase(nack_list_.begin(), it);
// If the nack list is too large, remove packets from the nack list until // the latest first packet of a keyframe. If the list is still too large, // clear it and request a keyframe. uint16_t num_new_nacks = ForwardDiff(seq_num_start, seq_num_end); if (nack_list_.size() + num_new_nacks > kMaxNackPackets) { while (RemovePacketsUntilKeyFrame() && nack_list_.size() + num_new_nacks > kMaxNackPackets) { } if (nack_list_.size() + num_new_nacks > kMaxNackPackets) { nack_list_.clear(); RTC_LOG(LS_WARNING) << "NACK list full, clearing NACK" " list and requesting keyframe."; keyframe_request_sender_->RequestKeyFrame(); return; } } for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) { // Do not send nack for packets that are already recovered by FEC or RTX if (recovered_list_.find(seq_num) != recovered_list_.end()) continue; NackInfo nack_info(seq_num, seq_num + WaitNumberOfPackets(0.5), clock_->TimeInMilliseconds()); RTC_DCHECK(nack_list_.find(seq_num) == nack_list_.end()); nack_list_[seq_num] = nack_info; }}
该函数的中心思想是:
- nack_list的最大长度为kMaxNackPackets,即本次发送的nack包至多可以对kMaxNackPackets个丢失的包进行重传请求。如果丢失的包数量超过kMaxNackPackets,会循环清空nack_list中关键帧之前的包,直到其长度小于kMaxNackPackets。也就是说,放弃对关键帧首包之前的包的重传请求,直接而快速的以关键帧首包之后的包号作为重传请求的开始;
- nack_list中包号的距离不能超过kMaxPacketAge个包号。即nack_list中的包号始终保持 [cur_seq_num - kMaxPacketAge, cur_seq_num] 这样的跨度,以保证nack请求列表中不会有太老旧的包号。
NackModule2::GetNackBatch:决定是否发送NACK请求重传该报文,两种触发方式都是调用这个函数。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
std::vector<uint16_t> NackModule2::GetNackBatch(NackFilterOptions options) { // Called on worker_thread_. bool consider_seq_num = options != kTimeOnly; bool consider_timestamp = options != kSeqNumOnly; Timestamp now = clock_->CurrentTime(); std::vector<uint16_t> nack_batch; auto it = nack_list_.begin(); while (it != nack_list_.end()) { TimeDelta resend_delay = TimeDelta::Millis(rtt_ms_); if (backoff_settings_) { resend_delay = std::max(resend_delay, backoff_settings_->min_retry_interval); if (it->second.retries > 1) { TimeDelta exponential_backoff = std::min(TimeDelta::Millis(rtt_ms_), backoff_settings_->max_rtt) * std::pow(backoff_settings_->base, it->second.retries - 1); resend_delay = std::max(resend_delay, exponential_backoff); } } bool delay_timed_out = now.ms() - it->second.created_at_time >= send_nack_delay_ms_; bool nack_on_rtt_passed = now.ms() - it->second.sent_at_time >= resend_delay.ms(); bool nack_on_seq_num_passed = it->second.sent_at_time == -1 && AheadOrAt(newest_seq_num_, it->second.send_at_seq_num); if (delay_timed_out && ((consider_seq_num && nack_on_seq_num_passed) || (consider_timestamp && nack_on_rtt_passed))) { nack_batch.emplace_back(it->second.seq_num); ++it->second.retries; it->second.sent_at_time = now.ms(); if (it->second.retries >= kMaxNackRetries) { RTC_LOG(LS_WARNING) << "Sequence number " << it->second.seq_num << " removed from NACK list due to max retries."; it = nack_list_.erase(it); } else { ++it; } continue; } ++it; } return nack_batch;}
该函数的中心思想是:
- 因为报文有可能出现乱序抖动情况,不能说检测出丢包就立即重传,需要等待send_nack_delay_ms_,当等待时间大于send_nack_delay_ms_,申请重传。send_nack_delay_ms_是系统初始化时,在GetSendNackDelay()配置。根据实际场景配置合理值。比方说可以牺牲一定带宽保证实时性要求比较高场景,send_nack_delay_ms_可以配置成0;
- 因为NACK产生的延时主要在RTT环路延时上,所以再次重传的时间一定要大于rtt_ms_,当两次发送NACK重传请求时间大于rtt_ms_时,才会申请再次重传;
- 视频会议场景对实时性要求很高,当报文一直处于丢包状态,不能持续申请重传,最大重传次数为kMaxNackRetries,超过最大重传次数,放弃该报文。不再重传。使用其他QoS手段进行恢复。
接收端NACK参数汇总
关于云架构平台部
云架构平台部是腾讯规模最大的技术部门之一,长期深耕音视频、存储、接入和计算服务等技术领域,通过海量的存储和数据库平台,世界级的CDN&音视频服务,先进的操作系统和视频编解码技术,助力腾讯云以技术的力量持续赋能客户,帮他们提升效率,降低成本。
WebRTC相关资源汇总
为了方便广大开发者快速了解上手WebRTC,我们对WebRTC相关的开源项目、工作招聘、测试工具以及行业内的RTC厂商资源进行了汇总。感兴趣的同学可以点击「阅读原文」前往 https://github.com/webrtcwork/webrtcwork 全面了解WebRTC相关内容
腾讯云音视频在音视频领域已有超过21年的技术积累,持续支持国内90%的音视频客户实现云上创新,独家具备 RT-ONE™ 全球网络,在此基础上,构建了业界最完整的 PaaS 产品家族,并通过腾讯云视立方 RT-Cube™ 提供All in One 的终端SDK,助力客户一键获取众多腾讯云音视频能力。腾讯云音视频为全真互联时代,提供坚实的数字化助力。