本文分析了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)
//利用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)
//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
// 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)
//遍历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()
不准。
(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
赋值并不一定准确。
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不进行处理:
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)
//遍历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();
}