题记:视频文件通过解封装获取到streams包含视频流的信息,本章节介绍对视频流进行解码处理获取可以展示的数据信息,关于理论基础参考编码简介,建议先看线程设计了解整体线程。同样可以结合ffplay和Demo更容易理解。
解码解析
解码准备
进入解码前可以先了解一下以下内容,做一些准备工作:
formatContext->interrupt_callback设置中断回调- callback 设置回调函数
- opaque 设置回调函数的参数。回调函数一般是静态或者全局函数,需要传递状态,这里是C风格void * 类型
formatContext->flags |= AVFMT_FLAG_GENPTS在某些情况下,输入流可能缺少这些时间戳,或者时间戳可能不正确。通过设置 AVFMT_FLAG_GENPTS,FFmpeg 能够尝试自动修复这些问题av_format_inject_global_side_data向AVFormatContext注入全局的侧边数据(side data)。比如如果需要向后续AVPacket中传输一些媒体信息,就可以通过这种方式formatContext->pb->eof_reached是否读到结尾,开始播放时设为0,为了避免一些bugav_dump_format打印媒体信息,展示参考笔记一av_find_best_stream查找最佳匹配的媒体流av_guess_sample_aspect_ratio猜测视频宽高比
设置这些之后,完整播放器可以选择设置播放方式和音视频同步方式。如视频只有图像或音频,只有音频播放方式有显示WAVES、RDFT(傅里叶变换)波形或者显示贴图(音频文件可以带贴图)。本文的介绍和Demo暂时不处理这些额外的设置(本人精力有限)。
音视频同步后续会单独章节介绍,这里简要说明下:
- 只有视频(图像),采用视频同步,本质上就是按照FPS或PTS展示图片;
- 有音频,一般会采用音频同步(人对音频比视频更敏感),就是按照音频的播放时间调整视频展示速度;
- 外部时钟同步,使用外部时钟同时调整音频和视频的播放速度,一般用于视频会议或者直播系统;
解码解析
解码流程
解码是编码的逆过程,具体到FFmpeg就是从文件中不断读取AVPacket并转换成播放数据AVFrame的过程,如下图(注意这里的结束只是解码的结束,并不代表播放的结束)
看一下这几个相关函数
av_read_frame用于从多媒体文件或流中读取下一帧数据,为AVPacket填充数据AVFormatContext *上下文指针AVPacket *待填充数据的packet
avcodec_send_packet向解码器发送压缩数据包AVCodecContext *解码上下文AVPacket *需要解码的packet
avcodec_receive_frame从解码器接收解码后的帧数据AVCodecContext *解码上下文AVFrame *解码后的数据结构体
由于帧信息之间有依赖性,详细可见编码原理 ,解码时不能使用单独的packet解码到数据,需要依赖其他GOP中信息(如参考帧等),于是需要这样的AVCodecContext解码上下文,包含对应格式的解码器。大概的过程并不复杂,伪代码如下:
// 构建解码上下文
const AVCodec *acodec = avcodec_find_decoder(codecpar->codec_id);
if (!acodec) return;
AVCodecContext *aCodecContext = avcodec_alloc_context3(acodec);
avcodec_parameters_to_context(aCodecContext, codecpar);
int aRet = avcodec_open2(aCodecContext, acodec, nullptr);
if (aRet != 0) return;
// 解码过程
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while(true) {
av_read_frame(formatContext, packet);
avcodec_send_packet(aCodecContext, packet);
// 使用完了packet,需要释放引用
av_packet_unref(packet);
while (true) {
avcodec_receive_frame(context, frame);
xxxxxxx
av_frame_unref(frame);
}
}
下面具体看一下解码结构体:
解码结构体
AVStream对应于音视频处理中的流,一个视频文件中可能包含多个流,如前文介绍中的视频流、音频流、字幕流等。相关字段如下:
index:流索引,在streams中的索引
id:流标识
codecpar:解码器参数的结构体,如编码类型、码率、帧率、分辨率、像素格式、采样率、声道数等;
time_base:表示了流中时间戳的单位,就是用分数表达。例如time_base为{1, 25},每帧1/25秒
duration:时长,需要配合时间基使用
start_time:第一个样本的时间戳,一般为0或者尽可能靠近0的时刻,配合时间基
metadata:包含元信息,艺术家,年份等
attached_pic:封面图,例如一些MP3或AAC音频文件附带的专辑封面
sidedata:附加信息
AVStream是视频流信息的封装,通常根据streams创建处理线程,每个类型或每个流(可以有多路音频或视频)创建一个线程。codecpar包含了解码器所需要的参数:
codec_type:流类型,AVMEDIA_TYPE_VIDEO(视频)、AVMEDIA_TYPE_AUDIO(音频)或AVMEDIA_TYPE_SUBTITLE(字幕)
codec_id:编码数据类型,如h264、MPEG4、MJPEG等
codec_tag:编解码器的附加信息
format:对于视频来说,指的是像素格式(如YUV420、YUV422等);对于音频来说,指的是音频的采样格式。
width&height:视频的宽度和高度
sample_rate、channels&sample_fmt:音频的采样率、声道数和采样格式
channel_layout:音频声道布局
解码器根据codecpar信息,查找AVCodec和构建AVCodecContext,整体结构如下图:
这里需要关注的函数:
avcodec_find_decoder用于查找并获取指定编解码器avcodec_alloc_context3为指定的编解码器分配并初始化一个AVCodecContext结构avcodec_parameters_to_context将AVCodecParameters结构中的信息复制到AVCodecContext结构中。
数据结构体
AVPacket
AVPacket是压缩数据的封装,主要字段:
buf:AVBufferRef结构体指针,引用指针设计
data:数据的指针
size:数据的长度,单位为字节
pts:Presentation Timestamp,解码后该数据包内容在整个媒体流中的显示时间
dts:Decode Timestamp,数据包在整个媒体流中的时间排序
duration:该数据包所持续的时间长度
stream_index:数据流的索引号
flags:一个32位的标志位,支持一些特定编码器和格式的编解码特性
side_data及:额外信息,通过av_format_inject_global_side_data传入
AVFrame
AVFrame是解码后数据的封装,主要字段:
data:指针数组,用于存放数据帧的各个通道的数据指针。视频帧,通常图像平面(如YUV中的Y、U、V平面)的指针;对于音频帧,这通常是音频通道的数据指针(左右声道)。
linesize:对应于data,各个通道数据的每行字节数
extended_data:扩展数据指针数组的指针,通常用于音频数据,表示多个通道的音频样本。
width和height:视频数据帧的宽度和高度。
format:像素格式或样本格式。对于视频帧,表示像素格式(如YUV420P);对于音频帧,表示样本格式(如PCM)。
pts:帧的时间戳(Presentation Timestamp)。
pkt_pts和pkt_dts:AVPacket结构体中的时间戳,用于与解码后的AVFrame结构体中的时间戳进行比较、计算和修正。
nb_samples:音频帧中采样的数量,如音频帧采样率为44.1kHZ,那么该帧播放时间为 nb_samples/44100秒
sample_rate、channels和channel_layout:音频帧的采样率、声道数和声道布局。
key_frame:指示该视频帧是否为关键帧。
extended_buf:扩展缓冲区数组,用于存储超出`buf`数组限制的数据。
colorspace和color_range:视频帧的色彩空间和色彩范围
解码设计
通过上述介绍大概可以了解流的解码过程,而解码的整体设计是两层生产消费模型,建议先看线程设计。
两层的生产消费模型和核心是两层缓冲区的设计,第一层用于存储Packet,第二层存储Frame。先来看一下ffplay中的设计。
第一层缓存PacketQueue的封装如图
typedef struct MyAVPacketList {
AVPacket pkt;
struct MyAVPacketList *next;
int serial;
} MyAVPacketList;
typedef struct PacketQueue {
MyAVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
int64_t duration;
int abort_request;
int serial;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
这里可以看到,实际上就是单项链表,Queue中存储链表的头尾。其他信息说明
- serial: 结点和链表都包含这个字段,用于各个部分的同步(
packet和frame)。seek操作时,由于重新定位时间点,前面解析出来的frame都失效了,需要情况缓存。这个serial就起到这个作用。在讲到seek时会详细解释。 - nb_packets、size和duration:用于记录当前Queue的存储数量,包含的字节数、能够播放的时长。
- abort_request:同步abort操作。
- mutex和cond:锁和条件锁,操作队列时使用。锁用于队列增删改查,条件锁(也需要配合锁使用)用于如果Queue为空,获取packet时需要。这里顺便提一下,调用SDL_CondWait实际上会先释放mutex,到被唤醒时重新加锁。
第二层FrameQueue的封装如图
typedef struct FrameQueue {
Frame queue[FRAME_QUEUE_SIZE];
int rindex;
int windex;
int size;
int max_size;
int keep_last;
int rindex_shown;
SDL_mutex *mutex;
SDL_cond *cond;
PacketQueue *pktq;
} FrameQueue;
FrameQueue实际上采用的是环形列表,这种结构在随机访问上更有优势。由于是环形操作,新增两个索引rindex、windex。
- rindex和windex: 读索引和写索引,由于是环形操作,实际相当队列的头尾,队列先进先出,读操作在头部开始,写操作在尾部进行。
- size、max_size:帧的数量和最大容量,通常就是FRAME_QUEUE_SIZE
- keep_last:标识否保留最后一帧(例如播放完毕后停在最后一帧)
- rindex_shown:标识当前帧是否已经被展示过。这个主要用于视频展示时有一定持续时间,可能下一个处理周期还是展示当前帧。
- mutex和cond:同PacketQueue作用,但此处的
cond不仅要锁队列为空,也要锁队列满。 - pktq:指向对应的PacketQueue。
Demo中对应OPPacketQueue和OPFrameQueue,为了方便理解直接使用了C++的list模版,frame中也使用了emptyCon和fullCon。
了解完基本结构,再来看下图。
- 解封装线程会通过
av_read_frame生产packet(实际上是填充数据),根据类型分配到对应的PacketQueue中。- 当有三个PacketQueue有足量的packet时(
stream_has_enough_packets根据需要定义),阻塞解封装线程,即停止下载和解析文件。 - 当PacketQueue取数后(或其他一些操作如abort),发送signal,解封装线程继续工作。
- 当有三个PacketQueue有足量的packet时(
PacketQueue为解码提供数据源- PacketQueue 中的cond,用于当PacketQueue为空时,取数操作进入waitng状态;
FrameQueue为播放提供数据源- FrameQueue 中的cond,当FrameQueue达到最大容量,解码进入waiting状态,从而控制整个流程;
- 当FrameQueue没有数据,cond会让取数操作进入waiting状态。形成卡顿。