WebRTC视频接收JitterBuffer

视频接收JitterBuffer

带着问题去阅读代码或许会更好:

  1. 它是如何保证收到完整的帧?
  2. 遇到收不完整的帧如何处理?
  3. 它是如何处理动态分辨率的?
  4. 它是如何保证帧的顺序?
  5. 如何保证不花屏?
  6. 整个环节有几个线程?那些环节分别使用什么线程?
  7. 视频的分辨率、旋转角度是从哪里来的?

网络接收组帧(以H.264为例)

SignalReadPacket -> WebRtcVideoChannel::OnPacketReceived -> Call::DeliverPacket ->
Call::DeliverRtp -> RtpStreamReceiverController::OnRtpPacket -> RtpDemuxer::OnRtpPacket ->
RtpVideoStreamReceiver::OnRtpPacket -> RtpVideoStreamReceiver::ReceivePacket ->
RtpReceiverImpl::IncomingRtpPacket -> RTPReceiverVideo::ParseRtpPacket ->
RtpVideoStreamReceiver::OnReceivedPayloadData -> PacketBuffer::InsertPacket ->
RtpVideoStreamReceiver::OnReceivedFrame -> RtpFrameReferenceFinder::ManageFrame ->
VideoReceiveStream::OnCompleteFrame -> FrameBuffer::InsertFrame
  • SignalReadPacket(rtc::AsyncPacketSocket*, const char*, size_t, const rtc::SocketAddress&, const rtc::PacketTime&)
    rtc::Socket收到数据会通过信号槽SignalReadPacket回调我们,此时我们拿到了二进制数据和数据大小以及Socket收到此数据的时间戳。在我们把数据交付给WebRtcVideoChannel之前,我们需要通过AddRecvStream告诉它哪个ssrc的数据将会过来,让它提前准备好一个WebRtcVideoReceiveStream

  • WebRtcVideoChannel::OnPacketReceived(rtc::CopyOnWriteBuffer* packet, const rtc::PacketTime& packet_time)

    1. 收到数据之后标准它是视频数据,然后把它交付给Call::DeliverPacket
    2. 当第一次交付给Call::DeliverPacket失败的时候,尝试去获取这个包的ssrc和payload_type,根据payload_type判断它是不是rtx、red_rtx或者ulpfec,如果是则直接返回,如果不是它可能是一个未能识别的关联类型。之后为此ssrc创建一个默认的RecvStream重新交付给Call::DeliverPacket
  • Call::DeliverPacket(MediaType media_type, const uint8_t* packet, size_t length, const PacketTime& packet_time)
    把数据交付给Call模块,它判断此包RTP还是RTCP包,根据类型交付给DeliverRtp还是DeliverRtcp处理

  • Call::DeliverRtp(MediaType media_type, const uint8_t* packet, size_t length, const PacketTime& packet_time)
    数据到了这里,已经确保了它是一个RTP包。使用Call::ParseRtpPacket拿到RTP包的信息,并把数据封装为RtpPacketReceived数据结构,这个数据结构包含的信息有:

    1. recovered_:是否是RTX或者FEC包装的RTP包
    2. arrival_time_ms_:数据包到达时间(上一个函数传递过来的)
    3. payload_type_frequency_:此RTP包的负载类型,不同的负载类型对应不同的编解码格式,这个负载类型是在通信之前已经知道的
    4. 父类webrtc::rtp::Packet:这个数据结构会解析整个RTP,可以拿到的信息有:marker_(标志位)、payload_type_(媒体的负载类型)、padding_size_(此RTP包中有多少填充数据)、sequence_number_(包序号)、timestamp_(采集时间戳)、ssrc_(此包的唯一标识)、payload_offset_(媒体数据偏移此RTP包的位置)、payload_size_(媒体数据的大小)、extension_entries_(扩展类型以及对应的值)、extensions_size_(有多少个扩展)和buffer_(完整的RTP包内容)。也可以看看我的另外一篇文章WebRTC之RTP包
  • RtpStreamReceiverController::OnRtpPacket(const RtpPacketReceived& packet)
    数据包过来以后它没有做任何处理直接交付给了RtpDemuxer

  • RtpDemuxer::OnRtpPacket(const RtpPacketReceived& packet)
    它根据此RTP的扩展mid和rsid以及ssrc信息找到一个能处理此RTP的,实际上WebRTC并不支持mid、rsid,它只支持ssrc的方式,ssrc只可能是media ssrc和rtx ssrc,具体可以看VideoReceiveStream的构造函数。所以数据交付给RtpVideoStreamReceiver::OnRtpPacket

  • RtpVideoStreamReceiver::OnRtpPacket(const RtpPacketReceived& packet)

    1. 从webrtc::rtp::Packet中拿到它的头部信息,并封装到webrtc::RTPHeader数据结构中
    2. 添加此视频payload的频率,视频只有一个频率90000
    3. 判断此包是否是按顺序的,详细可以阅读 RtpVideoStreamReceiver::IsPacketInOrder,原理就是判断包序号sequence_number_是否比上一个收到的要大,要做好边界处理,所以[1 3 5]这样的顺序也是InOrder的
      再把以上处理后的数据交付给ReceivePacket
  • RtpVideoStreamReceiver::ReceivePacket(const uint8_t* packet, size_t packet_length, const RTPHeader& header, bool in_order)

    1. 通过RTPPayloadRegistry::IsEncapsulated判断此RTP包是否数据RED或者RTX类型,如果属于把此数据包交付给ParseAndHandleEncapsulatingHeader处理,它会根据不同的类型做去壳处理,最后也是交付给RtpReceiverImpl::IncomingRtpPacket,直接跳过以下两步
    2. 通过webrtc::RTPHeader的headerLength我们得到了封包数据的起始位置payload和大小payload_length(此大小包含了填充信息padding)
    3. 通过RTPPayloadRegistry::GetPayloadSpecifics可以知道此包的视频编解码类型以及对应的profile,并存储到webrtc::PayloadUnion的数据结构中。我们需要在数据过来之前就通过RTPPayloadRegistry::RegisterReceivePayload告诉它这些信息,注册信息在信令交付的时候传递过来的。
      把以上处理后的数据交付给RtpReceiverImpl::IncomingRtpPacket
  • RtpReceiverImpl::IncomingRtpPacket(const RTPHeader& rtp_header, const uint8_t* payload, size_t payload_length, PayloadUnion payload_specific, bool in_order)

    1. 调用CheckSSRCChanged检查当前SSRC是否改变,如果改变把改变后的信息通知对应的模块
    2. 调用CheckPayloadChanged检查Payload信息是否改变(编辑解码类型、采样率、通道等),如果改变了并返回对应的负载类型信息和判断是否是red类型,如果是空包直接返回成功,如果是未支持的负载类型则打印不支持
    3. 使用webrtc::WebRtcRTPHeader数据结构记录RTP包头部信息
    4. 或许音频Level值,如果不存在默认为0,并更新Level
    5. 判断此包是否是一个帧的第一个包,原理也很简单:如果未接收过任何包,则此包是是第一个包;如果已经接收过包,并且当前包序号等上一个接收到的包需要+1并且这两个包的时间戳不同
      把上面处理得结果交付给RTPReceiverVideo::ParseRtpPacket,如果RTPReceiverVideo::ParseRtpPacket处理成功,则把这个包当做最后一个受到的包,并记录一些信息:last_receive_time_(最近收到的包的时间戳)、last_received_payload_length_(最后一次收到的封包数据大小,不包含填充信息)、如果包是有序的还会修改last_received_sequence_number_(最后一个包序号)、如果包的时间戳不同还会修改last_received_timestamp_(最后一个包的时间戳)以及last_received_frame_time_ms_(最近收到的一个帧的时间戳),这些信息在前面会用到
  • RTPReceiverVideo::ParseRtpPacket(WebRtcRTPHeader* rtp_header, const PayloadUnion& specific_payload, bool is_red, const uint8_t* payload, size_t payload_length, int64_t timestamp_ms, bool is_first_packet)
    此函数主要是根据RTP的扩展信息和解析封包格式RtpDepacketizer(这里主要看RtpPacketizerH264)来完善数据结构webrtc::WebRtcRTPHeader,关于RtpPacketizerH264可以看看我的另外一篇文章WebRTC之H.264打包RtpPacketizerH264

    1. 通过PayloadUnion获取到编解码类型,通过paddingLength知道实际包长度payload_data_length
    2. 根据编解码器类型创建一个解包器RtpDepacketizer,并把解包后的数据存放在webrtc::RtpDepacketizer::ParsedPayload数据结构中,此数据结构可以知道的信息有:payload(真正的媒体数据指针地址)、payload_length(真正的媒体数据长度)、frame_type(帧类型,是关键帧还是片段(对于H.264也就是P帧))和type(数据结构是webrtc::RTPTypeHeader,对于视频来说也就是webrtc::RTPVideoHeader),这个type数据结构中包含了视频信息:width(视频宽)、height(视频高)、rotation(视频旋转角度)、playout_delay(延迟多久渲染此帧)、content_type(视频内容,相机或者屏幕等)、video_timing(记录了此包从采集到发送时经历的各个环节的耗时)、is_first_packet_in_frame(是否是一个帧最开始的一个包)、codec(编解码类型)和codecHeader(此编码的头部信息,数据结构是webrtc::RTPVideoTypeHeader,对H.264来说对应的数据结构是webrtc::RTPVideoHeaderH264)。webrtc::RTPVideoHeaderH264数据结构的信息包含有:nalu_type(类型可以参考webrtc::H264::NaluType)、packetization_type(打包类型,有三种单个帧kH264SingleNalu、聚合帧kH264StapA、片段kH264FuA)、nalus(NaluInfo类型,对于聚合帧来说存在多个NALU)和packetization_mode(打包模式,单个SingleNalUnit和非单个NonInterleaved)
      说明:rotation、content_type、video_timing和playout_delay都是通过RTP扩展拿到的,编解码信息是通过解码器拿到的
      把以上处理得到的信息交付给RtpVideoStreamReceiver::OnReceivedPayloadData
  • RtpVideoStreamReceiver::OnReceivedPayloadData(const uint8_t* payload_data, size_t payload_size, const WebRtcRTPHeader* rtp_header)

    1. 获取NTP时间戳,这是一个估算的时间戳,需要根据RTCP的信息来估算的具体看webrtc::RemoteNtpTimeEstimator。并使用参数来封装一个webrtc::VCMPacket数据结构
    2. 告诉NACK模块收到一个包,这里不做深入,后面会详细解读这个模块
    3. 这里又会记录此包到达这个函数时的时间戳receive_time_ms(也是一个单调递增的时间戳)
    4. 判断媒体数据长度,如果长度等于0,那么它是一个空包,仅仅需要记录它的包序号即可,调用RtpFrameReferenceFinder::PaddingReceived和webrtc::video_coding::PacketBuffer::PaddingReceived告诉这两个模块此空包的序号
    5. 调用H264SpsPpsTracker::CopyAndFixBitstream去检查帧是否正确,是否需要重新请求关键帧,如果它是一个正确的帧,并根据打包类型以及是否是一帧的一个包把媒体数据提取出来并封装为Annex B类型的数据(在数据的最前面添加start code [0 0 0 0]),并存储在调用参数中
      再把以上处理后的数据交付给PacketBuffer::InsertPacket
  • PacketBuffer::InsertPacket(VCMPacket* packet)
    这个类正如它的名字,用于管理包的,这个函数是插入一个新的包,每次插入都会检查是否能组成一个新的帧,如果能组成一个新的帧则通过回调函数OnReceivedFrame告诉RtpVideoStreamReceiver对象。

    1. 新包会存储到data_buffer_中,它是一个vector数据结构,为了节约内存它采用一步步扩展的方式,有一个最大包max_buffer_size和起始包start_buffer_size,一开始最创建起始包大小的数组,当插入的时候发现数组已经满了再对数组进行扩容。
    2. 每次插入一个新包都会调用UpdateMissingPackets检查缺失的包
    3. 每次插入一个新包都会调用FindFrames检查是否能组成一个新的帧,如果可以则返回该帧的信息,该信息使用数据结构webrtc::video_coding::RtpFrameObject存储,该数据结构内存储的信息有:packet_buffer_(PacketBuffer的引用指针)、frame_type_(帧类型,关键帧与否)、codec_type_(编码类型)、first_seq_num_(该帧的第一个包序号)、last_seq_num_(该帧的最后一个包序号)、timestamp_(该帧的时间戳,RTP中获取的)、received_time_(收到此帧的时间戳,单调递增时间)和times_nacked_(此帧中最大NACK次数的包是多少个),RtpFrameObject的父类是FrameObject,所以它还包含了父类的信息:timestamp(采集时间戳,就是RTP中的时间戳)、num_references(帧的引用次数,对于H.264来说只有0和1两种情况,I帧不需要任何引用所以是0,P帧是1)、_buffer(该帧指针,在构造的时候它会拷贝first_seq_num_到last_seq_num_到该指针)、_length(该帧的实际长度)、_size(该指针的实际长度,H.264的_size=_length+8)
      再把以上处理后的数据交付给PacketBuffer::InsertPacket
  • RtpVideoStreamReceiver::OnReceivedFrame(std::unique_ptr<video_coding::RtpFrameObject> frame)

    1. 判断第一个收到的帧是否是关键帧,如果不是关键帧则请求关键帧
    2. 如果该帧存在通过NACK的包,则需要告诉webrtc::VCMTiming模块,调用webrtc::VCMTiming::IncomingTimestamp
      做了以上记录和错误判断之后交付给RtpFrameReferenceFinder::ManageFrame
  • RtpFrameReferenceFinder::ManageFrame(std::unique_ptr<video_coding::RtpFrameObject> frame)
    RtpFrameReferenceFinder保证GOP内的帧有序

    1. 数据过来之后会先判断此帧是否是已经被清理过的帧,如果是则直接丢弃(通过比较已经丢弃的帧序号来判断,如果新加入的帧第一个包序号在已经丢弃的帧序号之前则丢弃)
    2. 之后调用ManageFrameInternal来判断该如何处理该帧,我们这里是以H.264为例,所以相当于调用ManageFrameGeneric,返回值有三种情况:kStash(缓存)、kHandOff(可交付)和kDrop(丢弃)
    3. kStash:P帧在I帧之前先到达;一个不连续的P帧(例如这样的情况I P1 P2,P2先于P1到达)
    4. kHandOff:GOP中的第一个I帧;连续的P帧
    5. kDrop:该帧在GOP之前
      假设到这一步我们拿到了一个关键帧或者一个连续的P帧,然后我们通过CallBack把这个帧交付给VideoReceiveStream::OnCompleteFrame
  • VideoReceiveStream::OnCompleteFrame(std::unique_ptr<video_coding::FrameObject> frame)

    1. 收到一个有序的帧后会把它传给FrameBuffer,让FrameBuffer来保证帧的有序性
    2. 当保证前面的帧已经完全收到时,会通知NACK模块去ClearUpTo当前帧的包以及之前的包
  • FrameBuffer::InsertFrame
    FrameBuffer保证GOP有序,当前GOP内的P帧还未完全受到,下一帧的I帧已经到了,那么会丢弃当前GOP内的后续的P帧

解码流程(以H.264为例)

VideoReceiveStream::DecodeThreadFunction -> VideoReceiveStream::Decode ->
FrameBuffer::NextFrame -> VideoReceiver::Decode -> VideoReceiver::Decode ->
VCMGenericDecoder::Decode -> H264DecoderImpl::Decode ->
VCMDecodedFrameCallback::Decoded -> VideoStreamDecoder::FrameToRender ->
WebRtcVideoChannel::WebRtcVideoReceiveStream::OnFrame -> 我们自己的Sink(WebRtcVideoChannel::SetSink)
  • VideoReceiveStream::DecodeThreadFunction(void* ptr)
    这个是解码线程函数,当线程启动的时候会自动执行此函数,此函数就会循环执行Decode函数直到它返回false

  • VideoReceiveStream::Decode()

    1. 它会调用FrameBuffer::NextFrame获取到一帧可用的帧,此调用会返回三种情况:kFrameFound(找到一个可用的帧)、kTimeout(超时)和kStopped(停止)
    2. 当获得一帧可用的帧时,它会调用VideoReceiver::Decode进行解码
  • FrameBuffer::NextFrame(int64_t max_wait_time_ms, std::unique_ptr* frame_out, bool keyframe_required)
    从帧缓存中找到一帧可用的帧,如果没有则使用event进行等待,当超过max_wait_time_ms时间还没等到可用的帧是返回kTimeout

  • VideoReceiver::Decode(const webrtc::VCMEncodedFrame* frame)

    1. 解码前的数据回调,它会回调到VideoReceiveStream::OnEncodedImage,主要是做数据统计和写文件用的,这里不展开
    2. 然后数据交付给下一个Decode(const VCMEncodedFrame& frame)函数
  • VideoReceiver::Decode(const VCMEncodedFrame& frame)

    1. 通过VCMCodecDataBase::GetDecoder获取一个可用的解码器(根据payload_type确定解码器类型),如果对应的解码器不存在则创建一个(如果存在扩展解码器优先使用扩展解码器)并初始化解码器,如果该ssrc对应的payload_type改变则先释放之前的解码器并创建新的解码器并初始化新的解码器,并注册一个回调
    2. 通过上面的步骤拿到了一个通用解码器对象(VCMGenericDecoder),通用解码器对象里面包含了一个真正的解码器对象(webrtc::VideoDecoder)
    3. 调用通用解码器对象的解码函数,并把当前时间(一个单调递增的时间戳)传递给它(记录开始解码时间)
  • VCMGenericDecoder::Decode(const VCMEncodedFrame& frame, int64_t nowMs)

    1. 解码前对帧信息进行保存,这些信息是要在解码后传递给解码后的数据结构的
    2. 调用真正的解码器对象进行解码,这里我们以软解的实现为例H264DecoderImpl
  • H264DecoderImpl::Decode(const EncodedImage& input_image, bool, const RTPFragmentationHeader*, const CodecSpecificInfo* codec_specific_info, int64_t)

    1. 解码器前对帧的有效性进行判断:是不是空帧、是否是H.264编码类型,帧指针的大小够不够
    2. 通过上面判断确定它是我们想要的数据之后调用ffmpeg进行解码,ffmpeg解码后的数据是YUV类型的,ffmpeg部分不展开说
    3. 通过H264BitstreamParser::ParseBitstream函数解析解码前的数据,最终目的是获取qp值,这部分也不展开说,具体可以阅读此函数,此函数配合H.264的ios文档阅读会更容易理解
    4. 判断解码后分辨率是否大于视频的真实宽度(这个是由于解码器的align决定的),如果存在那么需要做裁剪
    5. 把解码后的数据封装为webrtc::VideoFrame数据结构回调VCMDecodedFrameCallback::Decoded,并带上解码后时间戳和qp值
  • VCMDecodedFrameCallback::Decoded(VideoFrame& decodedImage, Optional<int32_t> decode_time_ms, Optional<uint8_t> qp)

    1. 通过解码帧的时间戳可以拿到解码器的帧信息,根据解码后的时间和信息更新VCMTiming(我对这部分也不了解)
    2. 设置FrameToRender的timestamp_us和rotation之后交付给VideoStreamDecoder::FrameToRender
  • VideoStreamDecoder::FrameToRender(VideoFrame& video_frame, Optional<uint8_t> qp, VideoContentType content_type)

    1. 把解码后的数据交付给接收统计模块ReceiveStatisticsProxy::OnDecodedFrame,这不是我关心的部分,不展开
    2. 之后交付给WebRtcVideoChannel::WebRtcVideoReceiveStream::OnFrame去渲染
  • WebRtcVideoChannel::WebRtcVideoReceiveStream::OnFrame(const webrtc::VideoFrame& frame)

    1. 估算远端发送的ntp时间estimated_remote_start_ntp_time_ms_
    2. 直接交付给渲染模块,这部分不展开,这部分是与平台相关的

请求丢失的包 - NackModule

此模块的任务是在适当的时候向发送端请求丢失的包列表(通过NackSender),当丢失的包过多的时候直接请求关键帧(通过KeyFrameRequestSender)

class NackSender {
 public:
  virtual void SendNack(const std::vector<uint16_t>& sequence_numbers) = 0;
 protected:
  virtual ~NackSender() {}
};

NackModule::NackModule(Clock* clock, NackSender* nack_sender, KeyFrameRequestSender* keyframe_request_sender) {}

收到一个包 - OnReceivedPacket

从网络中收到一个包,这里只关注包序号

int NackModule::OnReceivedPacket(const VCMPacket& packet) {
  rtc::CritScope lock(&crit_);
  uint16_t seq_num = packet.seqNum;
  bool is_keyframe = packet.is_first_packet_in_frame && packet.frameType == kVideoFrameKey; // 此包属于关键帧的第一个包才标记为关键帧

  // 第一个包不需要进行后续的处理,只需要记录最新的一个包序号即可,如果该包属于关键帧则把该包序号加入到关键帧列表集合中
  if (!initialized_) {
    newest_seq_num_ = seq_num;
    if (is_keyframe)
      keyframe_list_.insert(seq_num);
    initialized_ = true;
    return 0;
  }

  // 收到重复的包,不做任何处理
  if (seq_num == newest_seq_num_)
    return 0;

  if (AheadOf(newest_seq_num_, seq_num)) {
    // 收到乱序包
    auto nack_list_it = nack_list_.find(seq_num); // 在nack列表中查找此包
    int nacks_sent_for_packet = 0; // 记录此乱序包进行了几次nack请求
    if (nack_list_it != nack_list_.end()) { // 属于nack列表中的包
      nacks_sent_for_packet = nack_list_it->second.retries;
      nack_list_.erase(nack_list_it);
    }
    return nacks_sent_for_packet;
  }
  // 如果是顺序,把中间的包加入到nack_list_,例如newest_seq_num_是5,seq_num是8,那么中间确实了6和7两个包,把这两个包加入到nack_list_中
  AddPacketsToNack(newest_seq_num_ + 1, seq_num);
  newest_seq_num_ = seq_num;

  // 如果是关键帧,加入到关键帧列表集合中
  if (is_keyframe)
    keyframe_list_.insert(seq_num);

  // 移除此关键帧之前的包
  auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);
  if (it != keyframe_list_.begin())
    keyframe_list_.erase(keyframe_list_.begin(), it);

  // 找到满足条件的缺失的包并发送给发送端,如果当前的包不是连续的,那么必定会发送一次nack请求(详细阅读GetNackBatch)
  std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);
  if (!nack_batch.empty())
    nack_sender_->SendNack(nack_batch);

  return 0;
}

找到满足条件缺失的包列表 - GetNackBatch

enum NackFilterOptions { kSeqNumOnly, kTimeOnly, kSeqNumAndTime };

std::vector<uint16_t> NackModule::GetNackBatch(NackFilterOptions options) {
  bool consider_seq_num = options != kTimeOnly; // 包序号过滤
  bool consider_timestamp = options != kSeqNumOnly; // 时间戳过滤
  int64_t now_ms = clock_->TimeInMilliseconds();
  std::vector<uint16_t> nack_batch;
  auto it = nack_list_.begin();
  while (it != nack_list_.end()) {
     // 使用包序号过滤 && 该缺失的包还未发送过请求 && 该缺失的包在最新的包之前
    if (consider_seq_num && it->second.sent_at_time == -1 && AheadOrAt(newest_seq_num_, it->second.send_at_seq_num)) {
      nack_batch.emplace_back(it->second.seq_num);
      ++it->second.retries;  // 重发次数+1
      it->second.sent_at_time = now_ms; // 记录请求重发时间
      if (it->second.retries >= kMaxNackRetries) { // 当一个包请求次数超过10次时从nack列表中移除,之后就不再请求了
        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;
    }
    // 使用时间戳过滤 && 距离上一次发送时间超过一次rtt(回环时间)
    if (consider_timestamp && it->second.sent_at_time + rtt_ms_ <= now_ms) {
      nack_batch.emplace_back(it->second.seq_num);
      ++it->second.retries; // 重发次数+1
      it->second.sent_at_time = now_ms; // 记录请求重发时间
      if (it->second.retries >= kMaxNackRetries) { // 当一个包请求次数超过10次时从nack列表中移除,之后就不再请求了
        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;
}

隔一段时间检查一次是否发送丢包请求 - Process

隔一段时间检查一下nack列表中的是否到达,要不要向发送端发送重传请求
关于Module可以阅读WebRTC之Module

// 更新回环时间,这个时间是根据SR得到的
void NackModule::UpdateRtt(int64_t rtt_ms) {
  rtc::CritScope lock(&crit_);
  rtt_ms_ = rtt_ms;
}
// 返回下一次执行Process()函数的时间
int64_t NackModule::TimeUntilNextProcess() {
  return std::max<int64_t>(next_process_time_ms_ - clock_->TimeInMilliseconds(), 0);
}
// 线程会
void NackModule::Process() {
  if (nack_sender_) {
    std::vector<uint16_t> nack_batch;
    {
      rtc::CritScope lock(&crit_);
      nack_batch = GetNackBatch(kTimeOnly);
    }

    if (!nack_batch.empty())
      nack_sender_->SendNack(nack_batch);
  }

  // 计算下一次执行此函数的时间,第一次执行,那么下一次的间隔就是直接加上间隔时间即可
  // 之后需要根据实际处理时间计算下一次的间隔,如果CPU可能比较忙,所以可能会出现延后处理的情况
  int64_t now_ms = clock_->TimeInMilliseconds();
  if (next_process_time_ms_ == -1) {
    next_process_time_ms_ = now_ms + kProcessIntervalMs;
  } else {
    next_process_time_ms_ = next_process_time_ms_ + kProcessIntervalMs + (now_ms - next_process_time_ms_) / kProcessIntervalMs * kProcessIntervalMs;
  }
}

组帧 - PacketBuffer

存储所有的包,当有能够组成完整帧的时候把该帧回调出去,当该帧解码成功以后调用ClearTo删除该帧腾出空间。

// A received frame is a frame which has received all its packets.
class OnReceivedFrameCallback {
 public:
  virtual ~OnReceivedFrameCallback() {}
  virtual void OnReceivedFrame(std::unique_ptr<RtpFrameObject> frame) = 0;
};
class PacketBuffer {
 public:
  static rtc::scoped_refptr<PacketBuffer> Create(Clock* clock, size_t start_buffer_size, size_t max_buffer_size, OnReceivedFrameCallback* frame_callback);
  virtual ~PacketBuffer();
  virtual bool InsertPacket(VCMPacket* packet);
  void ClearTo(uint16_t seq_num);
  void Clear();
  void PaddingReceived(uint16_t seq_num);
 private:
  // Buffer that holds the inserted packets.
  std::vector<VCMPacket> data_buffer_ GUARDED_BY(crit_);
  // Buffer that holds the information about which slot that is currently in use and information needed to determine the continuity between packets.
  std::vector<ContinuityInfo> sequence_buffer_ GUARDED_BY(crit_);
};

新包插入 - InsertPacket

bool PacketBuffer::InsertPacket(VCMPacket* packet) {std::vector<std::unique_ptr<RtpFrameObject>> found_frames;
{
    rtc::CritScope lock(&crit_);
    uint16_t seq_num = packet->seqNum;
    size_t index = seq_num % size_; // 通过取余得出当前包应该插入到数组的那个位置

    if (!first_packet_received_) {
      first_seq_num_ = seq_num; // 记录第一个收到的包序号
      first_packet_received_ = true; // 标记已经收到了第一个包
    } else if (AheadOf(first_seq_num_, seq_num)) { // 乱序包
      // 如果标记删除第一包之前的数据,那么删除此包直接返回
      if (is_cleared_to_first_seq_num_) {
        delete[] packet->dataPtr;
        packet->dataPtr = nullptr;
        return false;
      }
      first_seq_num_ = seq_num; // 把新进入的包序号作为第一个包序号
    }
    // 数组中这个位置已经被使用
    if (sequence_buffer_[index].used) {
      // 如果是序号相同的包,删除此包,直接返回
      if (data_buffer_[index].seqNum == packet->seqNum) {
        delete[] packet->dataPtr;
        packet->dataPtr = nullptr;
        return true;
      }

      // 如果序号不同,就是包已经满了,需要扩容
      while (ExpandBufferSize() && sequence_buffer_[seq_num % size_].used) {
      }
      index = seq_num % size_; // 根据新数组得到一个新的数组索引下标

      // 如果数组还是满的,那么就不存这个包了,直接返回
      if (sequence_buffer_[index].used) {
        delete[] packet->dataPtr;
        packet->dataPtr = nullptr;
        return false;
      }
    }
    // 把此包信息存入到数组sequence_buffer_中,用于判断是否得到一个完整的帧,此数组的大小和data_buffer_一样大,都是动态扩容的
    sequence_buffer_[index].frame_begin = packet->is_first_packet_in_frame;
    sequence_buffer_[index].frame_end = packet->markerBit;
    sequence_buffer_[index].seq_num = packet->seqNum;
    sequence_buffer_[index].continuous = false;
    sequence_buffer_[index].frame_created = false;
    sequence_buffer_[index].used = true;
    data_buffer_[index] = *packet; // 把此包存入到数组data_buffer_中
    packet->dataPtr = nullptr;
    // 更新丢失的包
    UpdateMissingPackets(packet->seqNum);

    int64_t now_ms = clock_->TimeInMilliseconds();
    last_received_packet_ms_ = rtc::Optional<int64_t>(now_ms);
    if (packet->frameType == kVideoFrameKey)
      last_received_keyframe_packet_ms_ = rtc::Optional<int64_t>(now_ms);
     // 遍历现有的列表中是否有连续的包能组成一个完整的帧
    found_frames = FindFrames(seq_num);
  }
  // 存在这样的帧,则回调它
  for (std::unique_ptr<RtpFrameObject>& frame : found_frames)
    received_frame_callback_->OnReceivedFrame(std::move(frame));

  return true;
}

更新丢失的包 - UpdateMissingPackets

  • 更新丢失的包,例如上一个收到的包是9,当前收到的包是12,那么10和11这两个包就丢失了,记录到missing_packets_中
  • 如果中间丢失的包过多,最多也记录1000个丢失的包,例如上一个收到的包是9,当前收到的包是2000,那么也只记录[2000 - 1000, 2000)之间的包
void PacketBuffer::UpdateMissingPackets(uint16_t seq_num) {
  if (!newest_inserted_seq_num_) // 如果还未设置最新插入的序号newest_inserted_seq_num_,则使用当前序号
    newest_inserted_seq_num_ = rtc::Optional<uint16_t>(seq_num);

  const int kMaxPaddingAge = 1000;
  // 包序号是顺序的,那么就判断是否存在丢包的情况
  if (AheadOf(seq_num, *newest_inserted_seq_num_)) {
    // 只保留[seq_num - 1000, 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);

    // 丢失的包太多了,那么也只记录seq_num - 1000的包
    if (AheadOf(old_seq_num, *newest_inserted_seq_num_))
      *newest_inserted_seq_num_ = old_seq_num;

    // 把丢失的包加入到missing_packets_中
    ++*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);
  }
}

判断包的连续性 - PotentialNewFrame

判断能不能组成新的一帧,判断依据是:当前包是一个帧的第一个包或者前一个包是连续的

bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const {
  size_t index = seq_num % size_;
  int prev_index = index > 0 ? index - 1 : size_ - 1;

  if (!sequence_buffer_[index].used)
    return false;
  if (sequence_buffer_[index].seq_num != seq_num)
    return false;
  if (sequence_buffer_[index].frame_created)
    return false;
  if (sequence_buffer_[index].frame_begin)
    return true;
  if (!sequence_buffer_[prev_index].used)
    return false;
  if (sequence_buffer_[prev_index].frame_created)
    return false;
  if (sequence_buffer_[prev_index].seq_num !=
      static_cast<uint16_t>(sequence_buffer_[index].seq_num - 1)) {
    return false;
  }
  if (sequence_buffer_[prev_index].continuous)
    return true;

  return false;
}

找到完整的帧 - FindFrames

  • 从当前seq_num开始往前查找,判断包是否连续到一帧开始的包,如果存在说明收到完整的一帧
  • H.264一帧的开始依据两个连续的包,并且包的时间戳不同来判断
std::vector<std::unique_ptr<RtpFrameObject>> PacketBuffer::FindFrames(uint16_t seq_num) {
  std::vector<std::unique_ptr<RtpFrameObject>> found_frames;
  for (size_t i = 0; i < size_ && PotentialNewFrame(seq_num); ++i) {
    size_t index = seq_num % size_;
    sequence_buffer_[index].continuous = true;

    // 如果该帧所有的包都是连续的,找到第一个帧,并使用这些包构造一个RtpFrameObject对象
    if (sequence_buffer_[index].frame_end) {
      size_t frame_size = 0; // 记录该帧的大小
      int max_nack_count = -1; // 记录该帧中的包最大nack次数
      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; // 判断是否是h264编码类型
      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; // 标记一下该包已经被用于构造帧了
        // 对于非H.264编码类型,这里认可frame_begin作为一帧的开始位置,对于H.264编码类型需要根据其他的判断
        if (!is_h264 && sequence_buffer_[start_index].frame_begin)
          break;
        // 如果是H.264编码类型,并且不是关键帧,我们需要判断它是不是IDR帧,IDR帧是一个特别的关键帧
        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;
        // 在H.264中,不存在任何位用于表示一帧开始的,这里只能通过rtp包中的时间戳不同来判断是否是一个帧的第一个包
        // 上一个包未使用或者上一个包跟这个包的时间戳不同,那么这个包就是这个帧的第一个包
        if (is_h264 &&  (!sequence_buffer_[start_index].used || data_buffer_[start_index].timestamp != frame_timestamp)) {
          break;
        }
        // 包序号往前挪一个位置
        --start_seq_num;
      }

      // 属于H.264帧 && 不是关键帧 && start_seq_num属于丢失的包(说明该帧的包不完整)
      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; // 重新把该帧的frame_created标记为false
          start_index = (start_index + 1) % size_;
        }

        return found_frames;
      }
      // 能到这里说明该seq_num包前面的所有包已经能够组成完整的帧了,所以可以把该包之前的missing_packets_删掉了
      missing_packets_.erase(missing_packets_.begin(), missing_packets_.upper_bound(seq_num));
      // 使用[start_seq_num, seq_num]构造一个RtpFrameObject对象,这里会分配一个frame_size的空间,把所有包内的数据拷贝到这个空间去
      found_frames.emplace_back(new RtpFrameObject(this, start_seq_num, seq_num, frame_size,max_nack_count, clock_->TimeInMilliseconds()));
    }
    ++seq_num;
  }
  return found_frames;
}

删除已经解码的包 - ClearTo

解码之后(具体看RtpVideoStreamReceiver::FrameDecoded)我们就可以把此帧及之前的包全面删除掉了

void PacketBuffer::ClearTo(uint16_t seq_num) {
  rtc::CritScope lock(&crit_);
  // 已经被标记删除了 && 并且seq_num在first_seq_num_之前
  if (is_cleared_to_first_seq_num_ && AheadOf<uint16_t>(first_seq_num_, seq_num)) {
    return;
  }

  // 所有的包已经被删除了,无需做任何操作
  if (!first_packet_received_)
    return;

  // 删除从first_seq_num_到seq_num的包
  ++seq_num;
  size_t diff = ForwardDiff<uint16_t>(first_seq_num_, seq_num);
  size_t iterations = std::min(diff, size_);
  for (size_t i = 0; i < iterations; ++i) {
    size_t index = first_seq_num_ % size_;
    RTC_DCHECK_EQ(data_buffer_[index].seqNum, sequence_buffer_[index].seq_num);
    if (AheadOf<uint16_t>(seq_num, sequence_buffer_[index].seq_num)) {
      delete[] data_buffer_[index].dataPtr;
      data_buffer_[index].dataPtr = nullptr;
      sequence_buffer_[index].used = false;
    }
    ++first_seq_num_;
  }

  // 把第一个包序号设置为当前包序号,因为前面的包已经删除掉了
  first_seq_num_ = seq_num;

  is_cleared_to_first_seq_num_ = true;
  missing_packets_.erase(missing_packets_.begin(), missing_packets_.upper_bound(seq_num)); // 把seq_num之前缺失的包移出列表
}

帧排序 - RtpFrameReferenceFinder

PacketBuffer会输出完整的帧,但是输出的帧可能是乱序的,这个类的作用就是对帧进行排序,输出顺序的帧。这些帧之间可能还会存在填充包
例如:输入的是I1 -> P1 -> P3 -> P2,那么它的输出是I1 -> P1,缓存P3,等待P2输入,输出P2 -> P3
例如:输入的是I1 -> P1_1 ->P1_2 ->P1_3 -> I2 -> P2_1 -> P1_4 -> P2_2,输出是 I1 -> P1_1 ->P1_2 ->P1_3 -> I2 -> P2_1 -> P2_2,P1_4在关键帧I2之后出现,此时I2已经解码了,所以丢弃P1_4

  • GOP(group of pictures)是指一组由I帧和P帧组成的图片集合,这个集合中只有一个关键帧,其他的非关键帧都要参考关键帧
  • 字段last_seq_num_gop_是一个map数据结构,用于存储关键帧列表,first是关键帧的最后一个序号,second是最后一个完整帧的最后一个数据包的序号。通过last_seq_num_gop_的upper_bound和lower_bound可以确定非关键帧属于哪一个关键帧

管理输入的帧 - ManageFrame

void RtpFrameReferenceFinder::ManageFrame(std::unique_ptr<RtpFrameObject> frame) {
  rtc::CritScope lock(&crit_);
  // 如果输入的帧序号在被清理帧序号之前,说明此帧太老了,不做任何处理
  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: // 回调该帧
      frame_callback_->OnCompleteFrame(std::move(frame));
      RetryStashedFrames();
      break;
    case kDrop: // 丢弃该帧
      break;
  }
}

管理输入的H.264帧 - ManageFrameGeneric

一组GOP类似一下的情况,有可能会有padding包

RtpFrameReferenceFinder::FrameDecision RtpFrameReferenceFinder::ManageFrameGeneric(RtpFrameObject* frame, int picture_id) {
  // H.264的picture_id固定是kNoPictureId,所以跳过这里
  if (picture_id != kNoPictureId) {
    if (last_unwrap_ == -1)
      last_unwrap_ = picture_id;

    frame->picture_id = unwrapper_.Unwrap(picture_id);
    frame->num_references = frame->frame_type() == kVideoFrameKey ? 0 : 1;
    frame->references[0] = frame->picture_id - 1;
    return kHandOff;
  }
   // 如果该帧是关键帧,那么把该帧加入到last_seq_num_gop_中,key是当前关键帧最后一个包序号,value默认也是当前关键帧最后一个包序号
  if (frame->frame_type() == kVideoFrameKey) {
    last_seq_num_gop_.insert(std::make_pair( frame->last_seq_num(), std::make_pair(frame->last_seq_num(), frame->last_seq_num())));
  }

  // 如果还未收到任何关键帧,先缓存此帧
  if (last_seq_num_gop_.empty())
    return kStash;

  // 清除太老的关键帧(关键帧之前的100帧),last_seq_num_gop_的key可能是这样的[0, 30, 60, 90, 120, 150],那么前100的被清除也就是清除[0, 30]这两个key
  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);
  }

  // 找到该帧间接引用的关键帧的最后一帧的最后一个序列号
  auto seq_num_it = last_seq_num_gop_.upper_bound(frame->last_seq_num());
  if (seq_num_it == last_seq_num_gop_.begin()) {
    LOG(LS_WARNING) << "Generic frame with packet range [" << frame->first_seq_num() << ", " << frame->last_seq_num() << "] has no GoP, dropping frame.";
    return kDrop;
  }
  seq_num_it--; // upper_bound返回的是last_seq_num_gop_[frame->last_seq_num()]迭代器的下一个指针

  // 如果是非关键帧,判断帧的连续性,如果不连续则存储该帧。判断依据是上一个帧最后一个包序号是否等于等钱帧第一个包序号减1
  uint16_t last_picture_id_gop = seq_num_it->second.first;
  uint16_t last_picture_id_with_padding_gop = seq_num_it->second.second;
  if (frame->frame_type() == kVideoFrameDelta) {
    uint16_t prev_seq_num = frame->first_seq_num() - 1;

    if (prev_seq_num != last_picture_id_with_padding_gop)
      return kStash;
  }

  RTC_DCHECK(AheadOrAt(frame->last_seq_num(), seq_num_it->first));

  frame->picture_id = frame->last_seq_num(); // 对于H.264帧,设置picture_id为该帧最后一个包的序号
  frame->num_references = frame->frame_type() == kVideoFrameDelta; // 设置该帧的引用帧个数,关键帧的引用个数是0,非关键帧的引用个数是1
  frame->references[0] = generic_unwrapper_.Unwrap(last_picture_id_gop); // 设置该帧的第一个引用帧的结束位置
  if (AheadOf<uint16_t>(frame->picture_id, last_picture_id_gop)) {
     // 如果该帧是连续的,更新该GOP最后一个帧为该帧
    seq_num_it->second.first = frame->picture_id;
    seq_num_it->second.second = frame->picture_id;
  }

  last_picture_id_ = frame->picture_id; // 更新最新的pid
  UpdateLastPictureIdWithPadding(frame->picture_id); // 更新填充状态
  frame->picture_id = generic_unwrapper_.Unwrap(frame->picture_id); // 设置当前帧的pid为Unwrap形式
  return kHandOff;
}

UpdateLastPictureIdWithPadding

这个函数主要是更新last_picture_id_with_padding_gop,这个也就是字段last_seq_num_gop_[key_frame].second.second的值

void RtpFrameReferenceFinder::UpdateLastPictureIdWithPadding(uint16_t seq_num) {
  auto gop_seq_num_it = last_seq_num_gop_.upper_bound(seq_num); // 获取该包序号的下一个I帧的gop表

  // 该帧在gop列表之前,不属于任何gop,所以不做任何处理
  if (gop_seq_num_it == last_seq_num_gop_.begin())
    return;
  --gop_seq_num_it; // 获取上一个gop表,也就是该帧所属的gop表
  // 填充包的起始序号
  uint16_t next_seq_num_with_padding = gop_seq_num_it->second.second + 1;
   // 在填充包缓存列表stashed_padding_中查找第一个大于等于next_seq_num_with_padding的位置
  auto padding_seq_num_it = stashed_padding_.lower_bound(next_seq_num_with_padding);
  // padding指针未到头 && 填充列表中存在这样一个填充包next_seq_num_with_padding
  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; // 更新此帧的最近一个pid并携带padding的位置
    ++next_seq_num_with_padding; // 下一个连续的填充包序号
     // 从填充包缓存列表stashed_padding_中删除padding_seq_num_it,并指向下一个
    padding_seq_num_it = stashed_padding_.erase(padding_seq_num_it);
  }

  if (ForwardDiff(gop_seq_num_it->first, seq_num) > 10000) {
    RTC_DCHECK_EQ(1ul, last_seq_num_gop_.size());
    last_seq_num_gop_[seq_num] = gop_seq_num_it->second;
    last_seq_num_gop_.erase(gop_seq_num_it);
  }
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值