WebRtc Video Receiver(五)-设置参考帧

时间:2021-7-4 作者:qvyue

1)前言

  • 经过前面4篇文章的分析,针对WebRtc视频接收模块从创建接收模块、到对RTP流接收处理、关键帧请求的时机、丢包判断以及丢包重传、frame组帧等已经有了一定的概念和认识。
  • 基于以上本文分析rtp包组包后聚合帧发送给解码器前的处理流程,在将一帧完整的帧发送给解码模块之前需要进行一定的预处理,如需要查找参考帧,本文着重分析解码前的参考帧查找原理。
  • 承接上文的分析,rtp包组包成功后会将一帧完整的数据帧投递到RtpVideoStreamReceiver2模块由其OnAssembledFrame函数来进行接收处理。
  • 其实现如下:
void RtpVideoStreamReceiver2::OnAssembledFrame(
    std::unique_ptr frame) {
  RTC_DCHECK_RUN_ON(&worker_task_checker_);
  RTC_DCHECK(frame);
  .....
  //该模块默认未开启,新特性值得研究,顾名思义为丢包通知控制模块    
  // 可通过WebRTC-RtcpLossNotification/Enable开启,但是默认只支持VP8
  // SDP需要实现goog-lntf feedback    
  if (loss_notification_controller_ && descriptor) {
    loss_notification_controller_->OnAssembledFrame(
        frame->first_seq_num(), descriptor->frame_id,
        absl::c_linear_search(descriptor->decode_target_indications,
                              DecodeTargetIndication::kDiscardable),
        descriptor->dependencies);
  }         
  // If frames arrive before a key frame, they would not be decodable.
  // In that case, request a key frame ASAP.
  if (!has_received_frame_) {
    if (frame->FrameType() != VideoFrameType::kVideoFrameKey) {
      // |loss_notification_controller_|, if present, would have already
      // requested a key frame when the first packet for the non-key frame
      // had arrived, so no need to replicate the request.
      if (!loss_notification_controller_) {
        RequestKeyFrame();
      }
    }
    has_received_frame_ = true;
  }

  // Reset |reference_finder_| if |frame| is new and the codec have changed.
  if (current_codec_) {
    //每帧之间的时间戳不一样,当前帧的时间戳大于前一帧的时间戳(未环绕的情况下)  
    bool frame_is_newer =
        AheadOf(frame->Timestamp(), last_assembled_frame_rtp_timestamp_);

    if (frame->codec_type() != current_codec_) {
      if (frame_is_newer) {
        // When we reset the |reference_finder_| we don't want new picture ids
        // to overlap with old picture ids. To ensure that doesn't happen we
        // start from the |last_completed_picture_id_| and add an offset in case
        // of reordering.
        reference_finder_ =
            std::make_unique(
                this, last_completed_picture_id_ +
                          std::numeric_limits::max());
        current_codec_ = frame->codec_type();
      } else {
        // Old frame from before the codec switch, discard it.
        return;
      }
    }

    if (frame_is_newer) {
      last_assembled_frame_rtp_timestamp_ = frame->Timestamp();
    }
  } else {
    current_codec_ = frame->codec_type();
    last_assembled_frame_rtp_timestamp_ = frame->Timestamp();
  }

  if (buffered_frame_decryptor_ != nullptr) {
    buffered_frame_decryptor_->ManageEncryptedFrame(std::move(frame));
  } else if (frame_transformer_delegate_) {
    frame_transformer_delegate_->TransformFrame(std::move(frame));
  } else {
    reference_finder_->ManageFrame(std::move(frame));
  }
}
  • 首先该函数第一次接收到一帧数据的时候,需要判断是否是在关键帧之前收到,如果在未收到关键帧之前收到的话是不能解码的,所以此时需要发送关键帧请求使用RequestKeyFrame()函数发送关键帧请求。
  • 其次、根据不同帧之间的时间戳不一样的原则,判断是否为新的一帧,首次接收到一帧之后会实例化reference_finder_成员,后续对参考帧的查找处理在未加密的情况下,都基于该实例完成。
  • 如果为新的一帧,每帧数据查找参考帧后都会更新last_assembled_frame_rtp_timestamp_
  • 最后调用根据是否加密选择reference_finder_或者buffered_frame_decryptor_对视频帧调用ManageFrame或者ManageEncryptedFrame函数进行参考帧查找处理。
  • 本文的核心就是分析ManageFrame函数。

2)ManageFrame工作流程

  • 在分析该函数之前先了解RtpFrameReferenceFinderRtpVideoStreamReceiver2OnCompleteFrameCallback之间的关系。

    WebRtc Video Receiver(五)-设置参考帧
    WebRtc_Video_Stream_Receiver_05_01.png
  • 根据上图的关系图,在RtpFrameReferenceFinder模块中对video_coding::RtpFrameObject数据帧进行处理,如果处理成功最终会生成video_coding::EncodedFrame视频帧,接着回调OnCompleteFrameCallbackOnCompleteFrame函数将视频帧返回到RtpVideoStreamReceiver2模块。

  • ManageFrame()函数的代码如下:

void RtpFrameReferenceFinder::ManageFrame(
    std::unique_ptr frame) {
  // If we have cleared past this frame, drop it.
  if (cleared_to_seq_num_ != -1 &&
      AheadOf(cleared_to_seq_num_, frame->first_seq_num())) {
    return;
  }
  
  FrameDecision decision = ManageFrameInternal(frame.get());

  switch (decision) {
    case kStash:
      if (stashed_frames_.size() > kMaxStashedFrames)
        stashed_frames_.pop_back();
      stashed_frames_.push_front(std::move(frame));
      break;
    case kHandOff:
      HandOffFrame(std::move(frame));
      RetryStashedFrames();
      break;
    case kDrop:
      break;
  }
}
  • cleared_to_seq_num_变量记录的是已经清除的seq,比如说如果一帧数据已经发送到解码模块,或解码完成,那么需要将对应的seq进行清除,在这里的作用就是判断当前待解码的数据帧的首个包的seq和cleared_to_seq_num_大小进行比较,在未环绕的情况下,如果cleared_to_seq_num_大于frame->first_seq_num()则说明该帧数据之前的帧已经解码了,此帧应该放弃解码,所以直接返回。
  • cleared_to_seq_num_变量的更新通过调用ClearTo(uint16_t seq_num)函数来更新,调用流程后续会分析到。
  • 调用ManageFrameInternal函数对当前帧进行决策处理,结果返回三种,kStash表示当前帧解码时机未到需要存储、kHandOff可以解码、kDrop表示放弃该帧。
  • 对于可以解码的决策直接调用HandOffFrame函数进行后处理,而kStash的决策使用stashed_frames_容器将当前帧插入到容器头部,该容器的最大容量为100。
  • ManageFrameInternal函数的实现如下:
RtpFrameReferenceFinder::FrameDecision
RtpFrameReferenceFinder::ManageFrameInternal(RtpFrameObject* frame) {
  ........
  switch (frame->codec_type()) {
    case kVideoCodecVP8:
      return ManageFrameVp8(frame);
    case kVideoCodecVP9:
      return ManageFrameVp9(frame);
    case kVideoCodecGeneric:
      if (auto* generic_header = absl::get_if(
              &frame->GetRtpVideoHeader().video_type_header)) {
        return ManageFramePidOrSeqNum(frame, generic_header->picture_id);
      }
      ABSL_FALLTHROUGH_INTENDED;
    default:
      return ManageFramePidOrSeqNum(frame, kNoPictureId);
  }
}
  • 该函数根据当前帧数据的codec类型使用不同的实现来对当前帧进行决策,本文以H264为例进行分析讨论。
  • ManageFrameH264函数分成两部分,一部分可以理解成对方是使用硬编编码出来的数据,此时tid=0xff,这种情况把任务交给了ManageFramePidOrSeqNum函数。
  • 另一种情况针对openh264软编的数据此时tid不为0xff。
  • 首先对tid=0xff的情况进行分析。
  • 如果要支持H265的话需要在这里新增对H265视频帧的决策处理函数。

3)ManageFramePidOrSeqNum设置参考帧

RtpFrameReferenceFinder::FrameDecision RtpFrameReferenceFinder::ManageFrameH264(
    RtpFrameObject* frame) {
  const FrameMarking& rtp_frame_marking = frame->GetFrameMarking();

  uint8_t tid = rtp_frame_marking.temporal_id;
  bool blSync = rtp_frame_marking.base_layer_sync;
  /*android 硬编的情况收到的tid位0xff,传入的kNoPictureId=-1,这是h264的特性*/ 
  if (tid == kNoTemporalIdx)
    return ManageFramePidOrSeqNum(std::move(frame), kNoPictureId);
  ....  
}
  • 根据tid=0xff,直接调用ManageFramePidOrSeqNum对当前帧进行参考帧查找处理。

  • 在分析ManageFramePidOrSeqNum()函数之前首先介绍编码数据gop的概念。

    WebRtc Video Receiver(五)-设置参考帧
    WebRtc_Video_Stream_Receiver_05_02.png
  • 以上以h264为例,在H264数据中idr帧可以单独解码,而P帧需要前向参考,在一个GOP内的帧都需要前向参考帧才能顺利解码。

  • RtpFrameReferenceFinder通过last_seq_num_gop_容器来维护最近的GOP表,收到P帧后,RtpFrameReferenceFinder需要找到P帧所属的GOP,将P帧的参考帧设置为GOP内该帧的上一帧,之后传递给FrameBuffer模块。

    WebRtc Video Receiver(五)-设置参考帧
    WebRtc_Video_Stream_Receiver_05_03.png
  • 该容器是以当前待解码的帧所属的gop(由于IDR关键帧是gop的开始)关键帧的最后一个包的seq位key,以当前帧最后一个包的seq组成的std::pair为value的容器(当前帧也有可能是padding包。

  • 下面开始分析ManageFramePidOrSeqNum()函数原理如下

RtpFrameReferenceFinder::FrameDecision
RtpFrameReferenceFinder::ManageFramePidOrSeqNum(RtpFrameObject* frame,
                                                int picture_id) {
  // If |picture_id| is specified then we use that to set the frame references,
  // otherwise we use sequence number.
  // 1)确保非h264帧gop内维护的帧的连续性  
  if (picture_id != kNoPictureId) {
    frame->id.picture_id = unwrapper_.Unwrap(picture_id);
    frame->num_references =
        frame->frame_type() == VideoFrameType::kVideoFrameKey ? 0 : 1;
    frame->references[0] = frame->id.picture_id - 1;
    return kHandOff;
  }
    
  //2)判断是否为关键帧,其中frame_type在组帧的时候进行设置的
  if (frame->frame_type() == VideoFrameType::kVideoFrameKey) {
    last_seq_num_gop_.insert(std::make_pair(
        frame->last_seq_num(),//当前gop最后一个包的seq为key
        std::make_pair(frame->last_seq_num(), frame->last_seq_num())));
  }
  //3)如果到此为止还没有收到一帧关键帧,则存储该帧
  // We have received a frame but not yet a keyframe, stash this frame.
  if (last_seq_num_gop_.empty()) 
    return kStash;
    
  // Clean up info for old keyframes but make sure to keep info
  // for the last keyframe.
  // 4)清除老的gop frame->last_seq_num() - 100之前的所有都清除掉,但至少确保有一个。
  auto clean_to = last_seq_num_gop_.lower_bound(frame->last_seq_num() - 100);
  for (auto it = last_seq_num_gop_.begin();
       it != clean_to && last_seq_num_gop_.size() > 1;) {
    it = last_seq_num_gop_.erase(it);
  }
    
  // Find the last sequence number of the last frame for the keyframe
  // that this frame indirectly references.
  // 函数能走到这一步,gop 容器中是一定有存值的  
  //5.1) 如果关键帧的序号是大于该帧的序号的(未环绕的情况),那么该帧需要丢弃掉。  
  // 假设last_seq_num_gop_中存的是34号包,而本次来的帧的序号是10~16(非关键帧)。
  //5.2) 还有一种情况假设当前帧就是关键帧frame->last_seq_num()=34,假设事先last_seq_num_gop_存的是56号seq,由last_seq_num_gop_定义的排序规则,34号包被插入的时候会在头部,最终下面的条件依然成立。  
  auto seq_num_it = last_seq_num_gop_.upper_bound(frame->last_seq_num());
  if (seq_num_it == last_seq_num_gop_.begin()) {
    RTC_LOG(LS_WARNING) first_seq_num() last_seq_num()
                        second.first;
  // last_picture_id_with_padding_gop得到的也是上一帧的最后一个包的seq。
  // 当前GOP的最新包的序列号,可能是last_picture_id_gop, 也可能是填充包.  
  uint16_t last_picture_id_with_padding_gop = seq_num_it->second.second;
  // 非关键帧判断seq连续性,  
  if (frame->frame_type() == VideoFrameType::kVideoFrameDelta) {
    //得到上一帧最后一个包的seq,当前帧的第一个包的seq -1 得到上一帧的最后一个seq  
    uint16_t prev_seq_num = frame->first_seq_num() - 1;
    // 如果不相等说明不连续,如果正常未丢包的情况下是一定会相等的。  
    if (prev_seq_num != last_picture_id_with_padding_gop)
      return kStash;
  }
  //检查当前帧最后一个seq是否大于所属gop 关键帧的最后一个seq
  RTC_DCHECK(AheadOrAt(frame->last_seq_num(), seq_num_it->first));

  // Since keyframes can cause reordering we can't simply assign the
  // picture id according to some incrementing counter.
  //7) 给RtpFrameObject的id.picture_id赋值
  // 如果为关键帧num_references为false,否则为true  
  frame->id.picture_id = frame->last_seq_num();
  frame->num_references =
      frame->frame_type() == VideoFrameType::kVideoFrameDelta;
  //上一帧最后一个包号  
  frame->references[0] = rtp_seq_num_unwrapper_.Unwrap(last_picture_id_gop);
  //这一步确保第6步的逻辑能跑通,否则第6不逻辑是跑不通的last_picture_id_表示的是当前帧的上一个关键帧的最后一个包的seq,frame->id.picture_id为当前帧的最后一个包的seq,正常情况AheadOf函数是会返回true的。  
  if (AheadOf(frame->id.picture_id, last_picture_id_gop)) {
    //这里修改了容器last_seq_num_gop_对应关键帧的second变量,将当前帧最后一个包号的seq 赋值给他们 
    //正因为有这个操作,第6步才能顺利跑通  
    seq_num_it->second.first = frame->id.picture_id;
    seq_num_it->second.second = frame->id.picture_id;
  }

  last_picture_id_ = frame->id.picture_id;
  //更新填充包状态  
  UpdateLastPictureIdWithPadding(frame->id.picture_id);
  frame->id.picture_id = rtp_seq_num_unwrapper_.Unwrap(frame->id.picture_id);
  return kHandOff;
}
  • 1)确保gop内帧的连续性,对于google vpx系列的编码数据,只需要判断picture_id是否连续即可,num_references表示参考帧数目,对于IDR关键帧可以单独解码,不需要参考帧,所以num_references为0,若gop内任一帧丢失则该gop内的剩余时间都将处于卡顿状态。
  • 2)判断当前帧是否是关键帧,如果是则直接将其该关键帧的最后一个包的seq 生成相应的键值对插入到gop容器last_seq_num_gop_,关键帧是gop的开始。
  • 3)如果last_seq_num_gop_为空表示到此目前为止没收到关键帧,同时当前帧又不是关键帧所以没有参考帧,不能解码,需要缓存该帧。为什么不是直接丢弃?
  • 4)将last_seq_num_gop_容器维护的太旧的关键帧清除掉,规则是当前帧最后一个包seq即[frame->last_seq_num() - 100]之前的关键帧都清理掉,但是至少保留一个(假设规则之前一共就维护了一个gop那么不清除)。
  • 5)以当前帧的最后一个包的seq使用last_seq_num_gop_.upper_bound(frame->last_seq_num())查询,该查询返回last_seq_num_gop_容器中第一个大于frame->last_seq_num()的位置,假设查出的位置就是last_seq_num_gop_的首部,则丢弃该帧,为什么呢?来举个例子,假设last_seq_num_gop_此时存在的seq为34而此时传入的包的seq->first_seq_num() = 10,seq->last_seq_num() =16,而且当前传入的帧为非关键帧,这说明什么意思呢?在传输的过程中可能由于10~16号包这一帧数据中有几个包丢了,而又由于丢包重传发送了PLI请求,也或者是对端主动发送了关键帧,该关键帧的的最后一个包的序号恰好是34,在上文的分析中提到了组包流程,如果组包过程中出现了关键帧,它是不管该关键帧前面的帧的死活的,直接会将该关键帧投递到RtpVideoStreamReceiver2进行处理,而当该关键帧处理之后10~16号包之间被丢失的包又被恢复了,同理会传递到该函数进行处理,此时上述的假设条件就成立了,那么对于这种情况下,该帧应该丢弃掉,因为他后面的关键帧已经被处理了。
  • 6)根据last_seq_num_gop_来判断当前帧和上一帧的连续性,如果不连续(说明没有前向参考帧,不能进行解码)则返回kStash,进行缓存操作。
  • 7)设置picture_id,对于H264数据用一帧的最后一个seq来作为picture_id,设置当前帧的参考帧数目,对于关键帧不需要参考帧所以为0,对于P帧,参考帧数目为1(前向参考)。
  • 更新gop容器last_seq_num_gop_的value值,它也是一个std::pair,这两个值被设置成当前帧的最后一个包的seq,同时也更新RtpFrameObject的id成员,最后返回kHandOff
  • 此处RtpFrameObject父类有3个重要的成员变量id、num_references、references[0]被赋值,其中num_references表示的意思应该为当前帧的和上一帧是参考关系,h264的前向参考。
    WebRtc Video Receiver(五)-设置参考帧
    WebRtc_Video_Stream_Receiver_05_04.png
  • 该函数的决策主要是通过判断seq的连续性(是否有参考帧)或者是否是关键帧,来决定当前帧是否要发到解码模块,或者是进行存储,当出现丢帧现象的时候,需要缓存当前帧然后等待丢失的帧重传。
  • 到此为止,gop容器last_seq_num_gop_的数据成员如下:
    WebRtc Video Receiver(五)-设置参考帧
    WebRtc_Video_Stream_Receiver_05_05.png

    WebRtc Video Receiver(五)-设置参考帧
    WebRtc_Video_Stream_Receiver_05_06.png

4) UpdateLastPictureIdWithPadding更新填充包状态

void RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding(uint16_t seq_num) {
  //取第一个大于seq_num的对应的gop   
  auto gop_seq_num_it = last_seq_num_gop_.upper_bound(seq_num);

  // If this padding packet "belongs" to a group of pictures that we don't track
  // anymore, do nothing.
  if (gop_seq_num_it == last_seq_num_gop_.begin())
    return;
  --gop_seq_num_it;

  // Calculate the next contiuous sequence number and search for it in
  // the padding packets we have stashed.
  uint16_t next_seq_num_with_padding = gop_seq_num_it->second.second + 1;
  auto padding_seq_num_it =
      stashed_padding_.lower_bound(next_seq_num_with_padding);

  // While there still are padding packets and those padding packets are
  // continuous, then advance the "last-picture-id-with-padding" and remove
  // the stashed padding packet.
  while (padding_seq_num_it != stashed_padding_.end() &&
         *padding_seq_num_it == next_seq_num_with_padding) {
    gop_seq_num_it->second.second = next_seq_num_with_padding;
    ++next_seq_num_with_padding;
    padding_seq_num_it = stashed_padding_.erase(padding_seq_num_it);
  }

  // In the case where the stream has been continuous without any new keyframes
  // for a while there is a risk that new frames will appear to be older than
  // the keyframe they belong to due to wrapping sequence number. In order
  // to prevent this we advance the picture id of the keyframe every so often.
  if (ForwardDiff(gop_seq_num_it->first, seq_num) > 10000) {
    auto save = gop_seq_num_it->second;
    last_seq_num_gop_.clear();
    last_seq_num_gop_[seq_num] = save;
  }
}

5) ManageFrame函数业务处理

void RtpFrameReferenceFinder::ManageFrame(
    std::unique_ptr frame) {
  .....
  FrameDecision decision = ManageFrameInternal(frame.get());

  switch (decision) {
    case kStash:
      if (stashed_frames_.size() > kMaxStashedFrames)//最大100
        stashed_frames_.pop_back();
      stashed_frames_.push_front(std::move(frame));
      break;
    case kHandOff:
      HandOffFrame(std::move(frame));
      RetryStashedFrames();
      break;
    case kDrop:
      break;
  }
}
  • 在2.1中分析了ManageFrameInternal的原理,该函数会返回三种不同的决策。
  • 当返回kStash的时候会将当前待解码的帧插入到stashed_frames_容器,等待合适的时机获取,如果容器满了先将末尾的清除掉,然后从头部插入,同时根据上面的分析我们可以得知,出现这种情况是要等待前面的帧完整。所以在kHandOff的情况下先处理当前帧然后再通过RetryStashedFrames获取stashed_frames_中存储的帧进行解码。
  • 当返回kHandOff的时候调用HandOffFrame函数进行再处理。
  • 当返回kDrop的时候直接丢弃该帧数据。
  • stashed_frames_为一个std::deque>队列。
void RtpFrameReferenceFinder::HandOffFrame(
    std::unique_ptr frame) {
  //picture_id_offset_为0  
  frame->id.picture_id += picture_id_offset_;
  for (size_t i = 0; i num_references; ++i) {
    frame->references[i] += picture_id_offset_;
  }
  frame_callback_->OnCompleteFrame(std::move(frame));
}
  • 调用OnCompleteFrame将RtpFrameObject传递到RtpVideoStreamReceiver模块当中。
void RtpFrameReferenceFinder::RetryStashedFrames() {
  bool complete_frame = false;
  do {
    complete_frame = false;
    for (auto frame_it = stashed_frames_.begin();
         frame_it != stashed_frames_.end();) {
      FrameDecision decision = ManageFrameInternal(frame_it->get());

      switch (decision) {
        case kStash:
          ++frame_it;
          break;
        case kHandOff:
          complete_frame = true;
          HandOffFrame(std::move(*frame_it));
          RTC_FALLTHROUGH();
        case kDrop:
          frame_it = stashed_frames_.erase(frame_it);
      }
    }
  } while (complete_frame);
}
  • stashed_frames_容器进行遍历,重新调用ManageFrameInternal进行决策,最后如果决策可解码的话回调HandOffFrame进行处理。
  • 如果决策结果为kDrop直接释放。

6)总结

  • RtpFrameReferenceFinder模块的核心作用就是决策当前帧是否要进入到解码模块。
  • 决策的依据依然是根据seq的连续性,以及是否有关键帧等性质。
  • 在决策为kHandOff的情况下会通过其成员变量frame_callback_将数据重新传递到RtpVideoStreamReceiver模块的OnCompleteFrame函数。
  • 接下来的处理就是解码前的操作了如将数据放到jitterbuffer模块等。
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。