(01)Webrtc::Fec与Nack的二三事

  写在前面:要理解Fec与Nack逻辑,我喜欢先从接受端看, 理解了Fec与Nack是如何被使用的,才能更好的明白不同的机制应该怎么用,在什么场合用。

 

  更新丢包逻辑

void PacketBuffer::UpdateMissingPackets(uint16_t seq_num) {
  if (!newest_inserted_seq_num_)
    newest_inserted_seq_num_ = seq_num;

  const int kMaxPaddingAge = 1000;
  if (AheadOf(seq_num, *newest_inserted_seq_num_)) {
    uint16_t old_seq_num = seq_num - kMaxPaddingAge;
    auto erase_to = missing_packets_.lower_bound(old_seq_num);
    missing_packets_.erase(missing_packets_.begin(), erase_to);

    // Guard against inserting a large amount of missing packets if there is a
    // jump in the sequence number.
    if (AheadOf(old_seq_num, *newest_inserted_seq_num_)) {
      *newest_inserted_seq_num_ = old_seq_num;
    }

    ++*newest_inserted_seq_num_;
    while (AheadOf(seq_num, *newest_inserted_seq_num_)) {
      missing_packets_.insert(*newest_inserted_seq_num_);
      ++*newest_inserted_seq_num_;
    }
  } else {
    missing_packets_.erase(seq_num);
  }
}

  1.AheadOf(A, B)可以简单理解为A>B。实际环形数据的比较与环形存储空间size有关,相当与(A > B) xor (abs(A - B) > size / 2)。

  2.首先判断当前包序号seq_num是否在之前插入的包序号newest_inserted_seq_num之后,如果不是,证明是之前的丢包,走else逻辑。如果是,则开始判断是否有丢包。

  3.删除与当前包序号相差超过1000的丢包序号,因为太老了。如果seq_num - 1000 > newest_inserted_seq_num,证明中间丢包太多,只从seq_num - 1000开始插入丢包序号。

  4.newest_inserted_seq_num到seq_num之间的全部包被认定为丢包。

举例:收包1000, 1001, 1002, 1005,1003……收到1005时认定1003/1004丢包,收到1003时删除丢包列表中的1003,只剩1004。

注意:packet_buffer的missing_packets_在webrtc中虽然做了更新,但除了H264判断IPPP依赖是否有丢依赖帧之外,并没有被使用,所以这里其实并不影响实际的网络体验。

 

  判断收到数据包时是否可能组成一帧

bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const {
  size_t index = seq_num % buffer_.size();
  int prev_index = index > 0 ? index - 1 : buffer_.size() - 1;
  const auto& entry = buffer_[index];
  const auto& prev_entry = buffer_[prev_index];

  if (entry == nullptr)
    return false;
  if (entry->seq_num != seq_num)
    return false;
  if (entry->is_first_packet_in_frame())
    return true;
  if (prev_entry == nullptr)
    return false;
  if (prev_entry->seq_num != static_cast<uint16_t>(entry->seq_num - 1))
    return false;
  if (prev_entry->timestamp != entry->timestamp)
    return false;
  if (prev_entry->continuous)
    return true;

  return false;
}

  判断是否可能组成一个完整帧的条件有两个:1.该包为帧的地一个包。2.包连续,前一包存在且时间戳相同。可以看到,这里用于判断的是buffer_的实际数据,而非missing_packets_。buffer_在执行PacketBuffer::InsertPacket时被更新。

  注意这里的continuous,有两个位置会设置这个值:InsertPacket的时候被置为false,或是通过PotentialNewFrame检测后在FindFrames中被置为true。也就是说,一个包的continuous置为true,唯一的途径为通过PotentialNewFrame检测。于是一帧里的第二个包被置为true,依赖于收到第一个包,走is_first_packet_in_frame() == true分支,而第N个包被置为true依赖于第N-1个包被置为true。所以一个包的上一个包的continuous标记为true,就表示这个包所属帧,从第一个包到当前包都是连续的。

 

  寻找完整帧

std::vector<std::unique_ptr<PacketBuffer::Packet>> PacketBuffer::FindFrames(
    uint16_t seq_num) {
  std::vector<std::unique_ptr<PacketBuffer::Packet>> found_frames;

  for (size_t i = 0; i < buffer_.size() && PotentialNewFrame(seq_num); ++i) {
    size_t index = seq_num % buffer_.size();
    buffer_[index]->continuous = true;

    // If all packets of the frame is continuous, find the first packet of the
    // frame and add all packets of the frame to the returned packets.
    if (buffer_[index]->is_last_packet_in_frame()) {
      uint16_t start_seq_num = seq_num;

      // Find the start index by searching backward until the packet with
      // the |frame_begin| flag is set.
      int start_index = index;
      size_t tested_packets = 0;
      int64_t frame_timestamp = buffer_[start_index]->timestamp;

      // Identify H.264 keyframes by means of SPS, PPS, and IDR.
      bool is_h264 = buffer_[start_index]->codec() == kVideoCodecH264;
      bool has_h264_sps = false;
      bool has_h264_pps = false;
      bool has_h264_idr = false;
      bool is_h264_keyframe = false;
      int idr_width = -1;
      int idr_height = -1;
      while (true) {
        ++tested_packets;

        if (!is_h264 && buffer_[start_index]->is_first_packet_in_frame())
          break;

        if (is_h264) {
          // ture: RTC_LOG(LS_ERROR) << "kiki-h264";
          const auto* h264_header = absl::get_if<RTPVideoHeaderH264>(
              &buffer_[start_index]->video_header.video_type_header);
          if (!h264_header || h264_header->nalus_length >= kMaxNalusPerPacket)
            return found_frames;

          for (size_t j = 0; j < h264_header->nalus_length; ++j) {
            if (h264_header->nalus[j].type == H264::NaluType::kSps) {
              has_h264_sps = true;
            } else if (h264_header->nalus[j].type == H264::NaluType::kPps) {
              has_h264_pps = true;
            } else if (h264_header->nalus[j].type == H264::NaluType::kIdr) {
              has_h264_idr = true;
            }
          }
          if ((sps_pps_idr_is_h264_keyframe_ && has_h264_idr && has_h264_sps &&
               has_h264_pps) ||
              (!sps_pps_idr_is_h264_keyframe_ && has_h264_idr)) {
            is_h264_keyframe = true;
            // Store the resolution of key frame which is the packet with
            // smallest index and valid resolution; typically its IDR or SPS
            // packet; there may be packet preceeding this packet, IDR's
            // resolution will be applied to them.
            if (buffer_[start_index]->width() > 0 &&
                buffer_[start_index]->height() > 0) {
              idr_width = buffer_[start_index]->width();
              idr_height = buffer_[start_index]->height();
            }
          }
        }

        if (tested_packets == buffer_.size())
          break;

        start_index = start_index > 0 ? start_index - 1 : buffer_.size() - 1;

        // In the case of H264 we don't have a frame_begin bit (yes,
        // |frame_begin| might be set to true but that is a lie). So instead
        // we traverese backwards as long as we have a previous packet and
        // the timestamp of that packet is the same as this one. This may cause
        // the PacketBuffer to hand out incomplete frames.
        // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=7106
        if (is_h264 && (buffer_[start_index] == nullptr ||
                        buffer_[start_index]->timestamp != frame_timestamp)) {
          break;
        }

        --start_seq_num;
      }

      if (is_h264) {
        // Warn if this is an unsafe frame.
        if (has_h264_idr && (!has_h264_sps || !has_h264_pps)) {
          RTC_LOG(LS_WARNING)
              << "Received H.264-IDR frame "
                 "(SPS: "
              << has_h264_sps << ", PPS: " << has_h264_pps << "). Treating as "
              << (sps_pps_idr_is_h264_keyframe_ ? "delta" : "key")
              << " frame since WebRTC-SpsPpsIdrIsH264Keyframe is "
              << (sps_pps_idr_is_h264_keyframe_ ? "enabled." : "disabled");
        }

        // Now that we have decided whether to treat this frame as a key frame
        // or delta frame in the frame buffer, we update the field that
        // determines if the RtpFrameObject is a key frame or delta frame.
        const size_t first_packet_index = start_seq_num % buffer_.size();
        if (is_h264_keyframe) {
          buffer_[first_packet_index]->video_header.frame_type =
              VideoFrameType::kVideoFrameKey;
          if (idr_width > 0 && idr_height > 0) {
            // IDR frame was finalized and we have the correct resolution for
            // IDR; update first packet to have same resolution as IDR.
            buffer_[first_packet_index]->video_header.width = idr_width;
            buffer_[first_packet_index]->video_header.height = idr_height;
          }
        } else {
          buffer_[first_packet_index]->video_header.frame_type =
              VideoFrameType::kVideoFrameDelta;
        }

        // With IPPP, if this is not a keyframe, make sure there are no gaps
        // in the packet sequence numbers up until this point.
        const uint8_t h264tid =
            buffer_[start_index] != nullptr
                ? buffer_[start_index]->video_header.frame_marking.temporal_id
                : kNoTemporalIdx;
        if (h264tid == kNoTemporalIdx && !is_h264_keyframe &&
            missing_packets_.upper_bound(start_seq_num) !=
                missing_packets_.begin()) {
          return found_frames;
        }
      }

      const uint16_t end_seq_num = seq_num + 1;
      // Use uint16_t type to handle sequence number wrap around case.
      uint16_t num_packets = end_seq_num - start_seq_num;
      found_frames.reserve(found_frames.size() + num_packets);
      for (uint16_t i = start_seq_num; i != end_seq_num; ++i) {
        std::unique_ptr<Packet>& packet = buffer_[i % buffer_.size()];
        RTC_DCHECK(packet);
        RTC_DCHECK_EQ(i, packet->seq_num);
        // Ensure frame boundary flags are properly set.
        packet->video_header.is_first_packet_in_frame = (i == start_seq_num);
        packet->video_header.is_last_packet_in_frame = (i == seq_num);
        found_frames.push_back(std::move(packet));
      }

      missing_packets_.erase(missing_packets_.begin(),
                             missing_packets_.upper_bound(seq_num));
    }
    ++seq_num;
  }
  return found_frames;
}

  1.正常网络条件下的视频包,都会通过PotentialNewFrame检测,然后因为不是帧的最后一包,直接seq_num++,下一次循环entry == nullptr而退出循环。seq_num++的原因是,新插入的包可能是帧间丢失的最后一包,例如一帧由1000-1006包组成,只有1003包丢失,则收到1003包时会首先通过PotentialNewFrame检测,然后因为不是帧的最后一包,不断seq_num++,将1004/1005/1006包的continuous标志置为true,并从1006包开始组帧搜索。

  2.如果是帧的最后一包,开始反向寻找帧的第一包。对于h264,判断方式为a.前一包为空,b.前一包的时间戳不同。注释说h264的frame begin位是假的,所以不用改标志作为判断。不是很理解,实测标志位是有效且准确的。标志位通过video_rtp_depacketizer_h264的ParseFuaNalu函数,first_fragment = (rtp_payload.cdata()[1] & kSBit) > 0判断。

// In the case of H264 we don't have a frame_begin bit (yes,
// |frame_begin| might be set to true but that is a lie). So instead
// we traverese backwards as long as we have a previous packet and
// the timestamp of that packet is the same as this one. This may cause
// the PacketBuffer to hand out incomplete frames.
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=7106

  3.对于h264,如果missing_packets_.upper_bound(start_seq_num) != missing_packets_.begin()并且当前不是I帧,说明当前虽然能组成P帧,但IPPP依赖缺失,直接返回。一个问题是导致卡顿,因为当前画面本来是可以播放的,只是有可能花屏。另一个问题是导致丢帧,因为当前帧所有包已经收齐,接下来再收的包都不会触发这一帧的组帧搜索,而这一帧本身并没有被返回。(前面说的不对,FindFrames返回不止一帧,当前面的依赖帧收全后,会触发这一帧的返回)这里可以优化,即使前面P帧缺失也返回当前帧,但可能会导致部分花屏,特别对于GOP20秒且网络不好的情况,可能会一直在花屏状态

  4.从日志来看,h264收到的每个I帧都是IDR,包含SPS/PPS。

  5.nalus_length代表包里包含几帧,对于视频来说,h264为fu-a分包,即多个包组合一个帧。直到一帧的起始包,才会看到nalus_length为1,其它为0。

  6.h264tid一直是kNoTemporalIdx,不太理解这是什么标志。

  7.将可以组帧的所有包返回,并更新missing_packets_,既然已经组帧,那么seq_num之前的丢包全部不要了。这里很重要,如果没有这个,做了第3条红字部分优化,可能会导致退帧现象。std::move会清空buffer_对应位置数据。

 

  NACK

bool NackModule::RemovePacketsUntilKeyFrame() {
  while (!keyframe_list_.empty()) {
    auto it = nack_list_.lower_bound(*keyframe_list_.begin());

    if (it != nack_list_.begin()) {
      // We have found a keyframe that actually is newer than at least one
      // packet in the nack list.
      nack_list_.erase(nack_list_.begin(), it);
      return true;
    }

    // If this keyframe is so old it does not remove any packets from the list,
    // remove it from the list of keyframes and try the next keyframe.
    keyframe_list_.erase(keyframe_list_.begin());
  }
  return false;
}

void NackModule::AddPacketsToNack(uint16_t seq_num_start,
                                  uint16_t seq_num_end) {
  // 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();
      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;
  }
}

  1.nack_list_维护丢包seq_num,keyframe_list_维护已收到的I帧seq_num。

  2.当nacks数目超过kMaxNackPackets,首先不断调用RemovePacketUntilKeyFrame,指导返回结果为false或者数目不再超过kMaxNackPackets。

  3.RemovePacketUntilKeyFrame逻辑:

      a.如果nack_list_中有早于keyframe_list_最早关键帧的seq_num,删除这些包序号直到最早关键帧seq_num,返回true。

      b.如不符合条件a,删除最早关键帧seq_num,并再次进行判断。

      c.当删光keyframe_list_后返回false。

  4.如果删光keyframe_list_后,nacks数目依然超标,清空nack_list_,并请求重发关键帧。

  5.循环每一个丢包的seq_num,由seq_num生成NackInfo信息,插入nack_list_中。注意因为在NACK的OnReceivedPacket函数中,写死了”bool is_retransmitted = true;“,不会调用UpdateReorderingStatistics,所以WaitNumberOfPackets一直返回0。NackInfo的seq_num与send_at_seq_num均为丢包的seq_num。

  6.可以看到,在逻辑中,如果包已经被fec恢复,就不会加入nack_list_列表中。但因为ulpfec传输视频包与fec包的顺序,大部分情况下丢包检测先于fec包到达,这里实际没什么作用。

 

  收包的NACK回调

int NackModule::OnReceivedPacket(uint16_t seq_num,
                                 bool is_keyframe,
                                 bool is_recovered) {
  rtc::CritScope lock(&crit_);
  // 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;
}

  1.当新收到的seq_num存在于nack_list_中,表明这个包已经收到,不需要重传,从nack_list_中移除。返回这个包被重传次数。

  2.is_keyframe表示收到的包是否输入关键帧,is_recovered表示收到的包是否是从fec恢复回来的,分别处理keyframe_list_及recovered_list_。如果是恢复包,不触发丢包更新直接返回。

  3.AddPacketsToNack更新丢包列表。

std::vector<uint16_t> NackModule::GetNackBatch(NackFilterOptions options) {
  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;
}

  4.GetNackBatch函数对于nack_list_中满足一定条件的包,将其加入nack_batch一并返回,如果重传次数过多,从nack_list_中移除。这个函数与SendNack有两种被触发的机制,一是每次nack module收到包的回调(NackModule::OnReceivedPacket),模式为kSeqNumOnly,二是nack module轮询,每隔20ms一次(NackModule::Process),模式为kTimeOnly。值得注意的是,NackInfo的sent_at_time并不是-1,所以轮询方式只会根据delay_timed_out成立执行逻辑。限制条件的目的大概是为了防止前一个nack重传发送端还没处理完就又发了新的,同一个seq_num明明发一次就可以解决但被发了多次。

  5.SendNack调用:RtpVideoStreamReceiver::RtcpFeedbackBuffer::SendNack。这个函数首先将batch中的seq_num加入nack_sequence_numbers_中,然后根据buffering_allowed决定是否发送nack包。所以在回调中,只会做插入操作而不会发送nack包,只有在轮询中才会实际发送nack的rtcp请求。这里也可以优化,将OnReceivedPacket中的buffering_allowed改为true,以保证重传/组帧更快的发生,降低延迟。SendBufferedRtcpFeedback -> VideoReceiveStream::SendNack -> RtpVideoStreamReceiver::RequestPacketRetransmit -> ModuleRtpRtcpImpl::SendNack发送对应的rtcp包。

  6.补充:rtp_video_stream_receiver.cc初始化rtcp_feedback_buffer_的KeyFrameRequestSender及LossNotificationSender为自己,而NackSender为video_receive_stream.cc的VideoReceiveStream类。绕的要命,没理解这个设计模式。

 

  FEC

  1.fec维护两个重要队列,recovered_packets及received_fec_packets_。recovered_packets里保存收到的视频包,以及被fec恢复的包。received_fec_packets_里保存收到的fec包,每个fec包还包含指向所保护数据包的指针。

void ForwardErrorCorrection::DecodeFec(const ReceivedPacket& received_packet,
                                       RecoveredPacketList* recovered_packets) {
  RTC_DCHECK(recovered_packets);

  const size_t max_media_packets = fec_header_reader_->MaxMediaPackets();
  if (recovered_packets->size() == max_media_packets) {
    const RecoveredPacket* back_recovered_packet =
        recovered_packets->back().get();

    if (received_packet.ssrc == back_recovered_packet->ssrc) {
      const unsigned int seq_num_diff =
          MinDiff(received_packet.seq_num, back_recovered_packet->seq_num);
      if (seq_num_diff > max_media_packets) {
        // A big gap in sequence numbers. The old recovered packets
        // are now useless, so it's safe to do a reset.
        RTC_LOG(LS_INFO) << "Big gap in media/ULPFEC sequence numbers. No need "
                            "to keep the old packets in the FEC buffers, thus "
                            "resetting them.";
        ResetState(recovered_packets);
      }
    }
  }

  InsertPacket(received_packet, recovered_packets);
  AttemptRecovery(recovered_packets);
}

  2.首先做一些过期检测,如果当前的包的seq_num与最后一个收到或恢复的视频包的seq_num差距超过了fec所保护的数据包数量上限,说明之前的包无论如何不可能被恢复了,清空recovered_packets及received_fec_packets_。

void ForwardErrorCorrection::InsertPacket(
    const ReceivedPacket& received_packet,
    RecoveredPacketList* recovered_packets) {

  if (!received_fec_packets_.empty() &&
      received_packet.ssrc == received_fec_packets_.front()->ssrc) {

    auto it = received_fec_packets_.begin();
    while (it != received_fec_packets_.end()) {
      uint16_t seq_num_diff = MinDiff(received_packet.seq_num, (*it)->seq_num);
      if (seq_num_diff > 0x3fff) {
        it = received_fec_packets_.erase(it);
      } else {
        // No need to keep iterating, since |received_fec_packets_| is sorted.
        break;
      }
    }
  }

  if (received_packet.is_fec) {
    InsertFecPacket(*recovered_packets, received_packet);
  } else {
    InsertMediaPacket(recovered_packets, received_packet);
  }

  DiscardOldRecoveredPackets(recovered_packets);
}

  3.对于fec包,a.通过mask解析保护的所有视频包序列号。b.如果序列号存在于recovered_packets中,将对应的指针指向数据包。c.将fec包放入received_fec_packets_。

  4.对于视频包,a.首先放入recovered_packet。b.对于received_fec_packets_中每一个fec包,如果保护的数据包包含该视频包,更新指针。

  5.丢弃用不到的数据包:例如fec最多只保护48个包(看协议,应该最多只能保护24个包,不知道这里为什么设置为48,long mask是1:48bits mask),而recovered_packets中包数超过这个阈值,多出来的显然不可能被用到。

void ForwardErrorCorrection::AttemptRecovery(
    RecoveredPacketList* recovered_packets) {
  auto fec_packet_it = received_fec_packets_.begin();
  while (fec_packet_it != received_fec_packets_.end()) {
    int packets_missing = NumCoveredPacketsMissing(**fec_packet_it);
    if (packets_missing == 1) {
      std::unique_ptr<RecoveredPacket> recovered_packet(new RecoveredPacket());
      recovered_packet->pkt = nullptr;
      if (!RecoverPacket(**fec_packet_it, recovered_packet.get())) {
        fec_packet_it = received_fec_packets_.erase(fec_packet_it);
        continue;
      }
      auto* recovered_packet_ptr = recovered_packet.get();
      recovered_packets->push_back(std::move(recovered_packet));
      recovered_packets->sort(SortablePacket::LessThan());
      UpdateCoveringFecPackets(*recovered_packet_ptr);
      DiscardOldRecoveredPackets(recovered_packets);
      fec_packet_it = received_fec_packets_.erase(fec_packet_it);
      fec_packet_it = received_fec_packets_.begin();
    } else if (packets_missing == 0) {
      fec_packet_it = received_fec_packets_.erase(fec_packet_it);
    } else {
      fec_packet_it++;
    }
  }
}

  6.对于received_fec_packets_中的每一个fec包,首先确认其所保护的包里有多少丢包,判断方式即查看每一个保护包的指针是否为空。通过xor方式只能恢复一个丢包,例如:a xor b xor c = fec -> a = b xor c xor fec。所以如果丢包数超过1个,无法恢复丢包,如果丢包数为0,则fec所保护的所有包均已收到或恢复,fec包不再有意义,丢弃。只有丢包数为1时,通过xor恢复数据包,并将恢复的包,放入recovered_packets中。

 

  FEC解包逻辑

  1.RED包会调用两次ReceivePacket,第一次解析剥离RED头。

void RtpVideoStreamReceiver::NotifyReceiverOfEmptyPacket(uint16_t seq_num) {
  {
    rtc::CritScope lock(&reference_finder_lock_);
    reference_finder_->PaddingReceived(seq_num);
  }
  OnInsertedPacket(packet_buffer_.InsertPadding(seq_num));
  if (nack_module_) {
    nack_module_->OnReceivedPacket(seq_num, /* is_keyframe = */ false,
                                   /* is _recovered = */ false);
  }
  if (loss_notification_controller_) {
    // TODO(bugs.webrtc.org/10336): Handle empty packets.
    RTC_LOG(LS_WARNING)
        << "LossNotificationController does not expect empty packets.";
  }
}

PacketBuffer::InsertResult PacketBuffer::InsertPadding(uint16_t seq_num) {
  PacketBuffer::InsertResult result;
  rtc::CritScope lock(&crit_);
  UpdateMissingPackets(seq_num);
  result.packets = FindFrames(static_cast<uint16_t>(seq_num + 1));
  return result;
}

  2.Fec包会触发NotifyReceiverOfEmptyPacket函数。代码注释给的解释是“Notify video_receiver about received FEC packets to avoid NACKing these packets.”。

其中InsertPadding主要做几件事:更新missing_packets_,以及看有没有可能组帧。注意这里传入FindFrames里的是seq_num+1,无论后一个包是新一帧的起始包还是另一个fec包,都不可能找到完整帧(如果是新一帧并且收到全部包,则早已返回,这里PotentialNewFrame也只会entry == nullptr),不是很理解实际意义。OnInsertedPacket会调用AssembleFrame,将属于同一帧的所有包组成这里results.packets为空,当然也不会有操作。这里还有一件重要的事,调用RtpFrameReferenceFinder::PaddingReceived,这个RtpFrameReferenceFinder是用于判断帧依赖的,并且在引入fec后,对于h264的依赖判断有问题,后面仔细分析一下。另外调用nack的收包回调,更新丢包列表。从这里看,padding包/fec包与普通包在nack模块看来,都是一样的,所以fec包丢失也会重传。

  Note: 基于这个原因,Ulpfec一定程度会恶化网页版的体验,因为无法在接收端做对应优化。解决办法是使用独立的传输通路与独立的seq_num,即flexfec。

bool UlpfecReceiverImpl::AddReceivedRedPacket(
    const RtpPacketReceived& rtp_packet,
    uint8_t ulpfec_payload_type) {
  if (rtp_packet.Ssrc() != ssrc_) {
    RTC_LOG(LS_WARNING)
        << "Received RED packet with different SSRC than expected; dropping.";
    return false;
  }
  if (rtp_packet.size() > IP_PACKET_SIZE) {
    RTC_LOG(LS_WARNING) << "Received RED packet with length exceeds maximum IP "
                           "packet size; dropping.";
    return false;
  }
  rtc::CritScope cs(&crit_sect_);

  static constexpr uint8_t kRedHeaderLength = 1;

  if (rtp_packet.payload_size() == 0) {
    RTC_LOG(LS_WARNING) << "Corrupt/truncated FEC packet.";
    return false;
  }

  // Remove RED header of incoming packet and store as a virtual RTP packet.
  auto received_packet =
      std::make_unique<ForwardErrorCorrection::ReceivedPacket>();
  received_packet->pkt = new ForwardErrorCorrection::Packet();

  // Get payload type from RED header and sequence number from RTP header.
  uint8_t payload_type = rtp_packet.payload()[0] & 0x7f;
  received_packet->is_fec = payload_type == ulpfec_payload_type;
  received_packet->is_recovered = rtp_packet.recovered();
  received_packet->ssrc = rtp_packet.Ssrc();
  received_packet->seq_num = rtp_packet.SequenceNumber();

  // RTC_LOG(LS_ERROR) << "kiki-fec/packet " << received_packet->seq_num << " is fec/recovered? " << received_packet->is_fec << " " << received_packet->is_recovered;

  if (rtp_packet.payload()[0] & 0x80) {
    // f bit set in RED header, i.e. there are more than one RED header blocks.
    // WebRTC never generates multiple blocks in a RED packet for FEC.
    RTC_LOG(LS_WARNING) << "More than 1 block in RED packet is not supported.";
    return false;
  }

  ++packet_counter_.num_packets;
  packet_counter_.num_bytes += rtp_packet.size();
  if (packet_counter_.first_packet_time_ms == -1) {
    packet_counter_.first_packet_time_ms = rtc::TimeMillis();
  }

  if (received_packet->is_fec) {
    ++packet_counter_.num_fec_packets;
    // everything behind the RED header
    received_packet->pkt->data =
        rtp_packet.Buffer().Slice(rtp_packet.headers_size() + kRedHeaderLength,
                                  rtp_packet.payload_size() - kRedHeaderLength);
  } else {
    auto red_payload = rtp_packet.payload().subview(kRedHeaderLength);
    received_packet->pkt->data.EnsureCapacity(rtp_packet.headers_size() +
                                              red_payload.size());
    // Copy RTP header.
    received_packet->pkt->data.SetData(rtp_packet.data(),
                                       rtp_packet.headers_size());
    // Set payload type.
    received_packet->pkt->data[1] &= 0x80;          // Reset RED payload type.
    received_packet->pkt->data[1] += payload_type;  // Set media payload type.
    // Copy payload data.
    received_packet->pkt->data.AppendData(red_payload.data(),
                                          red_payload.size());
  }

  if (received_packet->pkt->data.size() > 0) {
    received_packets_.push_back(std::move(received_packet));
  }
  return true;
}

  3.AddReceivedRedPacket函数处理掉RED头,然后将解RED后的包放入received_packets_队列中。

int32_t UlpfecReceiverImpl::ProcessReceivedFec() {
  crit_sect_.Enter();

  // If we iterate over |received_packets_| and it contains a packet that cause
  // us to recurse back to this function (for example a RED packet encapsulating
  // a RED packet), then we will recurse forever. To avoid this we swap
  // |received_packets_| with an empty vector so that the next recursive call
  // wont iterate over the same packet again. This also solves the problem of
  // not modifying the vector we are currently iterating over (packets are added
  // in AddReceivedRedPacket).
  std::vector<std::unique_ptr<ForwardErrorCorrection::ReceivedPacket>>
      received_packets;
  received_packets.swap(received_packets_);
  // RTC_LOG(LS_ERROR) << "kiki-fec/size " << received_packets.size();

  for (const auto& received_packet : received_packets) {
    // Send received media packet to VCM.
    if (!received_packet->is_fec) {
      ForwardErrorCorrection::Packet* packet = received_packet->pkt;
      crit_sect_.Leave();
      recovered_packet_callback_->OnRecoveredPacket(packet->data.data(),
                                                    packet->data.size());
      crit_sect_.Enter();
      // Create a packet with the buffer to modify it.
      RtpPacketReceived rtp_packet;
      const uint8_t* const original_data = packet->data.cdata();
      if (!rtp_packet.Parse(packet->data)) {
        RTC_LOG(LS_WARNING) << "Corrupted media packet";
      } else {
        rtp_packet.IdentifyExtensions(extensions_);
        // Reset buffer reference, so zeroing would work on a buffer with a
        // single reference.
        packet->data = rtc::CopyOnWriteBuffer(0);
        rtp_packet.ZeroMutableExtensions();
        packet->data = rtp_packet.Buffer();
        // Ensure that zeroing of extensions was done in place.
        RTC_DCHECK_EQ(packet->data.cdata(), original_data);
      }
    }
    if (!received_packet->is_recovered) {
      // Do not pass recovered packets to FEC. Recovered packet might have
      // different set of the RTP header extensions and thus different byte
      // representation than the original packet, That will corrupt
      // FEC calculation.
      fec_->DecodeFec(*received_packet, &recovered_packets_);
    }
  }

  // Send any recovered media packets to VCM.
  for (const auto& recovered_packet : recovered_packets_) {
    // RTC_LOG(LS_ERROR) << "kiki-fec/recovered: " << recovered_packet->seq_num;
    if (recovered_packet->returned) {
      // Already sent to the VCM and the jitter buffer.
      continue;
    }
    ForwardErrorCorrection::Packet* packet = recovered_packet->pkt;
    ++packet_counter_.num_recovered_packets;
    // Set this flag first; in case the recovered packet carries a RED
    // header, OnRecoveredPacket will recurse back here.
    recovered_packet->returned = true;
    crit_sect_.Leave();
    recovered_packet_callback_->OnRecoveredPacket(packet->data.data(),
                                                  packet->data.size());
    crit_sect_.Enter();
  }

  crit_sect_.Leave();
  return 0;
}

  4.ProcessReceivedFec首先对每个处于received_packets_队列中的包进行处理(从代码来看,因为received_packets_只有在前述AddReceivedRedPacket会做插入操作,而前述函数每次只插入一个包,故这里实际每次只处理一个包)。对于视频包,会先调第二次的ReceivePacket,并最终调用OnReceivedPayloadData,以保证视频帧的尽快渲染。接着对于所有包(无论视频包还是fec包的is_recovered都是false),会做前述fec处理逻辑,尝试恢复丢包。最后循环recovered_packets_队列,如果有returned为false的包,说明是被fec恢复的丢包,如普通视频包一样调用第二次ReceivePacket。

void RtpVideoStreamReceiver::OnRecoveredPacket(const uint8_t* rtp_packet,
                                               size_t rtp_packet_length) {
  RtpPacketReceived packet;
  if (!packet.Parse(rtp_packet, rtp_packet_length))
    return;
  if (packet.PayloadType() == config_.rtp.red_payload_type) {
    RTC_LOG(LS_WARNING) << "Discarding recovered packet with RED encapsulation";
    return;
  }

  packet.IdentifyExtensions(rtp_header_extensions_);
  packet.set_payload_type_frequency(kVideoPayloadTypeFrequency);
  // TODO(nisse): UlpfecReceiverImpl::ProcessReceivedFec passes both
  // original (decapsulated) media packets and recovered packets to
  // this callback. We need a way to distinguish, for setting
  // packet.recovered() correctly. Ideally, move RED decapsulation out
  // of the Ulpfec implementation.

  ReceivePacket(packet);
}

  5.如代码注释所写,包的recover标志位并没有被正确设置,即使是被fec恢复的包,is_recovered依然是false。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值