前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WebRTC源码阅读——视频组帧

WebRTC源码阅读——视频组帧

原创
作者头像
媛儿
修改2021-08-02 15:24:49
1.9K0
修改2021-08-02 15:24:49
举报

本文分析了Google WebRTC 视频组帧的相关源码,给出了视频组帧的处理流程分析,为避免文章内容过多,文中对于关键函数的分析仅给出关键内容的说明,没有贴完整的源代码。文中所分析内容均基于WebRTC M86版本。

视频组帧

1.概括

组帧:视频一帧数据往往被拆分为多个packet进行发送,组帧是将接收到的packets重组为视频帧。组帧的关键在于找到视频帧的起始与终止packet。对于h264编码的视频帧,rtp传输时没有明确的起始标志,webrtc在处理时以判断连续序列号的时间戳是否相同为依据,若不相同则认为找到了视频帧的起始packet。视频帧的结束标识为rtp包的header中的Mark标志位。对于vp8、vp9则可以从rtp包中解析到明确的帧开始与结束标识符。组帧结束后,拿到完整的视频帧数据,之后对该视频帧数据进行参考帧信息设置,随后送入frameBuffer,以便从中取帧进行解码。

2.关键函数说明

本文内容着重分析webrtc源码中的rtp_video_stream_receiver2.cc、packet_buffer.cc文件的组帧部分。

RtpVideoStreamReceiver2接收到packet后,调用PacketBuffer::InsertPacket将packet进行存储并查找packet所在的帧以及之后帧的完整包数据,若找到该函数会返回完整视频帧的所有packets。若返回结果存在完整的视频帧,则继续由RtpVideoStreamReceiver2::OnInsertedPacket完成组帧。

packet_buffer.cc

packet_buffer使用buffer_记录了当前插入的所有packet,使用missing_packets_记录当前所丢失的包序号。

  • PacketBuffer::InsertResult PacketBuffer::InsertPacket( std::unique_ptr<PacketBuffer::Packet> packet)
代码语言:txt
复制
//利用packet的序列号计算出该packet存放于buffer_的位置
uint16_t seq_num = packet->seq_num;
size_t index = seq_num % buffer_.size();

//若buffer_[index]的值不为空,则按照序列号判断是否为同一packet,若是则返回,不是则不断扩充buffer_的容量,直到buffer_容量达到上限或packet待存放的位置未存储内容,若扩充达到上限依旧无法存放packet,则清除buffer_的内容后,直接返回。

 if (buffer_[index] != nullptr) {
    // Duplicate packet, just delete the payload.
    if (buffer_[index]->seq_num == packet->seq_num) {
      return result;
    }

    // The packet buffer is full, try to expand the buffer.
    while (ExpandBufferSize() && buffer_[seq_num % buffer_.size()] != nullptr) {
    }
    index = seq_num % buffer_.size();

    // Packet buffer is still full since we were unable to expand the buffer.
    if (buffer_[index] != nullptr) {
      // Clear the buffer, delete payload, and return false to signal that a
      // new keyframe is needed.
      RTC_LOG(LS_WARNING) << "Clear PacketBuffer and request key frame.";
      ClearInternal();
      result.buffer_cleared = true;
      return result;
    }
  }
  
//若buffer_[index]的值为空,则将packet存入buffer_,并且更新missing_packets_丢包记录,遍历buffer_找出当前packet所在的视频帧及其之后帧的所有packets。
  
packet->continuous = false;
buffer_[index] = std::move(packet);

UpdateMissingPackets(seq_num);

result.packets = FindFrames(seq_num);
  • void PacketBuffer::UpdateMissingPackets(uint16_t seq_num)
代码语言:txt
复制
//newest_inserted_seq_num_用于记录当前missing_packets_所插入的最新的序号,若seq_num比newest_inserted_seq_num_还要新,则说明seq_num与newest_inserted_seq_num_之间存在丢包。所以删除missing_packets_中从0开始到seq_num往前的1000个数据,并且不断更新newest_inserted_seq_num_值,并插入丢包的序列号到missing_packets_,直到newest_inserted_seq_num_为seq_num。

 const int kMaxPaddingAge = 1000;
 if (AheadOf(seq_num, *newest_inserted_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);
	...
	while (AheadOf(seq_num, *newest_inserted_seq_num_)) {
	  missing_packets_.insert(*newest_inserted_seq_num_);
	  ++*newest_inserted_seq_num_;
	}
}
  • bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const
代码语言:txt
复制
// Test if all previous packets has arrived for the given sequence number.按照官方注释译为判断是否给定seq_num之前的包都已经接收到。其具体实现其实是判断seq_num在buffer_存储index的packet与prev_index(index > 0 ? index - 1 : buffer_.size() - 1)对应packet的连续性 。当buffer[index]为一帧中的第一个packet或buffer[prev_index]->continuous = true时,该函数返回true,其他情况下比如两者序列号不符合连续条件,两者时间戳不相等都返回false。

bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const {
  size_t index = seq_num % buffer_.size();
  int prev_index = index > 0 ? index - 1 : buffer_.size() - 1;
  const auto& entry = buffer_[index];
  const auto& prev_entry = buffer_[prev_index];

  if (entry == nullptr)
    return false;
  if (entry->seq_num != seq_num)
    return false;
  if (entry->is_first_packet_in_frame())
    return true;
  if (prev_entry == nullptr)
    return false;
  if (prev_entry->seq_num != static_cast<uint16_t>(entry->seq_num - 1))
    return false;
  if (prev_entry->timestamp != entry->timestamp)
    return false;
  if (prev_entry->continuous)
    return true;

  return false;
}
  • std::vector<std::unique_ptr<PacketBuffer::Packet>> PacketBuffer::FindFrames( uint16_t seq_num)
代码语言:txt
复制
//遍历buffer_查找完整帧的包
 for (size_t i = 0; i < buffer_.size() && PotentialNewFrame(seq_num); ++i) {
 ...
 	size_t index = seq_num % buffer_.size();
    buffer_[index]->continuous = true;
    //当找到一帧的最后一个包时,利用while(true)向前查找一帧的第一个包的序列号start_seq_num
    if (buffer_[index]->is_last_packet_in_frame()) {
     	 uint16_t start_seq_num = seq_num;
     	 int start_index = index;
     	 size_t tested_packets = 0;
		 ...
      	 int64_t frame_timestamp = buffer_[start_index]->timestamp;
    	 ...
    	 while (true) {
    	 	 ++tested_packets;
    	 	 //非h264编码依据packet->is_first_packet_in_frame()判断是否找到帧的第一个包
	    	 if (!is_h264 && buffer_[start_index]->is_first_packet_in_frame())
	          break;
	    	 ...
	    	 
	    	 if (tested_packets == buffer_.size())
             break;
          
	    	 start_index = start_index > 0 ? start_index - 1 : buffer_.size() - 1;
	    	 //对于h264没有确切的一帧起始标识,所以利用时间戳是否相等,判断是否找到一帧的起始包
	    	 if (is_h264 && (buffer_[start_index] == nullptr ||
	                        buffer_[start_index]->timestamp != frame_timestamp)) {
	          break;
	        }
	    	  --start_seq_num;
    	 }
    	 if (is_h264) {
    	 	...
    	 	//如果不属于h264的关键帧,并且在start_seq_num位置之前存在丢包,则直接返回
    	 	if (!is_h264_keyframe && missing_packets_.upper_bound(start_seq_num) !=
                                     missing_packets_.begin()) {
          	return found_frames;
        	}
    	 }
    	 //将查找到的一帧所有包存储到found_frames中
    	 const uint16_t end_seq_num = seq_num + 1;
    	 for (uint16_t i = start_seq_num; i != end_seq_num; ++i) {
	        std::unique_ptr<Packet>& packet = buffer_[i % buffer_.size()];
	        RTC_DCHECK(packet);
	        RTC_DCHECK_EQ(i, packet->seq_num);
	        // Ensure frame boundary flags are properly set.
	        packet->video_header.is_first_packet_in_frame = (i == start_seq_num);
	        packet->video_header.is_last_packet_in_frame = (i == seq_num);
	        found_frames.push_back(std::move(packet));
      	 }
		 //删除seq_num之前的丢包记录 
        missing_packets_.erase(missing_packets_.begin(),
                             missing_packets_.upper_bound(seq_num));
    	 
    }
  	++seq_num;
 }
 return found_frames;

上述过程即为组帧的主要逻辑,剩余组帧部分就是将packets转换为RtpFrameObject类型的对象。关于上述packet_buffer的处理,这里讨论几点问题,以下属于个人思考,不一定准确,大家可以一起讨论看看。

  • 1.上述处理逻辑找到的packets真的是一帧数据所有的packets么?

个人认为对于h264上述FindFrames的处理逻辑存在缺陷,h264编码的packet没有明确的起始标识符,在PacketBuffer::PotentialNewFrame函数中判断条件保障了一定可以找到帧的起始packet。但h264的packet->is_first_packet_in_frame()不准。

代码语言:txt
复制
(bool is_first_packet_in_frame() const {
   return video_header.is_first_packet_in_frame;
 })

可以在video_rtp_depacketizer_h264.cc文件看到,is_first_packet_in_frame赋值并不一定准确。

代码语言:txt
复制
absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> ProcessStapAOrSingleNalu(
    rtc::CopyOnWriteBuffer rtp_payload) {
    ...
     parsed_payload->video_header.is_first_packet_in_frame = true;
    ...
}

absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> ParseFuaNalu(
    rtc::CopyOnWriteBuffer rtp_payload) {
  	...
  	bool first_fragment = (rtp_payload.cdata()[1] & kSBit) > 0;
  	...
   parsed_payload->video_header.is_first_packet_in_frame = first_fragment;
   ...
 }

所以个人认为对于h264,并不能保证一定可以找到起始包,假如目前真的没有收到起始包,FindFrames函数中的while(true)循环由于非时间戳不一致而终止,那么此时start_seq_num不一定代表起始包序列号,while(true)循环里找到的若不是真正的起始包序列号,那么说明start_seq_num前存在丢包,这时对于非关键帧,有如下机制可以保证对找到的packets不进行处理:

代码语言:txt
复制
if (!is_h264_keyframe && missing_packets_.upper_bound(start_seq_num) !=
                                     missing_packets_.begin()) {
	return found_frames;
}

但对于关键帧呢?怎么保障?这里还没有阅读过视频RTP包的发送逻辑,所以不是很肯定。若是对于关键帧都是以H264::NaluType::kFuA类型发送RTP包,那么这里应该不会存在太大问题(默认解析kFuA类型的packet时拿到的is_first_packet_in_frame准确)。

上述逻辑在master分支最新内容上依旧未有变动。

为避免上述问题存在,个人认为FindFrames这里应该添加一个标识符,用于表示是否真的找到起始包,在while(true)中,对于h264若满足时间戳不一致导致的break,那么记标识符为true,后面当检测到当前标识符为true,则再添加packets到found_frames。

  • 2.PacketBuffer::PotentialNewFrame判断顺序可否更改?

不可以,条件entry->is_first_packet_in_frame()表明只要是属于一帧的起始包,就可以进行完整帧包的查找,若把时间戳等判断条件提前,那么FindFrames函数可能永远不会继续向下执行。这里的顺序也保障了一次FindFrames函数调用可以返回多个帧的packets。

  • 3.PacketBuffer::FindFrames中关于missing_packets_.erase(missing_packets_.begin(), missing_packets_.upper_bound(seq_num))的处理合适么?

个人感觉不是很合理,函数执行到此处,对于除了h264非关键帧的情况,只能表示start_seq_num与seq_num之间不存在丢包。所以这里从begin开始清除,感觉逻辑有点问题。不过对处理并不影响,只是提前清除了missing_packets_中相关丢包的记录。

rtp_video_stream_receiver2.cc

packet_buffer返回待处理的packets(result.packets)后,传递到RtpVideoStreamReceiver2::OnInsertedPacket进行组帧的最后处理。

  • void RtpVideoStreamReceiver2::OnInsertedPacket( video_coding::PacketBuffer::InsertResult result)
代码语言:txt
复制
//遍历result.packets
for (auto& packet : result.packets) {
 	if (packet->is_first_packet_in_frame()) {
 		...
 		payloads.clear();
      	packet_infos.clear();
 	}
	...
    payloads.emplace_back(packet->video_payload);
    packet_infos.push_back(packet->packet_info);
	...
	//若此packet为帧的结束packet,则进行转换
	 if (packet->is_last_packet_in_frame()) {
	 	...
	 	//将全部的video_payload拼接合成EncodedImageBuffer
	 	rtc::scoped_refptr<EncodedImageBuffer> bitstream =
          depacketizer_it->second->AssembleFrame(payloads);
      	...
      	//利用上述过程结果,将一帧数据的packets转换为RtpFrameObject类型对象(至此组帧完成),并交由OnAssembledFrame进行下一步处理。
      	 OnAssembledFrame(std::make_unique<video_coding::RtpFrameObject>(
          first_packet->seq_num,                    //
          last_packet.seq_num,                      //
          last_packet.marker_bit,                   //
          max_nack_count,                           //
          min_recv_time,                            //
          max_recv_time,                            //
          first_packet->timestamp,                  //
          first_packet->ntp_time_ms,                //
          last_packet.video_header.video_timing,    //
          first_packet->payload_type,               //
          first_packet->codec(),                    //
          last_packet.video_header.rotation,        //
          last_packet.video_header.content_type,    //
          first_packet->video_header,               //
          last_packet.video_header.color_space,     //
          RtpPacketInfos(std::move(packet_infos)),  //
          std::move(bitstream)));
	 }

}
//当packet_buffer插入packet发现buffer_已经再无法添加元素时,会清空buffer_,设置result.buffer_cleared标识为true,故此时需要重新请求关键帧。
if (result.buffer_cleared) {
	RequestKeyFrame();
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 视频组帧
    • 1.概括
      • 2.关键函数说明
        • packet_buffer.cc
        • rtp_video_stream_receiver2.cc
    相关产品与服务
    实时音视频
    实时音视频(Tencent RTC)基于腾讯21年来在网络与音视频技术上的深度积累,以多人音视频通话和低延时互动直播两大场景化方案,通过腾讯云服务向开发者开放,致力于帮助开发者快速搭建低成本、低延时、高品质的音视频互动解决方案。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档