本节介绍FFmpeg编程常用的几种数据结构,首先描述FFmpeg对于音视频数据的编码和封装及其用到的数据结构,接着叙述FFmpeg组织音视频数据的三种数据包样式,然后阐述FFmpeg的过滤概念以及与滤镜相关的几种数据结构。
初学者觉得音视频入门难,一个原因是音视频行业充斥着很多专业术语,令人不明所以,仿佛是在看天书。而且一个名词往往有好几种叫法,具体怎么叫取决于你从哪个角度看它,虽然这些叫法指的是同一个东西,但初学者常常以为是不同的事物,导致越看越迷糊,越想越糊涂。
比如FFmpeg用到的muxer和demuxer这两个名词,直译过来为“复用器”和“解复用器”,然而对于初学者而言,完全不明白这两个词汇是什么意思。如果说“复用”也叫作“封装”,“解复用”也叫作“解封装”,会不会更好理解一些呢?然而“解封装”三个字还是比较拗口,日常生活中没有这种叫法,只有“解封”的说法,可是“解封”对应“封锁”,它跟“封装”有什么关系呢?诸如此类的疑问,在音视频的学习过程中层出不穷,如果不刨根问底,搞清楚这些基本概念,就会磕磕绊绊,增加学习成本。
其实“解封装”的概念很好理解,就像用户上网购物,收到的是一个个快递包裹,要把这些包裹拆了才能拿出里面的东西,这个拆包裹的过程就对应“解封装”的操作。反之,商家给客户发货时,要先把商品装进纸箱打包,再交给快递公司运输,这个打包的过程就对应“封装”的操作。打包好的包裹会贴上快递单,标明发货人、收货人、商品名称等收件信息,正如封装好的音视频文件都拥有文件头,内含音视频的格式类型、持续时长等基本信息。
之所以会有“复用”和“解复用”的叫法,是因为通信行业在传输信号时需要共享信道。mux是multiplexing(多路传输)的缩写,复用技术包括频分复用、时分复用、波分复用、码分复用等类型。以时分复用为例,系统把时间划分为一个个时间段,每个时间段都分配给某种数据传输,比如第1时间段、第3时间段、第4时间段传输视频数据,第2时间段、第5时间段传输音频数据,相当于通过切割时间片来反复使用信道资源,如此实现了在同一个信道传输音视频数据的需求。至于解复用,则是复用的逆向过程,也就是接收器陆续收到各时间段的传输数据后,把这些分片了的信号重新解析成播放器能够识别的音视频数据。demux开头的de只是个前缀,表示相反的含义,把后面的动作反过来而已,像拆包就是打包的逆向操作,所以说把demux直译为“解复用”真的是伤脑筋。
至于“封装”和“解封装”,更常用于计算机行业的文件操作方面。这个比信号传输要好理解,一个文件相当于一个包裹,把一串音频流数据和一串视频流数据合并到同一个文件里面,可不就是把两个物品打包到同一个包裹吗?当然,封装音视频文件不像现实中的打包这么简单,而是要遵循某种格式规则才行。比如文件开头的若干字节用来保存头部信息,接着后面若干字节保存一段视频数据,再后面若干字节保存一段音频数据,再后面重复“视频+音频”的操作。之所以交错存储音视频数据,是为了让播放器能够及时取出相同时间戳的音频和视频,避免出现话音对不上嘴型的尴尬情况。解封装便是从音视频文件的交错数据中分离出音频数据和视频数据,之后再分别用音频的解码器解析音频数据,用视频的解码器解析视频数据。
因此,“复用”和“解复用”属于通信行业的信号传输领域,“封装”和“解封装”更偏向计算机行业的文件存储领域,“打包”和“拆包”则属于人们日常生活的行为,“合并”和“分离”是此类操作的目标或者说结果。在学习音视频的过程中,无论是否涉及FFmpeg编程开发,都可能遇到以上这些名词。对于初学者来说,可以认为“复用”“封装”“打包”是同一种概念,而“解复用”“解封装”“拆包”是另一种概念。
不过用于打包和拆包的音视频数据是压缩了的数据,并非原始的音视频数据。在压缩数据和原始数据之间,还存在着编码和解码的过程。很早以前,通过电报发送文字消息的时候,就用到了专门的电报编码技术,发送方把一段文字翻译成电报代码,然后通过无线电发出去,这便是编码的过程;接收方侦测指定频率段,将收到的电报代码翻译成文字,这便是解码(也叫译码)的过程。
当然,音视频的编解码不像电报翻译这么简单,因为给音视频编码的主要目的是压缩数据大小,压缩效率越高,这个编码标准就越好。压缩的逆过程是解压缩,简称解压,也就是把压缩后的数据还原为压缩前的原始数据。所以音视频的编码标准也称作压缩标准,“编码”和“解码”属于对音频数据或者对视频数据的再加工,“压缩”和“还原”是此类操作的目标或者说结果。这里的“编码”跟软件行业的“编码”是两码事,音视频的编码指的是把原始的音视频数据通过某种标准进行压缩以便减少占用的内存,而软件行业的编码指的是编写程序代码。为了避免混淆,本书把软件行业的“编码”改称为“编程”或者“编写代码”。
编码和解码都属于动词,执行这种动作的工具叫作编码器和解码器,合称编解码器。同一种标准的编码器和解码器可以合在一起,也可以分开单干。像H.264/AVC标准的编解码器都在x264中,H.265/HEVC标准的编解码器都在x265中,MP3的编解码器都在mp3lame中。也有把编码器和解码器分开的,比如国产的AVS2标准,它的编码器是xavs2,解码器是davs2。
下面介绍FFmpeg在打包和拆包,以及编码和解码过程中用到的结构说明,主要包括封装器实例AVFormatContext、编解码器AVCodec以及编解码器实例AVCodecContext。
AVFormatContext用于读写音视频文件,其中读文件对应拆包操作(也称解封装),写文件对应打包操作(也称封装),使用该结构需要包含头文件libavformat/avformat.h。在读取音视频文件时,AVFormatContext主要用到了以下函数。
· avformat_open_input:打开音视频文件。第二个参数为文件路径。
· avformat_find_stream_info:查找音视频文件中的流信息。
· av_find_best_stream:寻找指定类型的数据流。
· avformat_close_input:关闭音视频文件。
在写入音视频文件时,AVFormatContext主要用到了以下函数。
· avformat_alloc_output_context2:分配待输出的音视频文件封装实例。第四个参数为文件路径。
· avio_open:打开音视频实例的输出流。第一个参数为文件实例的pb字段,第二个参数为文件路径。
· avformat_new_stream:给音视频文件创建指定编码器的数据流。
· avformat_write_header:向音视频文件写入文件头。
· av_write_trailer:向音视频文件写入文件尾。
· avio_close:关闭音视频实例的输出流。
· avformat_free_context:释放音视频文件的封装实例。
AVCodec定义了编解码器的详细规格,使用该结构需要包含头文件libavcodec/avcodec.h,调用avcodec_find_encoder函数会获得指定编号的编解码器,也可以调用avcodec_find_encoder_by_name函数获得指定名称的编解码器。常见的编解码器与文件类型的对应关系见表2-1。
表2-1 常见的编解码器与文件类型的对应关系
AVCodecContext用于对音视频数据进行编码和解码操作,使用该结构需要包含头文件libavcodec/avcodec.h,调用avcodec_alloc_context3函数会获得指定编解码器的实例。下面是与AVCodecContext有关的函数说明。
· avcodec_alloc_context3:获取指定编解码器的实例。
· avcodec_open2:打开编解码器的实例。
· avcodec_send_packet:把压缩了的数据包发送给解码器的实例。
· avcodec_receive_frame:从解码器的实例接收还原后的数据帧。
· avcodec_send_frame:把原始的数据帧发送给编码器的实例。
· avcodec_receive_packet:从编码器的实例接收压缩后的数据包。
· avcodec_close:关闭编解码器的实例。
· avcodec_free_context:释放编解码器的实例。
2.2.1节提到了音视频数据存在“打包”和“拆包”、“编码”和“解码”这些操作,对应的结果是“合并”与“分离”、“压缩”与“还原”。在各种操作的前后,音视频的数据形式不可避免会发生变化,比如文件封装和文件解析,对应的数据转换流程如图2-3和图2-4所示。
图2-3 音视频文件的封装流程
图2-4 音视频文件的解析流程
可见,在文件封装的过程中,音视频依次出现了甲、乙、丙三种数据样式。鉴于文件解析是文件封装的逆向过程,此时音视频依次出现了丙、乙、甲三种数据样式。
在FFmpeg框架中,音视频数据表现为三个层次:数据流、数据包和数据帧,各自对应上述的丙、乙、甲三种数据样式。下面分别说明这三种数据样式。
数据流对应FFmpeg的AVStream结构,调用avformat_new_stream函数会给音视频文件创建指定编码器的数据流。
“流”指的是一个连续的管道,类似于轨道、河道,所有同类型的数据都要按照时间先后顺序放在同一个“流”里面。FFmpeg支持音频流、视频流、字幕流等类型,其中音频流和视频流比较常用,大多数视频文件也只包含这两种数据流。FFmpeg支持的数据流类型见表2-2。
表2-2 FFmpeg支持的数据流类型
之所以把音视频数据分为不同的流,是为了方便读写单独的音频流和视频流,毕竟音频和视频拥有不同的规格参数,比如音频有声道数量、采样格式、采样频率等参数,视频有分辨率、像素格式、帧率等参数。对于音频文件,其内部只有音频流没有视频流;对于无声的视频文件,其内部只有视频流没有音频流;只有带声音的视频文件,才会同时拥有音频流和视频流。
数据包对应FFmpeg的AVPacket结构,调用av_packet_alloc函数会分配一个数据包,调用av_packet_free函数会释放数据包资源。
数据流内部由一个个数据包组成,数据包存放着压缩后的音视频数据。数据包主要有以下几类规格参数。
(1)流索引:相当于流数组的下标,表示该数据包属于哪个流。
(2)时间戳相关参数:标明该数据包的时间大小,决定它在数据流中的时间位置。
(3)压缩后的数据:包括保存数据的字节数组,以及数据大小。
数据帧对应FFmpeg的AVFrame结构,调用av_frame_alloc函数会分配一个数据帧,调用av_frame_free函数会释放数据帧资源。
压缩后的数据包经过解码器处理就还原成了数据帧。对于视频来说,一个视频帧就是一幅完整的图像,它不但可以被播放器渲染为视频画面,还能被截图软件保存为图片文件。对于音频来说,一个音频帧包含的是一小段时间的音频采样,比如每20毫秒的音频采样数据合并成一帧(这里的采样时长由采样频率决定,初学者无须特别关心)。
视频帧的图像类型保存在AVFrame的pict_type字段,常见的图像类型说明见表2-3。
表2-3 常见的图像类型说明
音频采样指的是在某个时刻测量模拟信号得到的声音样本。真实声音虽然是连续的,但是在计算机看来,声音是离散且均匀的音频样本,离散样本需要经过数字转换才能存储为样本数据。对模拟信号进行数字采样的数值转换如图2-5所示。其中细的曲线为原始的模拟信号,粗的折线为采样后的数字信号。
图2-5 对模拟信号进行数字采样的数值转换
FFmpeg用到了Filter的概念,直译过来是“过滤器”,感觉像是净水器过滤自来水用的。过滤的字面含义是利用某种介质滤除水中的杂质,音视频数据确实存在一些需要滤除的噪杂信号,比如音频里面的噪声、杂音,视频画面的麻点、雪花等。但在通信行业一般不叫过滤器,而是称作“滤波器”。在处理信号的时候,滤波器通过选择指定的频率,使得信号只有特定的频率成分才能通过,极大地衰减了其他频率成分,从而让信号波形变得比较平滑。
可是滤波器的叫法太专业了,它跟音视频又有什么关系呢?其实日常生活中的摄影就有滤光镜辅助拍照,这个滤光镜简称“滤镜”,使用滤镜可以让摄影作品具有特殊的效果。对于视频画面来说,加上一层滤镜,即可让视频拥有别样的体验,比如光晕特效、怀旧特效、模糊特效等。当然,音视频的滤镜并不限于少数几种图像特效,而是拥有更广泛的加工处理,相当于对音视频文件的内容编辑操作。
因此,“过滤器”“滤波器”“滤镜”三者在FFmpeg编程中属于同一种事物,之所以有不同的叫法,是因为行业领域不同而已。“过滤器”是净水等大部分行业的通用叫法,“滤波器”是通信行业的叫法,“滤镜”是摄影行业的叫法。在处理图像时更多称呼“滤镜”,显然音视频方面称呼“滤镜”也更通顺。
FFmpeg在加工音视频数据时用到的结构包括AVFilter、AVFilterGraph、AVFilterContext、AVFilterInOut,使用这几个结构需要包含头文件libavfilter/avfilter.h,它们的用法说明如下。
AVFilter定义了滤镜的规格,调用avfilter_get_by_name函数会获得指定名称的输入滤镜或者输出滤镜,滤镜名称的对应说明见表2-4。
表2-4 滤镜名称的对应说明
AVFilterGraph指的是滤镜图,滤镜图执行具体的过滤操作,包括检查有效性、加工处理等。该结构的常用函数说明如下。
· avfilter_graph_alloc:分配一个滤镜图。
· avfilter_graph_parse_ptr:把通过字符串描述的图形添加到滤镜图,这里的字符串描述了一系列的加工操作及其详细的处理规格。
· avfilter_graph_config:检查过滤字符串的有效性,并配置滤镜图中的所有前后连接和图像格式。
· avfilter_graph_free:释放滤镜图资源。
AVFilterContext指的是滤镜实例,通过滤镜实例与数据帧交互,先把原始的数据帧送给输入滤镜的实例,再从输出滤镜的实例获取加工后的数据帧。该结构的常用函数说明如下。
· avfilter_graph_create_filter:根据滤镜定义以及音视频规格创建滤镜的实例,并将其添加到现有的滤镜图中。
· av_buffersrc_add_frame_flags:把一个数据帧添加到输入滤镜的实例中。
· av_buffersink_get_frame:从输出滤镜的实例获取加工后的数据帧。
· avfilter_free:释放滤镜的实例。
要先调用avfilter_graph_free函数,再调用avfilter_free函数,否则程序运行会报错。
AVFilterInOut定义了滤镜的输入输出参数,调用avfilter_inout_alloc函数会初始化滤镜的输入参数或者输出参数,调用avfilter_inout_free函数会释放滤镜的输入参数或者输出参数。
AVFilterInOut结构的主要字段说明如下。
· name:参数名称。对于单路音视频来说,填in表示输入,填out表示输出。
· filter_ctx:滤镜实例。对于输入参数来说,填输入滤镜的实例;对于输出参数来说,填输出滤镜的实例。
· pad_idx:填0即可。
· next:下一路音视频的输入输出参数。如果不存在下一路音视频,就填NULL。