WebRTC-nack机制详解

# 1.NACK的含义

丢包重传(NACK)是抵抗网络错误的重要手段。NACK在接收端检测到数据丢包后,发送NACK报文到发送端;发送端根据NACK报文中的序列号,在发送缓冲区找到对应的数据包,重新发送到接收端。NACK需要发送端,发送缓冲区的支持。

WebRTC中支持音频和视频的NACK重传。我们这里只分析nack机制,不分析jitterbuffer或者neteq的更多实现。

# 2.WebRTC中NACK请求发送的条件

这里以视频为例。

下面是webrtc中接收端触发nack的条件,我们看下nack_module.cc文件中OnReceivedPacket的实现。

```

void NackModule::OnReceivedPacket(const VCMPacket& packet) {

  rtc::CritScope lock(&crit_);

  if (!running_)

    return;

  //获取包的seqnum

  uint16_t seq_num = packet.seqNum;

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

  //判断第一帧是不是关键帧

  bool is_keyframe = packet.isFirstPacket && packet.frameType == kVideoFrameKey;

//拿到第一个包的时候判断,把第一个包的seqnum赋值给最新的last_seq_num,如果是关键帧的话,插入到关键帧列表中,同时把initialized_设置为true

  if (!initialized_) {

    last_seq_num_ = seq_num;

    if (is_keyframe)

      keyframe_list_.insert(seq_num);

    initialized_ = true;

    return;

  }

  if (seq_num == last_seq_num_)

    return;

//判断有无乱序,乱序了,如来1,2,3,6包,然后来4包,就乱序了,就把4从nack_list中去掉,不再通知发送端重新发送4了

  if (AheadOf(last_seq_num_, seq_num)) {

    // An out of order packet has been received.

    //把重新收到的包从nack_list中移除掉

    nack_list_.erase(seq_num);

    if (!is_retransmitted)

      UpdateReorderingStatistics(seq_num);

    return;

  } else {

  //没有乱序,如1,2,3,6包,就把(3+1,6)之间的包加入到nack_list中

    AddPacketsToNack(last_seq_num_ + 1, seq_num);

    last_seq_num_ = seq_num;

    // 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);

    // Are there any nacks that are waiting for this seq_num.

    //从nack_list 中取出需要发送 NACK 的序号列表, 如果某个 seq 请求次数超过 kMaxNackRetries = 10次则会从nack_list 中删除.

    std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);

    //LOG(LS_INFO) << "nack_batch size[" << nack_batch.size() << "].";

    //在 NackModule 中触发使用 NackSender::SednNack 发送 NACK 请求

    if (!nack_batch.empty())

      nack_sender_->SendNack(nack_batch);

  }

}

```

我们继续跟踪流程看下AddPacketsToNack函数的实现

```

void NackModule::AddPacketsToNack(uint16_t seq_num_start,

                                  uint16_t seq_num_end) {

  //LOG(LS_INFO) << "AddPacketsToNack. "

  //            << "start seq[" << seq_num_start

  //            << "],end seq[" << 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();

      LOG(LS_WARNING) << "NACK list full, clearing NACK"

                        " list and requesting keyframe.";

    //触发关键帧请求

      keyframe_request_sender_->RequestKeyFrameNack();

      return;

    }

  }

  for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) {

    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;

  }

  //LOG(LS_INFO) << "nack_list size[" << nack_list_.size() << "]";

}

```

我们可以看到AddPacketsToNack()函数主要实现了:

nack_list 的最大容量为 kMaxNackPackets = 1000, 如果满了会删除最后一个 KeyFrame 之前的所有nacked 序号, 如果删除之后还是满的那么清空 nack_list 并请求KeyFrame。

我们继续跟踪流程,我们看下GetNackBatch函数实现

```

std::vector<uint16_t> NackModule::GetNackBatch(NackFilterOptions options) {

  bool consider_seq_num = options != kTimeOnly;

  bool consider_timestamp = options != kSeqNumOnly;

  int64_t now_ms = clock_->TimeInMilliseconds();

  std::vector<uint16_t> nack_batch;

  auto it = nack_list_.begin();

  //LOG(LS_INFO) << "nack_list size[" << nack_list_.size() << "]";

  while (it != nack_list_.end()) {

    bool delay_timed_out =

        now_ms - it->second.created_at_time >= kDefaultSendNackDelayMs;

        //只考虑时间模式

        //当前序号是第一次发送(本地记录的send_at_time == -1)

        //当前最新收到的包序号在这个需要发送NAKC的序号的后面(避免当前还在收之前没收到的包)

//比如当前最新收到100, 当前检测是否需要发送NACK的序号为小于等于100的才满足条件, 比如 99

    if (delay_timed_out && consider_seq_num && it->second.sent_at_time == -1 &&

        AheadOrAt(last_seq_num_, it->second.send_at_seq_num)) {

      nack_batch.emplace_back(it->second.seq_num);

      ++it->second.retries;

      it->second.sent_at_time = now_ms;

      //从nack_list 中取出需要发送 NACK 的序号列表, 如果某个 seq 请求次数超过 kMaxNackRetries = 10次则会从nack_list 中删除

      if (it->second.retries >= kMaxNackRetries) {

        LOG(LS_WARNING) << "Sequence number " << it->second.seq_num

                        << " removed from NACK list due to max retries.";

        //从nack_list_列表中移除

        it = nack_list_.erase(it);

      } else {

        ++it;

      }

      continue;

    }

    //只考虑时间模式

    //发送nack的条件变成,该序号上次发送NACK的时间到当前时间要超过1个RTT(该序号一次也没发送过NACK(send_at_time == -1)也满足

    if (delay_timed_out && consider_timestamp && it->second.sent_at_time + rtt_ms_ <= now_ms) {

      nack_batch.emplace_back(it->second.seq_num);

      ++it->second.retries;

      it->second.sent_at_time = now_ms;

      if (it->second.retries >= kMaxNackRetries) {

        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;

}

```

从上面GetNackBatch函数我们可以知道,获取nack_list存在2种控制逻辑。

# 3.WebRTC中处理NACK请求的实现

1. 首先是正常的 RTCP 处理流程: RTCPReceiver 中解析处理RTCP, 在 rtcp_receiver.cc中的TriggerCallbacksFromRtcpPacket函数中处理不同的RTCP消息.

2. 如果nackSequenceNumbers.size大于0,则触发 RtpRtcp 对象的ModuleRtpRtcpImpl::OnReceivedNack 处理流程。

我们看下OnReceivedNACK/rtp_rtcp_imp.cc函数

```

void ModuleRtpRtcpImpl::OnReceivedNACK(

    int64_t id, const std::list<uint16_t>& nack_sequence_numbers) {

    //将丢包的序号 记录到PacketLossStats, 获取RTT后进入 RTPSedner.OnReceivedNack.

  for (uint16_t nack_sequence_number : nack_sequence_numbers) {

    send_loss_stats_.AddLostPacket(nack_sequence_number);

  }

  if (!rtp_sender_.StorePackets() ||

      nack_sequence_numbers.size() == 0) {

    return;

  }

  // Use RTT from RtcpRttStats class if provided.

  int64_t rtt = rtt_ms();

  if (rtt == 0) {

    rtcp_receiver_.RTT(rtcp_receiver_.RemoteSSRC(), NULL, &rtt, NULL, NULL);

  }

  rtp_sender_.OnReceivedNACK(id, nack_sequence_numbers, rtt);

}

```

我们继续跟踪流程,看下rtp_sender.cc下OnReceivedNACK()函数

```

void RTPSender::OnReceivedNACK(int64_t id,

                                const std::list<uint16_t>& nack_sequence_numbers,

                                int64_t avg_rtt) {

  TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("webrtc_rtp"),

              "RTPSender::OnReceivedNACK", "num_seqnum",

              nack_sequence_numbers.size(), "avg_rtt", avg_rtt);

  const int64_t now = clock_->TimeInMilliseconds();

  uint32_t bytes_re_sent = 0;

  uint32_t target_bitrate = GetTargetBitrate();

    //比特率限制检查

  // Enough bandwidth to send NACK?

  if (!ProcessNACKBitRate(now)) {

    LOG(LS_INFO) << "NACK bitrate reached. Skip sending NACK response. Target "

                << target_bitrate;

    return;

  }

  for (std::list<uint16_t>::const_iterator it = nack_sequence_numbers.begin();

      it != nack_sequence_numbers.end(); ++it) {

    const int32_t bytes_sent = ReSendPacket(id, *it, 5 + avg_rtt);

    if (bytes_sent > 0) {

      bytes_re_sent += bytes_sent;

    } else if (bytes_sent == 0) {

      // The packet has previously been resent.

      // Try resending next packet in the list.

      continue;

    } else {

      // Failed to send one Sequence number. Give up the rest in this nack.

      LOG(LS_WARNING) << "Failed resending RTP packet " << *it

                      << ", Discard rest of packets";

      break;

    }

    // Delay bandwidth estimate (RTT * BW).

    if (target_bitrate != 0 && avg_rtt) {

      // kbits/s * ms = bits => bits/8 = bytes

      size_t target_bytes =

          (static_cast<size_t>(target_bitrate / 1000) * avg_rtt) >> 3;

      if (bytes_re_sent > target_bytes) {

        break;  // Ignore the rest of the packets in the list.

      }

    }

  }

  if (bytes_re_sent > 0) {

    UpdateNACKBitRate(bytes_re_sent, now);

  }

}

```

我们继续看下ReSendPacket()函数重新发送数据的实现

```

int32_t RTPSender::ReSendPacket(int64_t id, uint16_t packet_id, int64_t min_resend_time) {

  size_t length = IP_PACKET_SIZE;

  uint8_t data_buffer[IP_PACKET_SIZE];

  int64_t capture_time_ms;

//从缓存包中去获取数据包

  if (!packet_history_.GetPacketAndSetSendTime(packet_id, min_resend_time, true,

                                              data_buffer, &length,

                                              &capture_time_ms)) {

    // Packet not found.

    LOG(LS_INFO) << "ReSendPacket not found.seq[" << packet_id << "].";

    return 0;

  }

//如果开启平滑发送的话

  if (paced_sender_) {

    RtpUtility::RtpHeaderParser rtp_parser(data_buffer, length);

    RTPHeader header;

    if (!rtp_parser.Parse(&header)) {

      assert(false);

      return -1;

    }

    // Convert from TickTime to Clock since capture_time_ms is based on

    // TickTime.

    int64_t corrected_capture_tims_ms = capture_time_ms + clock_delta_ms_;

    paced_sender_->InsertPacket(

        id, RtpPacketSender::kNormalPriority, header.ssrc, header.sequenceNumber,

        corrected_capture_tims_ms, length - header.headerLength, true);

    return length;

  }

  int rtx = kRtxOff;

  {

    rtc::CritScope lock(&send_critsect_);

    rtx = rtx_;

  }

  //重新发送数据

  if (!PrepareAndSendPacket(id, data_buffer, length, capture_time_ms,

                            (rtx & kRtxRetransmitted) > 0, true)) {

    return -1;

  }

  return static_cast<int32_t>(length);

}

```

3.通过上面我们可以知道,RTPSender中完成在PacketHistory中查找需要发送的RTP seq, 并决定重发时间. 重发也需要经过重发的比特率限制的检查. RTPSedner 初始化话时可以配置是否使用(PacedSend, 均匀发送), 最后检查重发格式(RtxStatus() 可以获取是否使用 RTX 封装)后使用 RTPSedner::PrepareAndSendPacket进行立即重发. 如果是使用 PacedSend, 则使用 PacedSender::InsertPacket 先加入发送列表中, 它的process会定时处理发送任务.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350