目录
一. 前言
音视频传输通常使用 UDP,由于网络中存在丢包,抖动,乱序等现象,接收端收到的媒体包需要有个包缓冲区存放,对于视频而言,一帧数据可能被打包到多个 RTP 包传输,因此接收端收到 RTP 包后会判断是否可以组成视频帧,如果可以组成视频帧还要判断其参考帧是否存在,如果存在则将该帧送入帧缓冲区,等待解码线程进行解码。
二. Video JitterBuffer架构
如上所示,RtpVideoStreamReceiver 是收视频包的处理类,其中的 Video JitterBuffer 逻辑主要由 PacketBuffer,RtpFrameReferenceFinder 和 FrameBuffer 互相协作实现。
PacketBuffer 是 RTP 包的缓冲区,收到 RTP 包后根据序列号存放到环形数组的特定位置,如果某一帧对应的 RTP 包收集完整则弹出该帧的所有 RTP 包数据。
RtpFrameReferenceFinder 用于帧参考关系的查找确认,例如对于 I 帧不需要参考其他帧就可以进行解码,P 帧则需要前向参考,而 B 帧则需要前后双向参考,因此某一帧的数据收集完整后,还需要等待其参考帧就绪才能送入帧缓冲区等待解码。
FrameBuffer 为帧缓冲区,解码线程会读取该缓冲区的视频帧数据进行解码。
三. PacketBuffer
一个视频帧在发送时可能被打包成多个 RTP 包,接收端接收时需要有个包级别的缓冲区,该缓冲区等待接收帧的完整 RTP 包,然后提交给下个流程进行组帧等处理。
PacketBuffer 的实现代码在 modules/video_coding/packet_buffer.h 和 modules/video_coding/packet_buffer.cc 中。
PacketBuffer 类涉及的成员变量和方法如上所示,其中最重要的成员是 std::vector<std::unique_ptr<Packet>> buffer_,它是一个用于存放 Packet 的动态环形数组(起始大小为 512,最大为 2048),即接收到 RTP 包后根据其序列号将包存放到该环形数组的对应位置(index = seq_num % buffer_.size()),每次插入 RTP 包都会判断缓冲区靠前的 RTP 包是否已经是某一视频帧的完整数据,如果是则将这些 RTP 包带在 InsertPacket 函数的返回值 InsertResult 中。
对于 PacketBuffer 最重要的方法为 InsertPacket,它首先计算 RTP 包存放在环形数组的位置,如果该位置当前有存放数据,则通过包序号判断是否为重复包,是重复包则不做处理,如果不是重复包说明此时缓冲区已经不够用了,需要调用 ExpandBuffer 扩容后再继续不断尝试将包塞入缓冲区。
每次 InsertPacket 将 Packet 存放到 buffer_[index] 后会再调用 FindFrames(seq_nunm),它查找当前是否收到了帧的完整数据,如果是则将帧对应的完整 RTP 包返回存放到 result.packets 中,FindFrames 逻辑如下。
FindFrames 在 buffer_ 大小不为 0 的情况下,判断插入 seq_num 的包后是否可能拿到帧的完整 RTP 包,如果不可能则先跳过,下次 InsertPacket 还会再调用 FindFrames 查看是否能拿到帧的完整 RTP 包。
PotentialNewFrame(uint16_t seq_num) 判断的逻辑如下。
1. 如果是帧的第一个包,它是有可能凑成帧的完整数据的,一方面是有些非关键帧只需要一个 RTP 包,即便是需要多个 RTP 包的关键帧,seq_num 之后的包可能早就收齐存在于缓冲区了
2. 如果不是帧的第一个包,并且缓冲区 index 位置的前一个位置是空数据,说明这个帧的数据肯定不完整,是不可能凑出帧完整数据的
3. 如果 index 位置的前一个位置的包序号不是 seq_num - 1,或者时间戳跟 seq_num 包的时间戳不一样,说明它们是没有关系的包
4. 如果 index 前一个位置的包的 continuous 为 true,说明 seq_num 对应的帧在 seq_num 之前的包已经是收齐连续的了,再收到该包是可能凑齐帧完整数据的
如果 PotentialNewFrame 为 true 并且 buffer_[index] 是帧的最后一个包,说明此时可以拿到帧对应的完整 RTP 包数据了。
此时从 PotentialNewFrame 为 true 对应的 seq 位置往前遍历,如果遇到 is_first_packet_in_frame 对应位置的包,说明帧的包数据已经从尾部遍历到首部,此时只要把 [start_seq_num, end_seq_num) 位置的包都存放到 found_frames 中即可,start_seq_num 是从 PotentialNewFrame 为 true 对应的 seq_num 从后一直往前遍历到帧的第一个包的。
至此 PacketBuffer 的收包存放以及当帧对应的 RTP 包完整时如何获取到的流程已经介绍完成。
四. ReferenceFinder
ReferenceFinder 的作用是判断当前帧的参考帧是否存在,如果存在其参考帧则将当前帧送到 FrameBuffer,等待解码线程对其进行解码,如果不存在则暂存等待其参考帧到达后再把它送到 FrameBuffer。(例如对于 I 帧不需要参考其他帧,对于 P 帧需要前向参考,对于 B 帧需要前后双向参考)
如果调用 PacketBuffer InsertPacket 能组成完整帧数据,则其返回的 struct InsertResult 值中会携带该帧对应的完整 Packet 数据,然后调用 OnInsertedPacket。
struct InsertResult {
std::vector<std::unique_ptr<Packet>> packets;
// Indicates if the packet buffer was cleared, which means that a key
// frame request should be sent.
bool buffer_cleared = false;
};
OnInsertedPacket(packet_buffer_.InsertPacket(std::move(packet)));
OnInsertedPacket 逻辑如下,如果 result.packets 不为空则遍历 result.packets 将 Packet 的负载数据存放到 payloads,把 Packet 的信息存放到 packet_infos,当遍历到帧的最后一个包时会调用 video depacketizer 的 AssembleFrame 将 payloads 存放到 bitstream 中(bitstream 实际上是一个 uint8_t 数组),然后再调用 OnAssembledFrame。
RtpVideoStreamReceiver::OnAssembledFrame 逻辑如下,如果之前还没收到过任何帧并且当前的帧也不是关键帧则进行关键帧请求(RequestKeyFrame),之后再判断 current_codec_ 与 frame codec 是否一致或设置 current_codec_,然后再调用 video_coding::RtpFrameReferenceFinder 的 ManageFrame 方法查找当前 frame 对应的参考帧。
RtpFrameReferenceFinder::ManageFrame 逻辑如下,主要是调用 ManageFrameInternal 判断当前 frame 是否有对应的参考帧,如果还没找到参考帧则将 frame 暂时保存到 stashed_frames_ 中,如果当前 frame 已经找到参考帧则调用 HandOffFrame 处理将其送进 FrameBuffer,然后再把暂存的 frame 重新弹出查看它们是否能找到参考帧,如果找到参考帧则也送到 FrameBuffer 中。
关于如何查找帧对应的参考帧是否存在,VP8,VP9,H264 的判断逻辑不相同,具体可以查看 ManageFrameVp8,ManageFrameVp9,ManageFrameH264,本文不对此进行展开。
五. FrameBuffer
ReferenceFinder 查找到帧对应的参考帧后会将该帧送入 FrameBuffer,解码线程会对帧进行解码,HandOffFrame 函数调用 frame_callback_->OnCompleteFrame 将帧送入 FrameBuffer。
如下 VideoReceiveStream::OnCompleteFrame 中最关键的逻辑为 frame_buffer_->InsertFrame。
FrameBuffer 中最重要的成员为 frames_,它是一个 FrameMap 类型的对象,存储 frameId 与 frame 的映射关系,FrameBuffer::InsertFrame 就是将 frame 插入到 FrameMap 中。
using FrameMap = std::map<VideoLayerFrameId, FrameInfo>;
解码逻辑如下所示,调用 frame_buffer_->NextFrame 取出 buffer 中的帧,然后调用 HandleEncodedFrame 进行解码。