美文网首页 webrtc
WebRtc Video Receiver(五)-设置参考帧

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

作者: JeffreyLau | 来源:发表于2020-10-12 01:02 被阅读0次

    1)前言

    • 经过前面4篇文章的分析,针对WebRtc视频接收模块从创建接收模块、到对RTP流接收处理、关键帧请求的时机、丢包判断以及丢包重传、frame组帧等已经有了一定的概念和认识。
    • 基于以上本文分析rtp包组包后聚合帧发送给解码器前的处理流程,在将一帧完整的帧发送给解码模块之前需要进行一定的预处理,如需要查找参考帧,本文着重分析解码前的参考帧查找原理。
    • 承接上文的分析,rtp包组包成功后会将一帧完整的数据帧投递到RtpVideoStreamReceiver2模块由其OnAssembledFrame函数来进行接收处理。
    • 其实现如下:
    void RtpVideoStreamReceiver2::OnAssembledFrame(
        std::unique_ptr<video_coding::RtpFrameObject> 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<video_coding::RtpFrameReferenceFinder>(
                    this, last_completed_picture_id_ +
                              std::numeric_limits<uint16_t>::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_Stream_Receiver_05_01.png
    • 根据上图的关系图,在RtpFrameReferenceFinder模块中对video_coding::RtpFrameObject数据帧进行处理,如果处理成功最终会生成video_coding::EncodedFrame视频帧,接着回调OnCompleteFrameCallbackOnCompleteFrame函数将视频帧返回到RtpVideoStreamReceiver2模块。

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

    void RtpFrameReferenceFinder::ManageFrame(
        std::unique_ptr<RtpFrameObject> frame) {
      // If we have cleared past this frame, drop it.
      if (cleared_to_seq_num_ != -1 &&
          AheadOf<uint16_t>(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<RTPVideoHeaderLegacyGeneric>(
                  &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_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_Stream_Receiver_05_03.png
    • 该容器是以当前待解码的帧所属的gop(由于IDR关键帧是gop的开始)关键帧的最后一个包的seq位key,以当前帧最后一个包的seq组成的std::pair<seq,seq>为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) << "Generic frame with packet range ["
                            << frame->first_seq_num() << ", "
                            << frame->last_seq_num()
                            << "] has no GoP, dropping frame.";
        return kDrop;
      }
      //如果上述条件不成立这里则返回last_seq_num_gop_最后一个元素对应的迭代器
      //如果当前帧为关键帧的话那么seq_num_it为last_seq_num_gop_.end(),进行--操作后旧对应了最后一个关键帧  
      seq_num_it--;
    
      // Make sure the packet sequence numbers are continuous, otherwise stash
      // this frame.
      // 6) 该步用来判断该帧和上一帧的连续性 
      // last_picture_id_gop得到的是当前gop所维护的当前帧的上一帧(前向参考帧)的最后一个包的seq。  
      uint16_t last_picture_id_gop = seq_num_it->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<uint16_t>(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,seq>,这两个值被设置成当前帧的最后一个包的seq,同时也更新RtpFrameObject的id成员,最后返回kHandOff
    • 此处RtpFrameObject父类有3个重要的成员变量id、num_references、references[0]被赋值,其中num_references表示的意思应该为当前帧的和上一帧是参考关系,h264的前向参考。
      WebRtc_Video_Stream_Receiver_05_04.png
    • 该函数的决策主要是通过判断seq的连续性(是否有参考帧)或者是否是关键帧,来决定当前帧是否要发到解码模块,或者是进行存储,当出现丢帧现象的时候,需要缓存当前帧然后等待丢失的帧重传。
    • 到此为止,gop容器last_seq_num_gop_的数据成员如下:
      WebRtc_Video_Stream_Receiver_05_05.png
      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<RtpFrameObject> 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<std::unique_ptr<RtpFrameObject>>队列。
    void RtpFrameReferenceFinder::HandOffFrame(
        std::unique_ptr<RtpFrameObject> frame) {
      //picture_id_offset_为0  
      frame->id.picture_id += picture_id_offset_;
      for (size_t i = 0; i < frame->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模块等。

    相关文章

      网友评论

        本文标题:WebRtc Video Receiver(五)-设置参考帧

        本文链接:https://www.haomeiwen.com/subject/aflypktx.html