WebRtc Video Receiver(六)-FrameBuffer原理

1)前言

  • 经过前面5篇文章的分析,针对WebRtc视频接收模块从创建接收模块、到对RTP流接收处理、关键帧请求的时机、丢包判断以及丢包重传、frame组帧、组帧后的决策工作(是要发送到解码模块还是继续等待?)等已经有了一定的概念和认识。
  • 本文着重分析组帧后并且进行决策后的分析,根据上文的分析,每帧数据经过决策后,如果条件满足,则会回调RtpFrameReferenceFinder模块对每帧数据进行设置参考帧,之后通过HandOffFrame函数将一帧数据发送到RtpVideoStreamReceiver2模块进行处理。
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));
}
  • RtpFrameReferenceFinder模块和RtpVideoStreamReceiver2模块的派生关系如下图:

    WebRtc_Video_Stream_Receiver_06_01.png

  • 由上图RtpVideoStreamReceiver2的派生关系可知,最终会将RtpFrameObject编码帧送到RtpVideoStreamReceiver2模块的OnCompleteFrame函数进行处理。

void RtpVideoStreamReceiver2::OnCompleteFrame(
    std::unique_ptr<video_coding::EncodedFrame> frame) {
  RTC_DCHECK_RUN_ON(&worker_task_checker_);
  video_coding::RtpFrameObject* rtp_frame =
      static_cast<video_coding::RtpFrameObject*>(frame.get());
  //由上文可知,picture_id指向当前帧的最后一个包的seq number  
  last_seq_num_for_pic_id_[rtp_frame->id.picture_id] =
      rtp_frame->last_seq_num();

  last_completed_picture_id_ =
      std::max(last_completed_picture_id_, frame->id.picture_id);
  complete_frame_callback_->OnCompleteFrame(std::move(frame));
}
  • 经该函数最终将数据由送到complete_frame_callback_进行处理,complete_frame_callback_由谁实现?请看下图:
    WebRtc_Video_Stream_Receiver_06_02.png
  • 由上图可知最终video_coding::RtpFrameObject送给了VideoReceiveStream2模块由该模块的OnCompleteFrame进行处理。
void VideoReceiveStream2::OnCompleteFrame(
    std::unique_ptr<video_coding::EncodedFrame> frame) {
  RTC_DCHECK_RUN_ON(&worker_sequence_checker_);

  // TODO(https://bugs.webrtc.org/9974): Consider removing this workaround.
  /*如果两次插入的视频帧的时间超过10分钟则清除该帧*/  
  int64_t time_now_ms = clock_->TimeInMilliseconds();
  if (last_complete_frame_time_ms_ > 0 &&//10 minutes.
      time_now_ms - last_complete_frame_time_ms_ > kInactiveStreamThresholdMs) {
    frame_buffer_->Clear();
  }
  last_complete_frame_time_ms_ = time_now_ms;
  
  //获取rtp头部的播放延迟,默认值为{-1,-1},该值得作用为啥? 
  const PlayoutDelay& playout_delay = frame->EncodedImage().playout_delay_;
  if (playout_delay.min_ms >= 0) {
    frame_minimum_playout_delay_ms_ = playout_delay.min_ms;
    UpdatePlayoutDelays();
  }

  if (playout_delay.max_ms >= 0) {
    frame_maximum_playout_delay_ms_ = playout_delay.max_ms;
    UpdatePlayoutDelays();
  }

  int64_t last_continuous_pid = frame_buffer_->InsertFrame(std::move(frame));
  if (last_continuous_pid != -1)
    rtp_video_stream_receiver_.FrameContinuous(last_continuous_pid);
}
  • 最终在VideoReceiveStream2模块的OnCompleteFrame函数中将编码帧通过调用frame_buffer_->InsertFrame(std::move(frame))将其插入到video_coding::FrameBuffer模块。
  • frame_buffer_VideoReceiveStream2模块的成员变量,在VideoReceiveStream2模块的构造函数中对其进行实例化。
  • 通过调用UpdatePlayoutDelays函数来将播放最大和最小延迟作用到VCMTiming,该值和Jitter buffer配合起来会得到一个合理的播放延迟时间。
  • 如果插入成功则会返回上一帧数据的picture_id,最终通过回调rtp_video_stream_receiver_.FrameContinuous(last_continuous_pid)将该id作用到NACK Module清除重传列表。(清除范围为该picture_id之前的seq 都被清除掉)。
  • 结合上文的分析,从决策到此步骤的大致导致流程如下:


    WebRtc_Video_Stream_Receiver_06_03.png
  • 本文的重点是分析FrameBuffer的工作原理。

2)InsertFrame插入原理

int64_t FrameBuffer::InsertFrame(std::unique_ptr<EncodedFrame> frame) {
  TRACE_EVENT0("webrtc", "FrameBuffer::InsertFrame");
  RTC_DCHECK(frame);

  rtc::CritScope lock(&crit_);

  const VideoLayerFrameId& id = frame->id;
  //得到上一个连续帧的pid  
  int64_t last_continuous_picture_id =
      !last_continuous_frame_ ? -1 : last_continuous_frame_->picture_id;
  //1) 和前向参考帧进行对比,如前向参考帧的seq和当前帧的seq进行比较。
  if (!ValidReferences(*frame)) {
    RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("
                        << id.picture_id << ":"
                        << static_cast<int>(id.spatial_layer)
                        << ") has invalid frame references, dropping frame.";
    //正常情况下前向参考帧的seq比当前的seq肯定是要小的,这里如果发现该帧的seq 比前向参考帧的seq 还小的话直接丢弃。  
    return last_continuous_picture_id;
  }
  //最大800个frame,如果容器已经满了直接丢弃当前帧,若
  if (frames_.size() >= kMaxFramesBuffered) {
    //如果是关键帧这里将decoded_frames_history_中的历史记录清空,后续介绍。
    //同时也清空FrameBuffer所维护的frames_容器,所有待解码的帧先缓存到该容器。  
    if (frame->is_keyframe()) {
      RTC_LOG(LS_WARNING) << "Inserting keyframe (picture_id:spatial_id) ("
                          << id.picture_id << ":"
                          << static_cast<int>(id.spatial_layer)
                          << ") but buffer is full, clearing"
                             " buffer and inserting the frame.";
      ClearFramesAndHistory();
    } else {
      RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("
                          << id.picture_id << ":"
                          << static_cast<int>(id.spatial_layer)
                          << ") could not be inserted due to the frame "
                             "buffer being full, dropping frame.";
      // 非关键帧,如果缓存容器满了的话直接返回上一个连续帧的pid  
      return last_continuous_picture_id;
    }
  }
  //得到最进一个发送到解码队列中的帧的picture_id,对于h264而言是帧最后一个包序列号seq
  auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();
  //得到最进一个发送到解码队列中的帧的时间戳,该时间戳每一帧是不同的
  auto last_decoded_frame_timestamp =
      decoded_frames_history_.GetLastDecodedFrameTimestamp();
  //如果当前帧的最后一个包的seq(或者picture_id) < 最近解码帧的picture_id,说明有可能是出现乱序,也有可能是序列号环绕所致  
  if (last_decoded_frame && id <= *last_decoded_frame) {
    //如果当前帧的时间戳比上一次已经发送到解码队列的帧的时间戳还要新,可能是编码器重置或者序列号环绕的情况发生,这种情况下如果当前帧是关键帧的话还是可以继续进行解码的。  
    if (AheadOf(frame->Timestamp(), *last_decoded_frame_timestamp) &&
        frame->is_keyframe()) {
      // If this frame has a newer timestamp but an earlier picture id then we
      // assume there has been a jump in the picture id due to some encoder
      // reconfiguration or some other reason. Even though this is not according
      // to spec we can still continue to decode from this frame if it is a
      // keyframe.
      RTC_LOG(LS_WARNING)
          << "A jump in picture id was detected, clearing buffer.";
      //先清空之前缓存的所有帧和历史记录,为啥呢?因为要么编码器已经重置。或者跳帧的现象发生。
      ClearFramesAndHistory();
      last_continuous_picture_id = -1;
    } else {
      // 如果是乱序发生,而且不是关键帧,则丢弃该帧数据。  
      RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("
                          << id.picture_id << ":"
                          << static_cast<int>(id.spatial_layer)
                          << ") inserted after frame ("
                          << last_decoded_frame->picture_id << ":"
                          << static_cast<int>(last_decoded_frame->spatial_layer)
                          << ") was handed off for decoding, dropping frame.";
      return last_continuous_picture_id;
    }
  }

  // Test if inserting this frame would cause the order of the frames to become
  // ambiguous (covering more than half the interval of 2^16). This can happen
  // when the picture id make large jumps mid stream.
  // 如果跳帧较大,清除之前的缓存从该帧开始解码。  
  if (!frames_.empty() && id < frames_.begin()->first &&
      frames_.rbegin()->first < id) {
    RTC_LOG(LS_WARNING)
        << "A jump in picture id was detected, clearing buffer.";
    ClearFramesAndHistory();
    last_continuous_picture_id = -1;
  }

  auto info = frames_.emplace(id, FrameInfo()).first;
  //这表明原先frames_容器中已经有该id的key,本次为重复插入,直接返回上一个连续帧的ID。  
  if (info->second.frame) {
    return last_continuous_picture_id;
  }
  //更新帧信息,如设置帧还未连续的参考帧数量,并建立被参考帧与参考他的帧之间的参考关系,用于当被参考帧有效时,更新参考他的帧的参考帧数量(为0则连续)
  // 以及可解码状态,该函数会更新last_continuous_frame_
  if (!UpdateFrameInfoWithIncomingFrame(*frame, info))
    return last_continuous_picture_id;
    //如果当前帧没有重传包的话,可以用于计算时延,timing_用于计算很多时延指标以及帧的预期渲染时间.
  if (!frame->delayed_by_retransmission())
    timing_->IncomingTimestamp(frame->Timestamp(), frame->ReceivedTime());

  if (stats_callback_ && IsCompleteSuperFrame(*frame)) {
    stats_callback_->OnCompleteFrame(frame->is_keyframe(), frame->size(),
                                     frame->contentType());
  }
  //将当前帧记录到缓存
  info->second.frame = std::move(frame);
  // 如果该帧的未连续的参考帧数量为0,说明当前帧已经连续,如关键帧或者当前P帧参考的上个P帧已经收到,本段代码需要先分析
  // UpdateFrameInfoWithIncomingFrame函数
  if (info->second.num_missing_continuous == 0) {
    info->second.continuous = true;
    //连续性状态传播,后面会分析
    PropagateContinuity(info);//本次插入的时候该函数正常情况下都会正常返回,啥都不做
    last_continuous_picture_id = last_continuous_frame_->picture_id;
    // Since we now have new continuous frames there might be a better frame
    // to return from NextFrame.
    if (callback_queue_) {
      callback_queue_->PostTask([this] {
        rtc::CritScope lock(&crit_);
        if (!callback_task_.Running())
          return;
        RTC_CHECK(frame_handler_);
        callback_task_.Stop();
        //触发解码任务,寻找待解码的帧,并将其发送到解码任务队列,后续会分析  
        StartWaitForNextFrameOnQueue();
      });
    }
  }
  //最终这里返回的是当前帧的picture_id
  return last_continuous_picture_id;
}
  • 以上分析中涉及到几个重要的成员变量如下:


    WebRtc_Video_Stream_Receiver_06_04.png
  • DecodedFramesHistory主要用于维护已解码的帧历史记录,FrameBuffer中维护一个成员变量decoded_frames_history_用于已经发送到解码队列的帧记录。
  • 成员变量last_decoded_frame_记录了上一次发送到解码队列的frame对应的id。
  • 成员变量last_decoded_frame_timestamp_记录上一次发送到解码队列的frame对应的时间戳。该时间戳是rtp时间戳,遵循同一帧数据的时间戳相同的原则。
  • DecodedFramesHistory提供两个方法用于获取上述两个成员变量,分别为GetLastDecodedFrameId()GetLastDecodedFrameTimestamp()
  • 这里为何要介绍它们?因为在FrameBuffer::InsertFrame函数的处理逻辑中首先会和它们进行比较,根据比较结果判断当前要插入的数据是否是合理的。

2.1)UpdateFrameInfoWithIncomingFrame更新参考帧信息

//参数info是表示当前帧在frame_容器中的位置对应的迭代器
bool FrameBuffer::UpdateFrameInfoWithIncomingFrame(const EncodedFrame& frame,
                                                   FrameMap::iterator info) {
  TRACE_EVENT0("webrtc", "FrameBuffer::UpdateFrameInfoWithIncomingFrame");
  const VideoLayerFrameId& id = frame.id;//VideoLayerFrameId

  auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();
  RTC_DCHECK(!last_decoded_frame || *last_decoded_frame < info->first);

  struct Dependency {
    VideoLayerFrameId id;
    bool continuous;
  };
  //还未填充依赖  
  std::vector<Dependency> not_yet_fulfilled_dependencies;

  // Find all dependencies that have not yet been fulfilled.
  // 根据当前帧的参考帧数目进行遍历,该值在设置参考帧模块里面被设置,对于h264数据而言非关键帧的num_references=1 
  for (size_t i = 0; i < frame.num_references; ++i) {
    //构造零时参考帧id实例。  
    VideoLayerFrameId ref_key(frame.references[i], frame.id.spatial_layer);
    // Does |frame| depend on a frame earlier than the last decoded one?
    // 如果当前帧的参考帧的id等于或者小于最新的解码帧,则有可能是乱序问题,正常情况下,当前帧的参考帧要么已经被解码(等于)要么是还未解码(大于)。
    if (last_decoded_frame && ref_key <= *last_decoded_frame) {
      // Was that frame decoded? If not, this |frame| will never become
      // decodable.
      // 如果这个参考帧还未解码(乱序),那么这个参考帧将不再有机会被解码, 那么当前帧也无法被解码,
      // 返回失败,反之如果这个参考帧已经被解码了,则属于正常状态。  
      if (!decoded_frames_history_.WasDecoded(ref_key)) {
        int64_t now_ms = clock_->TimeInMilliseconds();
        if (last_log_non_decoded_ms_ + kLogNonDecodedIntervalMs < now_ms) {
          RTC_LOG(LS_WARNING)
              << "Frame with (picture_id:spatial_id) (" << id.picture_id << ":"
              << static_cast<int>(id.spatial_layer)
              << ") depends on a non-decoded frame more previous than"
                 " the last decoded frame, dropping frame.";
          last_log_non_decoded_ms_ = now_ms;
        }
        return false;
      }
    } else { //如果当前帧的参考帧比最新的解码帧的id要大,说明该参考帧可能还未连续,还未发送到解码队列。
      // 查询缓存
      auto ref_info = frames_.find(ref_key);
      //如果ref_info != frames_.end()说明当前帧的参考帧还在缓存当中,这里是判断当前帧的参考帧是否连续。
      //同时满足ref_info != frames_.end()和ref_info->second.continuous则表示该参考帧是联系的  
      bool ref_continuous =
          ref_info != frames_.end() && ref_info->second.continuous;
      // 该参考帧不管连续还是不连续都会插入到not_yet_fulfilled_dependencies临时依赖容器 
      not_yet_fulfilled_dependencies.push_back({ref_key, ref_continuous});
    }
  }// end for loop

  // Does |frame| depend on the lower spatial layer?
  if (frame.inter_layer_predicted) {
    VideoLayerFrameId ref_key(frame.id.picture_id, frame.id.spatial_layer - 1);
    auto ref_info = frames_.find(ref_key);

    bool lower_layer_decoded =
        last_decoded_frame && *last_decoded_frame == ref_key;
    bool lower_layer_continuous =
        lower_layer_decoded ||
        (ref_info != frames_.end() && ref_info->second.continuous);

    if (!lower_layer_continuous || !lower_layer_decoded) {
      not_yet_fulfilled_dependencies.push_back(
          {ref_key, lower_layer_continuous});
    }
  }
  //未连续参考帧计数器,初始值为not_yet_fulfilled_dependencies容器大小
  info->second.num_missing_continuous = not_yet_fulfilled_dependencies.size();
  //未解码参考帧计数器,当前帧还未发送到解码队列的参考帧个数,初始值也未容器大小
  info->second.num_missing_decodable = not_yet_fulfilled_dependencies.size();
  
  // 遍历not_yet_fulfilled_dependencies容器,根据内部元素的continuous值来更新info->second.num_missing_continuous
  // 的个数,因为在插入not_yet_fulfilled_dependencies容器的值其内部成员的continuous有可能为true也有可能为false  
  for (const Dependency& dep : not_yet_fulfilled_dependencies) {
    // 如果某个参考帧已经连续,则将当前帧记录未连续参考帧的计数减1  
    if (dep.continuous)
      --info->second.num_missing_continuous;
    // 建立参考帧->依赖帧反向关系,用于传播状态,此时的dep.id对应的是参考帧的id,对于H264而言应该就是前向参考帧的ID。
    // 这里是为当前帧的参考帧所管理的dependent_frames填充id,而该id为当前帧的id。  
    frames_[dep.id].dependent_frames.push_back(id);
  }

  return true;
}
  • 根据上面的分析以及结合代码,FrameBuffer插入流程所涉及的数据结构如下图:


    WebRtc_Video_Stream_Receiver_06_05.png
  • 如果插入成功会将当前的视频帧EncodedFrame实例,封装成FrameInfo结构,插入过程会议当前帧的帧ID做为key,以新实例化的FrameInfo为value插入到frames_容器。

  • 同时在数据插入的过程中通过调用UpdateFrameInfoWithIncomingFrame函数来遍历EncodedFrame实例中已经设置的参考帧,来初始化FrameInfo成员,主要是统计当前帧的参考帧的连续性,当统计得出如果当前帧的参考帧也是连续的则FrameInfo中的成员continuous会被设置成true,同时num_missing_continuous会被设置成0

  • EncodedFrame是被记录到对应FrameInfo的成员变量frame。

WebRtc_Video_Stream_Receiver_06_06.png
  • 每次插入数据的时候以H264数据为列,前向参考帧为一帧,所以假设本次插入的是第8帧,那么检测的是第8帧和第7帧之间的连续性。同时根据上述UpdateFrameInfoWithIncomingFrame函数的最后处理frames_[dep.id].dependent_frames.push_back(id)可得知,对于当前帧插入的时候当前帧所对应的FrameInfo结构中的dependents_frames集合是没有被初始化的,上述的push_back操作是将当前帧的picturd_id信息插入到当前帧的前向参考帧所对应的FrameInfo结构中的dependents_frames集合当中,使用这种操作来让参考帧和当前帧之间建立起关系。

2.2)PropagateContinuity连续性传播

//参数start是表示当前帧在frame_容器中的位置对应的迭代器
void FrameBuffer::PropagateContinuity(FrameMap::iterator start) {
  TRACE_EVENT0("webrtc", "FrameBuffer::PropagateContinuity");
  RTC_DCHECK(start->second.continuous);

  std::queue<FrameMap::iterator> continuous_frames;
  continuous_frames.push(start);

  // A simple BFS to traverse continuous frames.
  while (!continuous_frames.empty()) {
    auto frame = continuous_frames.front();
    continuous_frames.pop();
    if (!last_continuous_frame_ || *last_continuous_frame_ < frame->first) {
      last_continuous_frame_ = frame->first;
    }
    // Loop through all dependent frames, and if that frame no longer has
    // any unfulfilled dependencies then that frame is continuous as well.
    //   
    for (size_t d = 0; d < frame->second.dependent_frames.size(); ++d) {
      auto frame_ref = frames_.find(frame->second.dependent_frames[d]);
      RTC_DCHECK(frame_ref != frames_.end());
      // TODO(philipel): Look into why we've seen this happen.
      if (frame_ref != frames_.end()) {
        //对于h264数据而言num_missing_continuous的最大值为1
        --frame_ref->second.num_missing_continuous;
        if (frame_ref->second.num_missing_continuous == 0) {
          frame_ref->second.continuous = true;
          continuous_frames.push(frame_ref);
        }
      }
    }
  }
}
  • 该函数使用广度优先搜索算法,首先将根节点放入continuous_frames搜索队列中(也就是这里假设为第8帧数据)。

  • 从队列中取出第一个节点,并检验它是否为目标。这里的检测原理是根据第8帧数据对应的FrameInfo结构所存储的dependent_frames集合,通过遍历dependent_frames它,而经过上面的分析,对于当前刚插入的帧frame->second.dependent_frames.size()默认是等于0的,因为在上面UpdateFrameInfoWithIncomingFrame函数中是为当前帧的参考帧设置了dependent_frames

  • 上述函数的主要作用就是更新了last_continuous_frame_的值,将该值更新为当前插入帧的id。而对于for循环的函数体似乎没有什么作用,经过调试发现也是一直未执行的,至少对于H264的数据是这样的。

3)decode_queue_解码任务队列工作原理

  • 解码任务队列定义在VideoReceiveStream2模块当中,通过调用VideoReceiveStream2模块的Start()函数让解码任务队列工作处于循环模型。
void VideoReceiveStream2::Start() {
  RTC_DCHECK_RUN_ON(&worker_sequence_checker_);
  ....
  decode_queue_.PostTask([this] {
    RTC_DCHECK_RUN_ON(&decode_queue_);
    decoder_stopped_ = false;
    StartNextDecode();
  });
  ....  
}
  • VideoReceiveStream2::Start()函数通过解码任务队列PostTask,这样StartNextDecode函数会由decode_queue_的内部异步线程获取该任务并执行。
void VideoReceiveStream2::StartNextDecode() {
  // Running on the decode thread.
  TRACE_EVENT0("webrtc", "VideoReceiveStream2::StartNextDecode");
  frame_buffer_->NextFrame(
      GetMaxWaitMs(), //本次任务执行,最多等待多长时间
      keyframe_required_, //本次任务执行是否需要请求关键帧
      &decode_queue_,//解码任务队列
      /* encoded frame handler */
      [this](std::unique_ptr<EncodedFrame> frame, ReturnReason res) {
        RTC_DCHECK_EQ(frame == nullptr, res == ReturnReason::kTimeout);
        RTC_DCHECK_EQ(frame != nullptr, res == ReturnReason::kFrameFound);
        decode_queue_.PostTask([this, frame = std::move(frame)]() mutable {
          RTC_DCHECK_RUN_ON(&decode_queue_);
          if (decoder_stopped_)
            return;
          if (frame) {
            HandleEncodedFrame(std::move(frame));
          } else {
            int64_t now_ms = clock_->TimeInMilliseconds();
            worker_thread_->PostTask(ToQueuedTask(
                task_safety_, [this, now_ms, wait_ms = GetMaxWaitMs()]() {
                  RTC_DCHECK_RUN_ON(&worker_sequence_checker_);
                  HandleFrameBufferTimeout(now_ms, wait_ms);
                }));
          }
          StartNextDecode();
        });
      });
}
  • 该函数通过调用FrameBuffer::NextFrame,并传入相应的参数。其中有两个lamda匿名函数,第一个大外部lamda函数会被FrameBuffer内部调用。

  • 其中最为重要的就是lamda匿名函数,该函数用于处理待解码的视频帧,同时也是使用decode_queue_将其放入任务队列,进行异步处理。

  • 该函数的处理分为两类,一类是超时处理(也就是)NextFrame函数如果处理超时的话,其二是正常得到待解码的视频帧,通过调用HandleEncodedFrame函数对该帧数据进行解码操作。

  • 最后会继续调用StartNextDecode函数,向FrameBuffer模块获取待解码的视频帧。

  • 该函数此处不做过多的详细分析,首先分析NextFrame函数的工作机理。后续再来着重分析解码操作,其大致原理如下:

    WebRtc_Video_Stream_Receiver_06_07.png

  • 由上图可知,decode_queue_解码任务队列会是一种循环模型,其核心是通过FrameBuffer::NextFrame函数得到有效的待解码视频帧,然后继续通过decode_queue_的Post向decode_queue_解码队列投递任务。

4)FrameBuffer::NextFrame()函数工作流程

void FrameBuffer::NextFrame(
    int64_t max_wait_time_ms,//本次调度最多等待多少ms就认为是超时。
    bool keyframe_required,
    rtc::TaskQueue* callback_queue,
    std::function<void(std::unique_ptr<EncodedFrame>, ReturnReason)> handler) {
  RTC_DCHECK_RUN_ON(&callback_checker_);
  RTC_DCHECK(callback_queue->IsCurrent());
  TRACE_EVENT0("webrtc", "FrameBuffer::NextFrame");
  //当前时间+最大超时时间的毫秒数得到,本次调度的返回时间。  
  int64_t latest_return_time_ms =
      clock_->TimeInMilliseconds() + max_wait_time_ms;
  rtc::CritScope lock(&crit_);
  if (stopped_) {
    return;
  }
  //保存当前任务最大返回时间的相对时间值。  
  latest_return_time_ms_ = latest_return_time_ms;
  //当前任务是否要请求关键帧  
  keyframe_required_ = keyframe_required;
  //保存函数句柄,对应VideoReceiveStream2::StartNextDecode()函数中定义的外部大lamda匿名函数  
  frame_handler_ = handler;
  //保存解码循环队列指针  
  callback_queue_ = callback_queue;
  StartWaitForNextFrameOnQueue();
}
  • NextFrame函数记录本次传入的相关变量和指针,然后将任务交给StartWaitForNextFrameOnQueue进行获取已经准备好的视频帧数据。
void FrameBuffer::StartWaitForNextFrameOnQueue() {
  RTC_DCHECK(callback_queue_);
  RTC_DCHECK(!callback_task_.Running());
  int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
  callback_task_ = RepeatingTaskHandle::DelayedStart(
      callback_queue_->Get(), TimeDelta::Millis(wait_ms), [this] {
        RTC_DCHECK_RUN_ON(&callback_checker_);
        // If this task has not been cancelled, we did not get any new frames
        // while waiting. Continue with frame delivery.
        rtc::CritScope lock(&crit_);
        if (!frames_to_decode_.empty()) {
          // We have frames, deliver!
          frame_handler_(absl::WrapUnique(GetNextFrame()), kFrameFound);
          CancelCallback();
          return TimeDelta::Zero();  // Ignored.
        } else if (clock_->TimeInMilliseconds() >= latest_return_time_ms_) {
          // We have timed out, signal this and stop repeating.
          frame_handler_(nullptr, kTimeout);
          CancelCallback();
          return TimeDelta::Zero();  // Ignored.
        } else {
          // If there's no frames to decode and there is still time left, it
          // means that the frame buffer was cleared between creation and
          // execution of this task. Continue waiting for the remaining time.
          int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
          return TimeDelta::Millis(wait_ms);
        }
      });
}
  • 该函数首先通过FindNextFrame函数从frames_中找到满足条件的视频帧,并得到当前待解码帧在frames_中的迭代器,并将该迭代器插入到容器frames_to_decode_

  • 其次是构造延迟重复任务,并将该延迟重复任务放到解码任务队列decode_queue_中运行,这样看来在上图的步骤4~6以及步骤9~11之间,需要先执行该重复任务的,然后再该重复任务处理当中,如果检测到frames_to_decode_容器不为空,则调用frame_handler_也就是StartNextDecode函数中传入的外部lamda匿名函数体,最后停止该重复任务,等待下次一解码任务循环调用。

  • 为什么要重复延迟任务?因为异步的原因,在查找视频的时候可能数据还没有插入,那么就需要重复查找?

  • 而该节分析着重分析FindNextFrame的原理,分析之前先看一下所涉及到的相关数据结构。

    WebRtc_Video_Stream_Receiver_06_08.png

  • FrameBuffer模块中对于插入线程,有个frames_容器用于缓存数据,而通过FindNextFrame函数的处理会从frames_容器中查找合适的数据帧,并得到对应数据帧的迭代器,将该迭代器记录到frames_to_decode_容器当中。

  • 在延迟重复任务的执行过程当中如果发现frames_to_decode_容器不为空则会通过GetNextFrame()函数访问frames_to_decode_容器,最终得到需要解码的视频帧将其投递到decode_queue_任务队列里。

5)FrameBuffer::FindNextFrame()函数工作流程

int64_t FrameBuffer::FindNextFrame(int64_t now_ms) {
  //latest_return_time_ms_为本次任务最大的超时时间时间的相对值,这个计算得到最大的等待时间间隔  
  //该值在使用640*480@25fps的屏幕共享调试过程中有3000ms左右,也就是说最大可等待3s如果3秒还没找到合适的帧,那么本次调度就按照超时来算了。  
  int64_t wait_ms = latest_return_time_ms_ - now_ms;
  //首先清空frames_to_decode_,这说明每次是获取一帧数据,然后立马送到解码队列。  
  frames_to_decode_.clear();

  // |last_continuous_frame_| may be empty below, but nullopt is smaller
  // than everything else and loop will immediately terminate as expected.
  //循环遍历frames_集合,从头部到尾部,frame_it->first <= last_continuous_frame_,当前要送到解码队列的数据帧
  // 比上一次插入的数据的id要小或相等。  
  for (auto frame_it = frames_.begin();
       frame_it != frames_.end() && frame_it->first <= last_continuous_frame_;
       ++frame_it) {
    //如果当前一帧的参考帧不连续则重新遍历。++frame_it  
    if (!frame_it->second.continuous ||
        frame_it->second.num_missing_decodable > 0) {
      continue;
    }
    
    EncodedFrame* frame = frame_it->second.frame.get();
    
    //如果本次解码任务是要求请求关键帧,但是当前遍历出来的这一帧是P帧,则重新遍历++frame_it  
    if (keyframe_required_ && !frame->is_keyframe())
      continue;
    
    auto last_decoded_frame_timestamp =
        decoded_frames_history_.GetLastDecodedFrameTimestamp();

    // TODO(https://bugs.webrtc.org/9974): consider removing this check
    // as it may make a stream undecodable after a very long delay between
    // frames.
    // 根据每帧数据的rtp时间戳不相等,并且后一帧的时间戳要比前一帧的时间戳要大的原则,如果 
    // last_decoded_frame_timestamp上一次送到解码队列的一帧的时间戳比当前遍历出的时间戳还要大的话则重新遍历  
    if (last_decoded_frame_timestamp &&
        AheadOf(*last_decoded_frame_timestamp, frame->Timestamp())) {
      continue;
    }

    // Only ever return all parts of a superframe. Therefore skip this
    // frame if it's not a beginning of a superframe.
    // VPX相关处理.  
    if (frame->inter_layer_predicted) {
      continue;
    }

    // Gather all remaining frames for the same superframe.
    std::vector<FrameMap::iterator> current_superframe;
    //尾部插入  
    current_superframe.push_back(frame_it);
    // H264为true只有一层.  
    bool last_layer_completed = frame_it->second.frame->is_last_spatial_layer;
    FrameMap::iterator next_frame_it = frame_it;
    while (true) {
      ++next_frame_it;
      //对于H264这个判断会break;  
      if (next_frame_it == frames_.end() ||
          next_frame_it->first.picture_id != frame->id.picture_id ||
          !next_frame_it->second.continuous) {
        break;
      }
      // Check if the next frame has some undecoded references other than
      // the previous frame in the same superframe.
      size_t num_allowed_undecoded_refs =
          (next_frame_it->second.frame->inter_layer_predicted) ? 1 : 0;
      if (next_frame_it->second.num_missing_decodable >
          num_allowed_undecoded_refs) {
        break;
      }
      // All frames in the superframe should have the same timestamp.
      if (frame->Timestamp() != next_frame_it->second.frame->Timestamp()) {
        RTC_LOG(LS_WARNING) << "Frames in a single superframe have different"
                               " timestamps. Skipping undecodable superframe.";
        break;
      }
      current_superframe.push_back(next_frame_it);
      last_layer_completed = next_frame_it->second.frame->is_last_spatial_layer;
    }
    // Check if the current superframe is complete.
    // TODO(bugs.webrtc.org/10064): consider returning all available to
    // decode frames even if the superframe is not complete yet.
    // 对于h264 last_layer_completed = true  
    if (!last_layer_completed) {
      continue;
    }
    //通过std::move将current_superframe迭代器容器移动到frames_to_decode_
    frames_to_decode_ = std::move(current_superframe);
    //如果未设置渲染时间,则这里设置渲染时间,默认h264数据frame->RenderTime() == -1
    if (frame->RenderTime() == -1) {
      frame->SetRenderTime(timing_->RenderTimeMs(frame->Timestamp(), now_ms));
    }
    //重新获取等待时间,是什么原理?很重要后续会进行深入分析
    wait_ms = timing_->MaxWaitingTime(frame->RenderTime(), now_ms);

    // This will cause the frame buffer to prefer high framerate rather
    // than high resolution in the case of the decoder not decoding fast
    // enough and the stream has multiple spatial and temporal layers.
    // For multiple temporal layers it may cause non-base layer frames to be
    // skipped if they are late.
    // 如果wait_ms小于-5 (kMaxAllowedFrameDelayMs的值为5),
    // 根据上面的英文注释是表示在高帧率的情况下解码器性能有限,该帧已经来不及渲染了,需要忽略该帧。  
    if (wait_ms < -kMaxAllowedFrameDelayMs)
      continue;
    //到此已经完美的找到了一个待解码帧对应在frames_容器中的迭代器位置了。
    break;
  }
  //更新剩余等待时间,先取最小值,后面和0取最大值,这里返回的是一个时间间隔,任务调度可能最大超时为3秒,经过上述的处理和评估,这里进行重新估计。
  //这个值会作用到哪里?  
  wait_ms = std::min<int64_t>(wait_ms, latest_return_time_ms_ - now_ms);
  wait_ms = std::max<int64_t>(wait_ms, 0);
  return wait_ms;
}
  • 本函数首先会进行一系列的校验。
  • 其次、通过遍历frames_容器获取待解码的视频帧,并带到它在frames_容器中的位置对应的迭代器,将其插入到frames_to_decode_容器。
  • 最后会返回一个时间间隔,那么这个时间间隔是干嘛用的,会作用到哪里?根据上述的代码显示该值最终传递到了RepeatingTaskHandle::DelayedStart函数,应该是表示,RepeatingTaskHandle::DelayedStart这个重复延迟任务经过多少wait_ms后会被执行。那么问题来了,这个延迟就会直接影响到解码和渲染的延迟,所以对于延迟的优化,这个参数是一个优化点
  • 同时本节也留下了一个问题timing_->MaxWaitingTime函数的原理是什么?

6)RepeatingTaskHandle::DelayedStart延迟重复任务工作流程

  • 首先回顾解码队列和延迟任务的配合流程。


    WebRtc_Video_Stream_Receiver_06_09.png
  • 通过FindNextFrame函数获取到下一次要进行解码的视频帧后后,该函数会返回一个延迟重复任务执行的延迟时间wait_ms(也就是过多长时间后延迟任务会被执行)。

  • 继续回到StartWaitForNextFrameOnQueue函数。

void FrameBuffer::StartWaitForNextFrameOnQueue() {
  RTC_DCHECK(callback_queue_);
  RTC_DCHECK(!callback_task_.Running());
  int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
  callback_task_ = RepeatingTaskHandle::DelayedStart(
      callback_queue_->Get(), TimeDelta::Millis(wait_ms), [this] {
        RTC_DCHECK_RUN_ON(&callback_checker_);
        // If this task has not been cancelled, we did not get any new frames
        // while waiting. Continue with frame delivery.
        rtc::CritScope lock(&crit_);
        if (!frames_to_decode_.empty()) {//已经有待解码的帧
          // We have frames, deliver!
          frame_handler_(absl::WrapUnique(GetNextFrame()), kFrameFound);
          CancelCallback();
          return TimeDelta::Zero();  // Ignored.
        } else if (clock_->TimeInMilliseconds() >= latest_return_time_ms_) {//已经超时
          // We have timed out, signal this and stop repeating.
          frame_handler_(nullptr, kTimeout);
          CancelCallback();
          return TimeDelta::Zero();  // Ignored.
        } else {//没找到帧也没有超时
          // If there's no frames to decode and there is still time left, it
          // means that the frame buffer was cleared between creation and
          // execution of this task. Continue waiting for the remaining time.
          int64_t wait_ms = FindNextFrame(clock_->TimeInMilliseconds());
          return TimeDelta::Millis(wait_ms);
        }
      });
}
  • 第一种情况为当frames_to_decode_不为空,也就是FindNextFrame找到了合适的待解码的视频帧,此时首先条用GetNextFrame()函数获取该帧,然后通过回调frame_handler_也就是在VideoReceiveStream2模块执行StartNextDecode调度的时候传入的外部lamda匿名函数。
  • 第二种情况,clock_->TimeInMilliseconds() >= latest_return_time_ms_说明本次调度已经超时了,当前时间的相对时间值已经大于超时的相对时间值了。
  • 第三种情况,未超时,但是也未找到合适的待解码帧,此时回调FindNextFrame进行重复找帧处理。
  • 本节着重分析GetNextFrame()函数的原理

6.1 )GetNextFrame()原理

EncodedFrame* FrameBuffer::GetNextFrame() {
  RTC_DCHECK_RUN_ON(&callback_checker_);
  int64_t now_ms = clock_->TimeInMilliseconds();
  // TODO(ilnik): remove |frames_out| use frames_to_decode_ directly.
  std::vector<EncodedFrame*> frames_out;
  RTC_DCHECK(!frames_to_decode_.empty());
  //定义超级帧是否由重传帧
  bool superframe_delayed_by_retransmission = false;
  //定义超级帧的大小
  size_t superframe_size = 0;
  //从头部获取,上面是尾部插入,这里刚好满足先入先出的原则  
  EncodedFrame* first_frame = frames_to_decode_[0]->second.frame.get();
  //得到预期渲染时间,在FindNextFrame函数中设置  
  int64_t render_time_ms = first_frame->RenderTime();
  /* 当前帧数据最后一个包的接收时间。接收时间和渲染时间一相减是不是就得出了当前帧数据
   * 从组帧到解码到渲染之间的延迟了?经过调试发现
   * 延迟在从组帧到解码再到渲染之间的时间确实是比较大的
   *华为mate30 1920*1080@30fps差不多平均有130ms,需要优化
   * 从接收到该帧的最后一个包到当前处理的延迟5~30ms,也就是从解码到渲染起码占100ms
   * 以上为在局域网测试
   * 将这段时间如果能降低到50ms以内,那整个延迟就真的很优秀了。
  */
  int64_t receive_time_ms = first_frame->ReceivedTime();
  // Gracefully handle bad RTP timestamps and render time issues.
  // 检查帧的渲染时间戳或者当前的目标延迟是否有异常,如果是则重置时间处理器,重新获取帧的渲染时间,规则在下面进行分析。
  if (HasBadRenderTiming(*first_frame, now_ms)) {
    jitter_estimator_.Reset();
    timing_->Reset();
    render_time_ms = timing_->RenderTimeMs(first_frame->Timestamp(), now_ms);
  }
  // 遍历所有待解码帧(他们应该有同样的时间戳),如果由多帧数据最后会封装成一个超级帧
  // 根据实验结果基本上都是一帧  
  for (FrameMap::iterator& frame_it : frames_to_decode_) {
    RTC_DCHECK(frame_it != frames_.end());
    //释放frame_容器中的FrameInfo结构中的frame内存,这里用frame来接收  
    EncodedFrame* frame = frame_it->second.frame.release();
    //每一次调度要送到解码队列中的待解码帧都由相同的渲染时间。
    //为每帧设置渲染时间,最后该集合中的帧会被打包成一个大的frame,送到解码队列  
    frame->SetRenderTime(render_time_ms);
    //每次遍历取或,如果里面有帧数据是属于重传过来的这里将被设置成true
    superframe_delayed_by_retransmission |= frame->delayed_by_retransmission();
    //计算最大接收时间,取最大的假设这个frames_to_decode_有5帧数据那么取时间戳最大的  
    receive_time_ms = std::max(receive_time_ms, frame->ReceivedTime());
    //累加所有帧的大小, 
    superframe_size += frame->size();
    //传播能否解码的连续性。这里要用来干嘛?
    PropagateDecodability(frame_it->second);
    //将即将要发送到解码队列的数据信息插入到历史记录,对已发送到解码队列中的帧进行统计。  
    decoded_frames_history_.InsertDecoded(frame_it->first, frame->Timestamp());
    // Remove decoded frame and all undecoded frames before it.
    // 状态回调,通过std::count_if统计在frame_it之前多少帧数据要被drop掉  
    if (stats_callback_) {
      unsigned int dropped_frames = std::count_if(
          frames_.begin(), frame_it,
          [](const std::pair<const VideoLayerFrameId, FrameInfo>& frame) {
            return frame.second.frame != nullptr;
          });
      if (dropped_frames > 0) {
        stats_callback_->OnDroppedFrames(dropped_frames);
      }
    }
    //将要发送的帧从缓存记录中清除。
    frames_.erase(frames_.begin(), ++frame_it);
    //清除的这一帧数据先存入到frames_out容器,最后会将该集合中的所有帧打包成一个超级帧
    frames_out.push_back(frame);
  }
  //如果上面得出要发送到解码队列的帧集合中有
  if (!superframe_delayed_by_retransmission) {
    int64_t frame_delay;
    //计算延迟
    if (inter_frame_delay_.CalculateDelay(first_frame->Timestamp(),
                                          &frame_delay, receive_time_ms)) {
      //frame_delay的值可能为负值  
      jitter_estimator_.UpdateEstimate(frame_delay, superframe_size);
    }
    //protection_mode_默认为kProtectionNack
    float rtt_mult = protection_mode_ == kProtectionNackFEC ? 0.0 : 1.0;
    absl::optional<float> rtt_mult_add_cap_ms = absl::nullopt;
    //若rtt_mult_settings_有值则获取该值,用于下面作用到JitterDelay
    if (rtt_mult_settings_.has_value()) {
      //可通过类似"WebRTC-RttMult/Enable-0.60,100.0/"来启用或者设置值,默认是没有值的  
      rtt_mult = rtt_mult_settings_->rtt_mult_setting;
      rtt_mult_add_cap_ms = rtt_mult_settings_->rtt_mult_add_cap_ms;
    }
    //设置JitterDelay
    timing_->SetJitterDelay(
        jitter_estimator_.GetJitterEstimate(rtt_mult, rtt_mult_add_cap_ms));
    //更新当前延迟
    timing_->UpdateCurrentDelay(render_time_ms, now_ms);
  } else {
    //如果有重传帧,那么延迟估计根据FrameNacked来更新。
    if (RttMultExperiment::RttMultEnabled() || add_rtt_to_playout_delay_)
      jitter_estimator_.FrameNacked();
  }
  //更新JitterDelay
  UpdateJitterDelay();
  //更新帧率时序信息
  UpdateTimingFrameInfo();
  //如果只有一帧的话则直接返回frames_out[0]
  if (frames_out.size() == 1) {
    return frames_out[0];
  } else {
    //打包超级帧
    return CombineAndDeleteFrames(frames_out);
  }
}
  • 该函数的核心原理是首先是通过HasBadRenderTiming函数判断待解码帧的时序是否有效
  • 其次是遍历frames_to_decode_容器将容器内的所有帧放到临时容器frames_out当中,并清理缓存记录
  • 根据frames_out中的帧中是否由重传帧存在做不同的时序更新处理。
  • 各种设置JitterDelay以及更新JitterDelay,这些内容在下文进行分析。
  • 最后若frames_out中的帧的数量大于一,则将该容器中的帧通过CombineAndDeleteFrames打包成一个超级聚合帧。
  • 最终将打包好的帧返回给frame_handler_函数句柄进行响应的处理。
  • 上述所涉及到的时延更新是延迟相关的重点,在下文进行深入分析。

6.2 )HasBadRenderTiming()原理


bool FrameBuffer::HasBadRenderTiming(const EncodedFrame& frame,
                                     int64_t now_ms) {
  // Assume that render timing errors are due to changes in the video stream.
  int64_t render_time_ms = frame.RenderTimeMs();
  // Zero render time means render immediately.
  if (render_time_ms == 0) {
    return false;
  }
  if (render_time_ms < 0) {
    return true;
  }
  const int64_t kMaxVideoDelayMs = 10000;
  if (std::abs(render_time_ms - now_ms) > kMaxVideoDelayMs) {
    int frame_delay = static_cast<int>(std::abs(render_time_ms - now_ms));
    RTC_LOG(LS_WARNING)
        << "A frame about to be decoded is out of the configured "
           "delay bounds ("
        << frame_delay << " > " << kMaxVideoDelayMs
        << "). Resetting the video jitter buffer.";
    return true;
  }
  if (static_cast<int>(timing_->TargetVideoDelay()) > kMaxVideoDelayMs) {
    RTC_LOG(LS_WARNING) << "The video target delay has grown larger than "
                        << kMaxVideoDelayMs << " ms.";
    return true;
  }
  return false;
}
  • 该函数的核心作用是判断当前帧的渲染时间是否合理。
  • 如果render_time_ms等于0表示立即渲染,而frame.RenderTimeMs()的时间是在FrameBuffer::FindNextFrame()中被设置。
  • 如果render_time_ms小于0,说明当前帧的渲染时间是有问题的。
  • 如果渲染时间和当前时间的差值大于10s说明也有问题。
  • 如果timing_->TargetVideoDelay() 大于10秒说明有问题。

7)总结

  • 本文从视频帧向Framebuffer插入流程着手,重点分析了其插入原理,以及Framebuffer的数据结构。
  • Framebuffer主要维护两大数据结构,其一是frames_容器,用于缓存待解码的视频帧。在插入的过程中会判断当前插入帧对应参考帧的连续性,如果当前帧在插入的时候发现前面的参考帧还没有,那么会插入失败。
  • 其二是frames_to_decode_该容器是用于缓存待输出到解码队列的视频帧所对应在frames_容器中的坐标迭代器。
  • 将视频帧由frames_容器取出发送到解码队列使用了解码任务队列驱动,如果frames_to_decode_容器中的大小大于1的话最终会将多帧数据打包成一个超级帧,然后发送到解码任务队列进行处理。
  • 同时在分析本文的时候发现,视频帧在接收过程中从收到一帧数据组帧到将其发送到解码队列前的时间基本都可以控制在60~80ms之内的(当然和丢包以及分辨率有关系),但是从实际调试信息来看,期望的渲染时间有点大,有很大的优化空间,那么这个期望的渲染时间是怎么得来的将是下文分析的重点。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341