如何把电脑改成服务器做网站,商超运营与管理,安阳市最新消息,网站栏目建设调研WebRTC 中的 NACK#xff08;Negative Acknowledgment#xff09;机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象#xff0c;尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响#xff0c;从而保持通信的连续性和质量…WebRTC 中的 NACKNegative Acknowledgment机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响从而保持通信的连续性和质量。
1. 总体架构
WebRTC NACK 总体架构如下图所示。
1发送端发送 RTP 报文时会缓存一份到 RtpPacketHistory收到 NACK 请求的时候从 RtpPacketHistory 获取对应缓存报文发送出去。RtpPacketHistory 收到 TransportFeedback 会将接收端确认的报文从缓存中移除。
2接收端收到的所有报文都从 NackRequester 过一遍只需要序列号丢失了哪个报文门清。由于丢包和乱序无法分辨NackRequest 在定时器驱动下发送 NACK 请求极端丢包情况会发送关键帧请求。 2. 发送端
2.1. 调用流程
发送端的调用流程由三条子流程组成
1发送出去的报文会被缓存到 RtpPacketHistory用来响应 NACK 请求。
2收到 TransportFeedback 将对应报文从 RtpPacketHistory 中移除。
3收到 NACK 请求从 RtpPacketHistory 获取缓存报文发送出去。 2.2. RtpPacketHistory
RtpSenderEgress 负责报文发送发送完后将报文缓存到 RtpPacketHistory。ModuleRtpRtcpImpl2 处理所有 RTCP 报文NACK 请求交给 RTPSender 处理RTPSender 从 RtpPacketHistory 获取请求重传的报文然后发送出去。 2.2.1. 重传条件
RtpSenderEgress 只会将满足条件的报文缓存到 RtpPacketHistory。正常的视频帧需要重传但 FEC 报文不重传。另外对于 simulcast 或 SVC需要根据重传策略来决定判断逻辑比较复杂这里暂不分析。
void RtpSenderEgress::CompleteSendPacket(const Packet compound_packet,bool last_in_batch) {...if (is_media packet-allow_retransmission()) {packet_history_-PutRtpPacket(std::make_uniqueRtpPacketToSend(*packet), now);} else if (packet-retransmitted_sequence_number()) {packet_history_-MarkPacketAsSent(*packet-retransmitted_sequence_number());}...
}
2.2.2. 队列长度
缓存队列长度非常重要太长的话会引入较大延迟太短的话会导致重传 miss。因此队列长度的设置需要在延迟和 miss 之间取得一个较好的平衡。
WebRTC 从时间和数量两个维度来对队列长度进行限制其中kMaxCapacity 是一个硬性数量限制不管缓存的报文是否新鲜都不能超过这个限制。
// packet_duration max(1 second, 3x RTT).
static constexpr TimeDelta kMinPacketDuration TimeDelta::Seconds(1);
static constexpr int kMinPacketDurationRtt 3;// With kStoreAndCull, always remove packets after 3x max(1000ms, 3x rtt).
static constexpr int kPacketCullingDelayFactor 3;// number_to_store_ min(kMaxCapacity, kMinSendSidePacketHistorySize)
static constexpr size_t kMaxCapacity 9600;
static const int kMinSendSidePacketHistorySize 600;
void RtpPacketHistory::CullOldPackets()
{// 当前时间Timestamp now clock_-CurrentTime();// 取 3 倍 RTT 和 1秒两者较大值即不小于 1 秒TimeDelta packet_duration rtt_.IsFinite()? std::max(kMinPacketDurationRtt * rtt_, kMinPacketDuration): kMinPacketDuration;while (!packet_history_.empty()) {// 队列中报文数量超过最大容量限制if (packet_history_.size() kMaxCapacity) {RemovePacket(0); // 移除最旧的报文continue;}// 取队列首报文进行判断const StoredPacket stored_packet packet_history_.front();// 正在重传中退出if (stored_packet.pending_transmission_) {return;}// 还很新鲜未超时退出if (stored_packet.send_time() packet_duration now) {return;}// 首报文已经不新鲜如果报文数量多或者首报文太老才需要移除if (packet_history_.size() number_to_store_ ||stored_packet.send_time() (packet_duration * kPacketCullingDelayFactor) now) {RemovePacket(0);} else {// No more packets can be removed right now.return;}}
}
2.2.3. PaddingMode
RtpPacketHistory 还可以用来生成带宽探测所需的 padding 报文用真实报文当 padding 报文既填充了码率又实现了冗余一石二鸟。
RtpPacketHistory 中缓存了很多报文挑选哪些报文做 padding 报文支持三种 padding 模式
enum class PaddingMode {// 选择最近缓存的报文作为 Padding 报文kDefault,// 基于发送时间、重传次数等因素选择更好的历史报文作为 Padding 报文kPriority,// 使用最近缓存的大包作为Padding报文kRecentLargePacket
};
对于 kPriority 模式优先级定义如下
bool RtpPacketHistory::MoreUseful::operator()(StoredPacket* lhs,StoredPacket* rhs) const {// 没有重传过的报文优先级更高if (lhs-times_retransmitted() ! rhs-times_retransmitted()) {return lhs-times_retransmitted() rhs-times_retransmitted();}// 时间越近的报文优先级越高return lhs-insert_order() rhs-insert_order();
}
最新代码已经不再使用 kDefault 模式。
RtpPacketHistory::PaddingMode GetPaddingMode(const FieldTrialsView* field_trials) {if (!field_trials ||!field_trials-IsDisabled(WebRTC-PaddingMode-RecentLargePacket)) {return RtpPacketHistory::PaddingMode::kRecentLargePacket;}return RtpPacketHistory::PaddingMode::kPriority;
}
3. 接收端
3.1. 调用流程
NackRequester 是接收端的 NACK 控制核心调用流程如下图所示。
1RtpVideoStreamReceiver2 收到报文在进行处理的同时也要通知 NackRequester。
2NackRequester 内部有一个 NACK 请求队列如果发现有丢包就会添加一个 NACK 请求项。
3NackPeriodicProcessor 会定时调用 NackRequester 发送 NACK 请求。
4通过层层调用将 NACK 请求发送出去。 3.2. NackRequester
每一个 RtpVideoStreamReceiver2 都持有一个 NackRequester用来发起 NACK 请求。NackRequester 被 NackPeriodicProcessor 定时驱动NACK 请求通过 NackSender 发送出去。如果丢包特别严重NackRequester 会使用 KeyFrameRequestSender 发起关键帧请求。 3.2.1. NackList
NackList 是 NackRequester 内部的 NACK 请求队列。每次收到新的报文与最近收到的报文 SN 进行比较如果两个 SN 之间有空洞SN 跳跃认为有丢包以空洞 SN 创建 NACK 请求项插入 NackList。
// 队列中首尾报文Sequence Number的最大跨度适用于NackList、KeyFrameList和RecoveredList
constexpr int kMaxPacketAge 10000;
// 队列中最大报文数
constexpr int kMaxNackPackets 1000;
// 最大重传次数
constexpr int kMaxNackRetries 10;
因为空洞也可能是乱序导致后续可能立即就会收到丢失报文所以不能立即发送 NACK 请求。WebRTC 会启动一个定时器确定 NackRequester 定时检查 NackList 中的 NACK 项判断是否需要发送 NACK 请求。
决定选取哪些 NACK 项发起 NACK 请求有不同筛选条件
enum NackFilterOptions { kSeqNumOnly, kTimeOnly, kSeqNumAndTime };
1kSeqNumOnly
基于报文乱序情况每个 NACK 项插入队列时都会计算一个触发重传的 SN表示后续收到此 SN 报文时如果NACK 项还在队列中且还没有发起过 NACK 请求则立即触发一次。
每收到一个报文会检查此条件当瞬时丢包比较严重的时候能够比定时器更快触发 NACK 请求的发送类似于 TCP 的快速重传机制。
2kTimeOnly
每次发送 NACK 请求都会更新 NACK 的最近请求时间如果最近请求时间距当前时间超过一个 RTT则会重新触发 NACK 请求。此条件由定时器驱动进行检查。
3kSeqNumAndTime
相当于“kSeqNumOnly || kTimeOnly”只要一个条件满足就会触发 NACK 请求。好像未使用
3.2.2. KeyFrameList
KeyFrameList 存储每个关键帧的第一个报文用来协助 NackList 进行收缩。对于视频来说GOP 中的帧是有依赖关系的如果前面的帧没有恢复恢复后面的帧没有意义。因此当 NackList 请求项溢出需要移除一些腾出空间时WebRTC 是按照 GOP 粒度去丢弃历史久远的 NACK 请求项。
下面举例说明。假设有一个视频流每个 GOP 由 5 个非 I 帧 报文和 2 个 I 帧报文组成报文序列如下所示
1,2,3,4,5,6,7,8,9,10,11,12,13,14,...
如果没有及时收到 3、4、11、13 四个报文NackList 和 KeyFrameList 状态如下 此时如果需要创建新的 NACK 项但 NackList 空间不够需要丢弃 GOP13和4两个Nack项状态如下 NackList 空出两个表项如果空间还不够则从 KeyFrameList 中弹出表项直到 SN 比 NackList 中的大然后重复删除过程。 3.2.3. RecoveredList
NackRequester 内部有一个 RecoveredList如果收到的是通过 FEC 或 RTX 恢复的报文不会用来生成 NACK 请求项而是被保存到 RecoveredList 中。在创建 NACK 请求项时如果此报文已经被恢复了则需要跳过。
为什么不把恢复报文当成普通的报文来处理目前看是如果那样做会影响乱序的统计而乱序的统计又会影响前面讲到的 kSeqNumOnly 快速重传序号的计算。
3.3. 源码分析
3.3.1. OnReceivedPacket
这是 NackRequester 主函数收到每个报文都需要调用此函数来生成或移除 NACK 请求项。
int NackRequester::OnReceivedPacket(uint16_t seq_num, bool is_keyframe,bool is_recovered) {bool is_retransmitted true;// 初始化if (!initialized_) {newest_seq_num_ seq_num;if (is_keyframe)keyframe_list_.insert(seq_num);initialized_ true;return 0;}// 重复接收if (seq_num newest_seq_num_)return 0;// 乱序包if (AheadOf(newest_seq_num_, seq_num)) {auto nack_list_it nack_list_.find(seq_num);int nacks_sent_for_packet 0;// 报文已经收到移除 nack 项if (nack_list_it ! nack_list_.end()) {nacks_sent_for_packet nack_list_it-second.retries;nack_list_.erase(nack_list_it);}// 直方图统计乱序情况重传报文的乱序不统计if (!is_retransmitted)UpdateReorderingStatistics(seq_num);return nacks_sent_for_packet;}// 保存关键帧报文序列号if (is_keyframe)keyframe_list_.insert(seq_num);// 关键帧报文太多了清理下auto it keyframe_list_.lower_bound(seq_num - kMaxPacketAge);if (it ! keyframe_list_.begin())keyframe_list_.erase(keyframe_list_.begin(), it);// 经 FEC 或 RTX 恢复的报文if (is_recovered) {recovered_list_.insert(seq_num);// 恢复报文太多清理下auto it recovered_list_.lower_bound(seq_num - kMaxPacketAge);if (it ! recovered_list_.begin())recovered_list_.erase(recovered_list_.begin(), it);// Do not send nack for packets recovered by FEC or RTX.return 0;}// 走到这里 seq_num 肯定比 newest_seq_num 大newest_seq_num_ 1, seq_num 之间// 可能存在 0 个或多个空洞这些空洞就是需要发送nack的报文AddPacketsToNack(newest_seq_num_ 1, seq_num);// 更新收到的最新序列号newest_seq_num_ seq_num;// 这里仅发送基于序列号触发的 NACK 请求std::vectoruint16_t nack_batch GetNackBatch(kSeqNumOnly);if (!nack_batch.empty()) {nack_sender_-SendNack(nack_batch, /*buffering_allowed*/true);}return 0;
}
3.3.2. AddPacketsToNack
当新收到报文与最近收的报文之间有空洞时会调用此函数插入 NACK 请求项。这里要关注下极端情况会清空 NACK 请求列表直接发送关键帧请求。
void NackRequester::AddPacketsToNack(uint16_t seq_num_start, uint16_t seq_num_end) {// NACK 项太多了清理下auto it nack_list_.lower_bound(seq_num_end - kMaxPacketAge);nack_list_.erase(nack_list_.begin(), it);// 计算空洞数量uint16_t num_new_nacks ForwardDiff(seq_num_start, seq_num_end);// 确保添加空洞 NACK 项后总 NACK 项不会超过最大限制if (nack_list_.size() num_new_nacks kMaxNackPackets) {// 先移除关键帧之前的 NACK 项while (RemovePacketsUntilKeyFrame() nack_list_.size() num_new_nacks kMaxNackPackets) {}// 还是腾不出足够的空间则清空 NACK 队列直接请求 I 帧if (nack_list_.size() num_new_nacks kMaxNackPackets) {nack_list_.clear();keyframe_request_sender_-RequestKeyFrame();return;}}// 遍历所有空洞创建 NACK 项for (uint16_t seq_num seq_num_start; seq_num ! seq_num_end; seq_num) {// 空洞报文可能已经被 FEC 或 RTX 恢复if (recovered_list_.find(seq_num) ! recovered_list_.end())continue;// 使用乱序长度的中位数来计算触发重传的序列号NackInfo nack_info(seq_num, seq_num WaitNumberOfPackets(0.5), clock_-CurrentTime());nack_list_[seq_num] nack_info;}
}
3.3.3. GetNackBatch
定时器驱动调用此函数定时检查发送 NACK 请求项。
void NackRequester::ProcessNacks() {// 定时器驱动只获取基于时间条件判断需要处理的 NACK 项std::vectoruint16_t nack_batch GetNackBatch(kTimeOnly);if (!nack_batch.empty()) {nack_sender_-SendNack(nack_batch, /*buffering_allowed*/false);}
}std::vectoruint16_t NackRequester::GetNackBatch(NackFilterOptions options) {// 仅考虑序列号bool consider_seq_num options ! kTimeOnly;// 仅考虑时间bool consider_timestamp options ! kSeqNumOnly;// 当前时间Timestamp now clock_-CurrentTime();// 筛选结果std::vectoruint16_t nack_batch;auto it nack_list_.begin();while (it ! nack_list_.end()) {// 等待发送 NACK 的时间已经到了bool delay_timed_out now - it-second.created_at_time send_nack_delay_;// 距离上一次发送 NACK 的时间也已经过去很久了bool nack_on_rtt_passed now - it-second.sent_at_time rtt_;// 基于序列号的发送只有在第一次发送Nack时生效bool nack_on_seq_num_passed it-second.sent_at_time.IsInfinite() 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; // 更新发送 NACK 请求次数it-second.sent_at_time now; // 更新最近发送 NACK 请求时间// 已经达到最大请求次数限制从队列中移除不再请求了if (it-second.retries kMaxNackRetries) {it nack_list_.erase(it);} else {it;}continue;}it;}return nack_batch;
}
4. 总结
WebRTC NACK 的实现简单明了发送端缓存报文接收端请求重传。但发送端和接收端实现关注重点不太一样。发送端是被动接收 NACK 请求实现相对简单一些重点关注缓存队列的长度。接收端需要主动发送发送 NACK 请求实现会相对复杂一些由于存在报文乱序什么时候发起 NACK 请求是一个值得斟酌的事情。
除此之外WebRTC 还考虑到了瞬间突发丢包的快速重传机制和基于关键帧的队列收缩等这些都凸显了 WebRTC 对细节的掌控和重视。