webrtc代码走读十五(X264编码FEC弱网效果差问题解析)

一、问题描述

1)预置条件

1、配置webrtc抗丢包方法为FEC Only。

     启动配置的时候,可以选NACK Only 或FEC Only 或NACK、FEC混合模式。

     为了测试webrtc的X264 FEC抗丢包性能,在启动配置的时候,选择FEC Only模式。

     配置方法请参考:VCMLossProtectionLogic::SetMethod

2、使用Network Emulator Client配置网络丢包模型为随机丢包,随机丢包概率为5%

2)测试用例

1、建立X264编码视频连接,查看视频清晰度及流畅性。

2、建立VP8编码视频连接,查看视频清晰度及流畅性。

3)预期效果

视频无长时间卡顿,至少X264编解码效果要与VP8一致。

4)实际效果

X264卡顿比VP8要卡顿很多。根据日志统计,

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第1张图片

VP8解码前连续600帧观察,最长掉帧61帧,连续两次卡顿最短时间是18帧。

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第2张图片

H264解码前连续600帧观察,最长掉帧86帧,根据时序图可以看出,只要启动丢包,X264会密集掉帧,效果比VP8要差很多。

二、问题分析

1)概述

视频数据从网卡收包到解码前,经过如下三方面的判断、处理:

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第3张图片

1、RTP包连续性判断。这部分根据收到报文查看是否有丢包,若存在就尝试解析FEC报文,查看是否可以恢复。

2、视频帧完整性判断。这部分可以参考《webrtc QOS方法六(花屏问题解决方法)》,判断一帧frame_begin、frame_end是否存在,在frame_begin、frame_end之间报文的序列号是否连续,若有一样不满足,这个帧就不是完整帧,该帧需要丢弃。

3、视频帧间关系判断。视频帧间有相互参考关系,若P帧前面的I帧丢失,即便有完整的P帧,也无法解码。RtpFrameReferenceFinder::ManageFrameVp8、RtpFrameReferenceFinder::ManageFrameGeneric函数就是判断帧间的参考关系的。

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第4张图片

2)视频帧间关系判断前帧统计

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第5张图片

 

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第6张图片

上两个图可以看出,在判断帧间参考关系前就不一样。

 

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第7张图片

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第8张图片

两种模型下,丢包趋势相差不大。目前就只能排查帧完整性判断这块代码,处理有什么不一样。

PacketBuffer::FindFrames函数中X264和VP8处理差异还真比较大。

1、VP8的判断比较简单,只要有frame_begin、frame_end标志位,并且frame_begin、frame_end之间报文连续就认为是完整的一帧。

2、X264的判断在VP8的基础上又增加了IDR NAL、时间戳、序列号连续性判断。

std::vector> PacketBuffer::FindFrames(
    uint16_t seq_num) {
  std::vector> found_frames;
  for (size_t i = 0; i < size_ && PotentialNewFrame(seq_num); ++i) {
    size_t index = seq_num % size_;
    sequence_buffer_[index].continuous = true;

    // If all packets of the frame is continuous, find the first packet of the
    // frame and create an RtpFrameObject.
    if (sequence_buffer_[index].frame_end) {
      size_t frame_size = 0;
      int max_nack_count = -1;
      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;

      bool is_h264 = data_buffer_[start_index].codec == kVideoCodecH264;
      bool is_h264_keyframe = false;
      int64_t frame_timestamp = data_buffer_[start_index].timestamp;

      while (true) {
        ++tested_packets;
        frame_size += data_buffer_[start_index].sizeBytes;
        max_nack_count =
            std::max(max_nack_count, data_buffer_[start_index].timesNacked);
        sequence_buffer_[start_index].frame_created = true;

        if (!is_h264 && sequence_buffer_[start_index].frame_begin)
          break;

        if (is_h264 && !is_h264_keyframe) {
          const RTPVideoHeaderH264& header =
              data_buffer_[start_index].video_header.codecHeader.H264;
          for (size_t i = 0; i < header.nalus_length; ++i) {
            if (header.nalus[i].type == H264::NaluType::kIdr) {
              is_h264_keyframe = true;
              break;
            }
          }
        }

        if (tested_packets == size_)
          break;

        start_index = start_index > 0 ? start_index - 1 : 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 &&
            (!sequence_buffer_[start_index].used ||
             data_buffer_[start_index].timestamp != frame_timestamp)) {
          break;
        }

        --start_seq_num;
      }

      // If this is H264 but not a keyframe, make sure there are no gaps in the
      // packet sequence numbers up until this point.
      if (is_h264 && !is_h264_keyframe &&
          missing_packets_.upper_bound(start_seq_num) !=
              missing_packets_.begin()) {
        uint16_t stop_index = (index + 1) % size_;
        while (start_index != stop_index) {
          sequence_buffer_[start_index].frame_created = false;
          start_index = (start_index + 1) % size_;
        }

        return found_frames;
      }

      missing_packets_.erase(missing_packets_.begin(),
                             missing_packets_.upper_bound(seq_num));

      found_frames.emplace_back(
          new RtpFrameObject(this, start_seq_num, seq_num, frame_size,
                             max_nack_count, clock_->TimeInMilliseconds()));
    }
    ++seq_num;
  }
  return found_frames;
}

分析这个函数,发现这段代码有问题

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第9张图片

这个函数设计思想是判断帧的完整性,上面这个判断,实际上相当于增加了帧间参考关系的判断。帧间参考关系应该在RtpFrameReferenceFinder::ManageFrameInternal函数判断的。那边判断检测长期没有可解码帧,还发送I帧请求进一步进行保护。这里直接给丢了,没有任何其他保护措施。

webrtc代码走读十五(X264编码FEC弱网效果差问题解析)_第10张图片

明显没有之前那么密集掉帧。

三、结论

PacketBuffer::FindFrames函数针对264编码又增加帧间参考关系的判断,并且没有请求I帧,导致视频效果弱于VP8。屏蔽这段代码,弱网下,视频流畅性与VP8持平。

webrtc之所以这么做是因为VP8的RTP协议里面有start标志位,H264没有。H264的start标志位是根据帧间时间戳变化来判断的,若帧首包丢失,webrtc判断不出来这是否是完整帧。

 

你可能感兴趣的:(webrtc,webrtc代码走读)