WebRTC NACK

本文介绍了WebRTC中用于应对丢包问题的NACK机制。NACK是一种反馈机制,当接收方检测到丢包时,会发送NACK请求让发送方重传。文章详细讨论了NACK的工作原理、实现细节,包括如何判断丢包、如何处理NACK请求以及相关的限制策略,如最大请求次数、处理周期等。

一、基本原理

WebRTC是基于udp协议来进行传输音视频数据的,所以基于udp的特性,RTC采用了2种方式来优化丢包问题

  • fec,前向纠错,在每个数据包中添加一些关于前一个信息的信息,以防丢失。在接收端,需要重新构建它们,如果fec为5%,那么在丢包小于5%的情况下,都可以通过fec进行恢复,组成完整的帧,但是需要发送额外的包,会占用更大的带宽,具体实现不做讨论。
  • NACK机制,和TCP的ACK机制正好相反,NACK是用来确认丢包的发送协议,当接收方检测到有丢包时,它会发送NACK类型的RTCP包给发送方,发送方会重发这些数据。

NACK 模块是 WebRTC 对抗弱网的核心 QoS 技术之一,有两种发送模式,一种是基于时间序列的发送模式,一种是基于包序列号的发送模式。

二、具体实现

当接收方检测到有丢包时,它会发送NACK类型的RTCP包给发送方。针对现实中网络的复杂程度,我们思考以下几个问题:

  • 如何判断丢包?也就是如何选择发送NACK的时机?因为UDP是无连接状态的,不能保证数据的连续性,比如我们先收到了序号1的包, 第二次收到了序号3的包,那么此时是否可以认为序号2的包已经丢失,需要发送NACK报告丢包情况,但很有可能下一时刻2号包就到了
  • 是否需要一直发送NACK包?假如我们发送NACK后,可能因为网络原因等等,一直没有收到接受方发送的重发包,那么还要一直继续发送NACK吗,会不会导致服务链路拥塞
  • 如果丢包数量过多,超过了一定的数量,是否需要放弃之前的丢包数据,不再进行发送NACK?

RTC内部也考虑到了这些问题,目前有一些实施的策略来保证,记住几个关键的数字如下。

const int kMaxNackRetries = 10;
const int kProcessIntervalMs = 20;
const int kDefaultRttMs = 100;
const int kMaxNackPackets = 1000;
const int kMaxPacketAge = 10000;
  • NACK 模块对同一包号的最大请求次数是10次,超过这个最大次数限制,会把该包号移出 nack_list,放弃对该包的重传请求。
  • NACK 模块每隔 20 毫秒批量处理 nack_list,获取一批请求包号存储到 nack_batch,生成 nack 包并发送。不过,nack_list 的处理周期并不是固定的 20ms ,而是基于 20ms 动态变化
  • NACK 模块默认 rtt 时间,如果距离上次 nack 发送时间不到一个 rtt 时间,那么不会发送 nack 请求,注意,100ms 只是 rtt 的默认值,在实际应用中,rtt 应该要根据网络状况动态计算,计算方式有很多种,比如对于接收端来说,可以通过发送 xr 包来计算 rtt。
  • nack_list 的最大长度,即本次发送的 nack 包至多可以对 1000 个丢失的包进行重传请求。
  • nack_list 中包号的距离不能超过 10000 个包号。即 nack_list 中的包号始终保持 [cur_seq_num - 10000, cur_seq_num] 这样的跨度,以保证 nack 请求列表中不会有太老旧的包号

    关于第四点,nack_list 的最大长度,这里拉出来单独理解下,如果丢失的包数量超过 1000,会循环清空 nack_list 中关键帧之前的包,直到其长度小于 1000,但是并不是清除到刚好到1000的数量,也就是说,放弃对关键帧首包之前的包的重传请求,直接而快速的以关键帧首包之后的包号作为重传请求的开始。

    我们知道在一个GOP内,解码时,后面的帧都是参考前面的帧进行解码的,如果一个GOP内,前面的帧被清掉了,后面的也没有重传的必要

    举个例子,假如我们接收方,收到的包序号是 1/701/1201,并且都是关键帧的包,那按照上面的算法,我们丢失包是700+500 = 1200个,此时触发了大于1000的条件,那么需要清空超过的包体,按照上面关键帧的算法,那么这里会将701之前的包都会清除掉保证重传的意义。因为如果按照只清除超过的包体算法,只会清除1-201的包,但是如果这样,201-700的包体,重传了也没有意义,因为无法进行解码。

  • 源码分析

  • class NackModule : public Module {
     public:
      ..............
      int OnReceivedPacket(uint16_t seq_num, bool is_keyframe);
      int OnReceivedPacket(uint16_t seq_num, bool is_keyframe, bool is_recovered);
    ​
      void ClearUpTo(uint16_t seq_num);
      void UpdateRtt(int64_t rtt_ms);
      void Clear();
    ​
      // Module implementation
      int64_t TimeUntilNextProcess() override;
      void Process() override;
    ​
     private:
      struct NackInfo {
        NackInfo();
        NackInfo(uint16_t seq_num,
                 uint16_t send_at_seq_num,
                 int64_t created_at_time);
    ​
        uint16_t seq_num;
        uint16_t send_at_seq_num;
        int64_t created_at_time;
        int64_t sent_at_time;
        int retries;
      };
      std::map<uint16_t, NackInfo, DescendingSeqNumComp<uint16_t>> nack_list_
          RTC_GUARDED_BY(crit_);
      std::set<uint16_t, DescendingSeqNumComp<uint16_t>> keyframe_list_
          RTC_GUARDED_BY(crit_);
      std::set<uint16_t, DescendingSeqNumComp<uint16_t>> recovered_list_
          RTC_GUARDED_BY(crit_);
      video_coding::Histogram reordering_histogram_ RTC_GUARDED_BY(crit_);
      bool initialized_ RTC_GUARDED_BY(crit_);
      int64_t rtt_ms_ RTC_GUARDED_BY(crit_);
      uint16_t newest_seq_num_ RTC_GUARDED_BY(crit_);
      // Only touched on the process thread.
      int64_t next_process_time_ms_;
      // Adds a delay before send nack on packet received.
      const int64_t send_nack_delay_ms_;
    ​
      const absl::optional<BackoffSettings> backoff_settings_;
    };
    }  // namespace webrtc
    ​
    #endif  // MODULES_VIDEO_CODING_DEPRECATED_NACK_MODULE_H_

  • nack_list_,丢包的数组,如果判断符合NACK条件,添加到数组之中
  • keyframe_list_,关键帧数组
  • recovered_list_,丢包重传恢复的数组
  • newest_seq_num_,当前最新的包的序号,用来判断包是否连续等等
  • NackInfo ,非常重要的一个结构体,上面我们说到NACK 有两种发送模式,基于时间和基于包序号的,如果sent_at_time 为-1,那么这是一个基于序列号发送的 nack,而且要在当前接收的最新包号 newest_seq_num_ 大于等于 send_at_seq_num 时才会发送。sent_at_time如果有值,那么这是一个基于时间序列发送的 nack,要将这个参数结合当前 rtt 来决定是否发送重传请求。

OnReceivedPacket 

通过该方法,可以拿到当前接收的所有RTP包的序列号,进行丢包检测

int NackModule2::OnReceivedPacket(uint16_t seq_num,
                                  bool is_keyframe,
                                  bool is_recovered) {
  RTC_DCHECK_RUN_ON(worker_thread_);
  // TODO(philipel): When the packet includes information whether it is
  //                 retransmitted or not, use that value instead. For
  //                 now set it to true, which will cause the reordering
  //                 statistics to never be updated.
  bool is_retransmitted = true;
​
  if (!initialized_) {
    newest_seq_num_ = seq_num;
    if (is_keyframe)
      keyframe_list_.insert(seq_num);
    initialized_ = true;
    return 0;
  }
​
  // Since the |newest_seq_num_| is a packet we have actually received we know
  // that packet has never been Nacked.
  if (seq_num == newest_seq_num_)
    return 0;
​
  if (AheadOf(newest_seq_num_, seq_num)) {
    // An out of order packet has been received.
    auto nack_list_it = nack_list_.find(seq_num);
    int nacks_sent_for_packet = 0;
    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;
  }
​
  // Keep track of new keyframes.
  if (is_keyframe)
    keyframe_list_.insert(seq_num);
​
  // And remove old ones so we don't accumulate keyframes.
  auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);
  if (it != keyframe_list_.begin())
    keyframe_list_.erase(keyframe_list_.begin(), it);
​
  if (is_recovered) {
    recovered_list_.insert(seq_num);
​
    // Remove old ones so we don't accumulate recovered packets.
    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;
  }
  AddPacketsToNack(newest_seq_num_ + 1, seq_num);
  newest_seq_num_ = seq_num;
​
  // Are there any nacks that are waiting for this seq_num.
  std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);
  if (!nack_batch.empty()) {
    // This batch of NACKs is triggered externally; the initiator can
    // batch them with other feedback messages.
    nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);
  }
​
  return 0;
}
  • 如果seq_num == newest_seq_num_,那说明是连续的包,不需要处理
  • 接下里判断,如果AheadOf(newest_seq_num_, seq_num),也就是收到的包序号比当前的最新的序号要小,那这里有两种情况
    • 因为包的乱序导致之前的包晚一点才到
    • 经过NACK后重传后的包到达
  • 通过nack_list_.find(seq_num)看看能否找到,如果能找到,则说明是重传的包,那么需要将这个包进行移除,反之则是乱序到达的包。
  • 判断包的连续性,如果当前包号不连续,则将中间断掉的包号加入到 nack 请求列表,并更新 newest_seq_num_
  • 一旦发现 nack_list 的大小已经超过 1000,那么就要根据关键帧序列号来调整其大小,也就是上面到的策略4
  • 最后,批量获取 nack_list 中的包序列号到数组 nack_batch 中,生成并发送 nack 包。

三、实现效果 

没有启动重传时,设置10%丢包 ,可以明显看到推流端与拉流端之间的画面差异。

 

启动重传时,设置30%丢包 ,可以看到推流端与拉流端之间的画面并无明显差异。 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值