音视频学习笔记六——从0开始的播放器之解码解析

434 阅读9分钟

题记:视频文件通过解封装获取到streams包含视频流的信息,本章节介绍对视频流进行解码处理获取可以展示的数据信息,关于理论基础参考编码简介,建议先看线程设计了解整体线程。同样可以结合ffplay和Demo更容易理解。

解码解析

解码准备

进入解码前可以先了解一下以下内容,做一些准备工作:

  • formatContext->interrupt_callback设置中断回调
    • callback 设置回调函数
    • opaque 设置回调函数的参数。回调函数一般是静态或者全局函数,需要传递状态,这里是C风格void * 类型
  • formatContext->flags |= AVFMT_FLAG_GENPTS 在某些情况下,输入流可能缺少这些时间戳,或者时间戳可能不正确。通过设置 AVFMT_FLAG_GENPTS,FFmpeg 能够尝试自动修复这些问题
  • av_format_inject_global_side_dataAVFormatContext注入全局的侧边数据(side data)。比如如果需要向后续AVPacket中传输一些媒体信息,就可以通过这种方式
  • formatContext->pb->eof_reached 是否读到结尾,开始播放时设为0,为了避免一些bug
  • av_dump_format 打印媒体信息,展示参考笔记一
  • av_find_best_stream 查找最佳匹配的媒体流
  • av_guess_sample_aspect_ratio 猜测视频宽高比

设置这些之后,完整播放器可以选择设置播放方式和音视频同步方式。如视频只有图像或音频,只有音频播放方式有显示WAVES、RDFT(傅里叶变换)波形或者显示贴图(音频文件可以带贴图)。本文的介绍和Demo暂时不处理这些额外的设置(本人精力有限)。

解码处理流程.jpg

音视频同步后续会单独章节介绍,这里简要说明下:

  • 只有视频(图像),采用视频同步,本质上就是按照FPS或PTS展示图片;
  • 有音频,一般会采用音频同步(人对音频比视频更敏感),就是按照音频的播放时间调整视频展示速度;
  • 外部时钟同步,使用外部时钟同时调整音频和视频的播放速度,一般用于视频会议或者直播系统;

解码解析

解码流程

解码是编码的逆过程,具体到FFmpeg就是从文件中不断读取AVPacket并转换成播放数据AVFrame的过程,如下图(注意这里的结束只是解码的结束,并不代表播放的结束解码流程.jpg

看一下这几个相关函数

  • 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,整体结构如下图: 解码关系图.jpg

这里需要关注的函数:

  • 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: 结点和链表都包含这个字段,用于各个部分的同步(packetframe)。seek操作时,由于重新定位时间点,前面解析出来的frame都失效了,需要情况缓存。这个serial就起到这个作用。在讲到seek时会详细解释。
  • nb_packetssizeduration:用于记录当前Queue的存储数量,包含的字节数、能够播放的时长。
  • abort_request:同步abort操作。
  • mutexcond:锁和条件锁,操作队列时使用。锁用于队列增删改查,条件锁(也需要配合锁使用)用于如果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实际上采用的是环形列表,这种结构在随机访问上更有优势。由于是环形操作,新增两个索引rindexwindex

  • rindexwindex: 读索引和写索引,由于是环形操作,实际相当队列的头尾,队列先进先出,读操作在头部开始,写操作在尾部进行。
  • sizemax_size:帧的数量和最大容量,通常就是FRAME_QUEUE_SIZE
  • keep_last:标识否保留最后一帧(例如播放完毕后停在最后一帧)
  • rindex_shown:标识当前帧是否已经被展示过。这个主要用于视频展示时有一定持续时间,可能下一个处理周期还是展示当前帧。
  • mutexcond:同PacketQueue作用,但此处的cond不仅要锁队列为空,也要锁队列满。
  • pktq:指向对应的PacketQueue。

Demo中对应OPPacketQueueOPFrameQueue,为了方便理解直接使用了C++的list模版,frame中也使用了emptyConfullCon

了解完基本结构,再来看下图。

  • 解封装线程会通过av_read_frame生产packet(实际上是填充数据),根据类型分配到对应的PacketQueue中。
    • 当有三个PacketQueue有足量的packet时(stream_has_enough_packets根据需要定义),阻塞解封装线程,即停止下载和解析文件。
    • 当PacketQueue取数后(或其他一些操作如abort),发送signal,解封装线程继续工作。
  • PacketQueue为解码提供数据源
    • PacketQueue 中的cond,用于当PacketQueue为空时,取数操作进入waitng状态;
  • FrameQueue为播放提供数据源
    • FrameQueue 中的cond,当FrameQueue达到最大容量,解码进入waiting状态,从而控制整个流程;
    • 当FrameQueue没有数据,cond会让取数操作进入waiting状态。形成卡顿。

生产消费模型.jpg