大纲目录
文章目录
前置知识
音视频基础概念
- 容器:即特定格式的多媒体文件,比如mp4、 flv、 mkv等都是指的容器格式。容器并不直接参与处理音频流和视频流,只是负责存储这些流。
- 压缩格式:音视频的压缩格式是指用来压缩和解压数据的算法和技术,它决定了数据如何从原始状态转换为压缩状态,以及如何从压缩状态还原回原始状态。例如音频压缩格式aac和视频压缩格式h264等。
- 媒体流(Stream):一个完整的视频当中一般包含多种媒体流,如音频流、视频流、字幕流等。例如一个mp4视频中如果有一个视频流和两个音频流,则这个视频中就有三个媒体流(stream)。其中视频流绝大多数情况都是经过压缩的,例如h264格式。音频流大多数也是经过压缩的,如aac格式,但有时也会使用未经压缩的pcm数据。
- 采样点(Sample):采样点是指在数字化的过程中,从连续信号中抽取出来的离散值。例如一个音频的采样频率(采样率)为48000Hz,就表示每秒钟采样48000次,即每秒钟会有48000个采样点。而音频采样点就等同于是像素。
- 帧内采样点的数量:音频中一个帧所包含的采样点数量。例如一个音频的采样率为44.1 kHz,帧内采样点为1024个,则每一帧所持续的时间就为 1024/44410≈0.0232 秒。
- 帧数据:视频是由一系列连续的图像组成的,每一幅图像被称为一帧。而一帧音频的长度是由其帧内采样点的数量决定的。
- 数据包(Packet)/数据帧(Frame):一个媒体流由大量的数据包或数据帧构成。压缩(未经解码处理)的媒体流是由数据包(Packet)构成的,未压缩(解码后)的原生媒体流是由数据帧(Frame)构成的。一个packet/frame就表示一帧音频或视频数据。
- 解复用器(AVformat):解复用器的主要功能有——容器识别(识别多媒体文件的容器格式,例如MP4、AVI、MKV、FLV等)、流提取(分离多媒体文件中的各个数据流,例如视频流、音频流、字幕流等)、解封装(将提取出来的媒体流解封装为大量的数据包packet)等。所以解复用器并不只是用于解封装
- 编解码器(AVcodec):解码器主要用于将packet解码(解压)为frame,编码器则负责将frame编码(压缩)为packet。
解复用、解码的流程分析
参照下图分析音视频解封装、解码的过程
解复用(解封装):要先经过解复用器(AVformat)对其处理,过滤器会分离出不同的媒体流,并将其拆分为packet,然后将产生的packet按照帧的先后顺序放到对应的packet队列中。
解码:接着解码器就从packet队列中拿数据,将packet解码为原生媒体流,也按照顺序将其放到frame队列中。
之所以要经过这一系列的过程,是因为一个压缩的媒体文件是无法直接播放,需要经解码处理后将其恢复为可播放的原始数据。
FFMPEG有8个常用库
- AVUtil:核心工具库,下面的许多其他模块都会依赖该库做一些基本的音视频处理操作。
- AVFormat:文件格式和协议库,该模块是最重要的模块之一,封装了Protocol层和Demuxer、 Muxer层,使得协议和格式对于开发者来说是透明的。
- AVCodec:编解码库,封装了Codec层,但是有一些Codec是具备自己的License的, FFmpeg是不会默认添加像libx264、 FDK-AAC等库的,但是FFmpeg就像一个平台一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开发者提供统一的接口。
- AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为音视频数据做特效处理是非常方便同时也非常高效的一种方式。
- AVDevice:输入输出设备库,比如,需要编译出播放声音或者视频的工具ffplay,就需要确保该模块是打开的,同时也需要SDL的预先编译,因为该设备模块播放声音与播放视频使用的都是SDL库。
- SwrRessample:该模块可用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换。
- SWScale:该模块是将图像进行格式转换的模块,比如,可以将YUV的数据转换为RGB的数据,缩放尺寸由1280720变为800480。
- PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数。
常见音视频格式的介绍
aac格式介绍
aac的格式有两种:ADIF不常用,ADTS是主流,所以这里主要讲解ADTS。简单来说,ADTS可以在任意帧解码,也就是说它每⼀帧都有头信息。ADIF只有⼀个统⼀的header,所以必须得到所有的数据后解码。参考下图
⼀个AAC原始数据块⻓度是可变的,对原始帧加上ADTS头进⾏ADTS的封装,就形成了ADTS帧。参考下图
adts-header的长度一般为7字节,当protection_absent=0
时,表示需要校验码,此时的adts-header就会额外添加一个2字节的校验码,此时的adts-header长度就为9字节。
⼀般情况下ADTS的头信息都是7个字节,分为2部分:
- adts_fixed_header
- adts_variable_header
其中,adts_fixed_header
为固定头信息,adts_variable_header
是可变头信息。固定头信息中的数据每⼀帧都相同,⽽可变头信息则在帧与帧之间不同。 参考下图
注:ADTS Header的长度可能为7字节或9字节,当protection_absent字段为时,表示需要校验码,此时是9字节;否则为7字节。
常见的header字段如下:
- 同步字(syncword):2个字节(16位) 同步字是ADTS文件的标志符,它用于确定音频帧的开始位置和结束位置,通常为0xFFF。
- ID (MPEG Version):1个字节(8位) ID指示使用的MPEG版本。值为0表示MPEG-4,值为1表示MPEG-2。
- Layer:2个比特 Layer定义了音频流所属的层级,对于AAC来说,其值为0。
- Protection Absent:1个比特 Protection Absent指示是否启用CRC错误校验。当该比特为0时,表明音频数据经过CRC校验,否则未经过CRC校验。
- Profile:2个比特 Profile指示编码所使用的AAC规范类型,如AAC LC、AAC HE-AAC等。
- Sampling Frequency Index (Sampling Rate):4个比特 Sampling Frequency Index表示采样率的索引,它告诉解码器当前音频数据的采样率。这个值的范围是0到15,每个值表示一个特定的采样率。参考下图
- Private Bit:1个比特 Private Bit为私有比特,通常被设置为0,没有实际作用。
- Channel Configuration:3个比特 Channel Configuration指示音频的通道数,如单声道、立体声或多声道等。
- Originality:1个比特 Originality指示编码数据是否被原始产生,通常为0。
- Home:1个比特 Home bit通常被设置为0,没有实际作用。
- Emphasis:2个比特 Emphasis指示对信号进行强调处理的类型,一般不使用。
- sampling_frequency_index:表示使⽤的采样率下标,通过这个下标在Sampling Frequencies[ ]数组中查找得知采样率的值。
这里只是对aac格式的简单介绍,想要了解更多内容,参考:AAC-ADTS格式分析【转载】-CSDN博客
h264格式分析
H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
H264主要分为两层:编码层(Video Coding Layer,VCL)和网络抽象层(NetworkAbstraction Layer (NAL));前者定义了各种编码的算法,后者将前者编码的数据按照一定的方式进行打包存储或者传输。而NAL单元(NALU)作为可以单独可以解码的结构,整个H264的码流可以理解为由多个NALU组成的。这里我们主要介绍NALU。
先来认识一些相关概念
- SPS:序列参数集,SPS中保存了⼀组编码视频序列的全局参数。
- PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
- I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
- P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
- B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。
- 在I帧之前,至少有一个SPS和PPS。
- GOP:GOP是一组连续的视频帧,这些帧按照一定的编码规则被组织在一起。GOP的设计目的是为了提高视频数据的压缩效率,并且使得视频流能够支持随机访问。
H.264/AVC只是定义了一种标准,常见的具体格式有两种:AnnexB格式和AVCC格式。AnnexB格式主要用于实时播放(.h264文件就是这种格式),AVCC格式主要用于视频存储,即AnnexB是能够直接播放的,而AVCC不能直接播放。
AnnexB格式:[start code]NALU | [start code] NALU | ...
SPS和PPS被嵌入到视频流中,其本身也是一种NALU。这种格式比较常见,也就是我们熟悉的每个帧前面都有0x00 00 00 01或者0x00 00 01作为起始码。
AVCC格式:([extradata]) | ([length] NALU) | ([length] NALU) | ...
这里的NALU一般没有SPS PPS等参数信息,参数信息属于extradata位于文件的头部。比如ffmpeg中解析mp4文件后SPS PPS存在streams[index]->codecpar->extradata
中。
AnnexB和AVCC的区别在于:
NALU之间的分隔方式不同:AnnexB是通过在每一个NALU前面添加一个start code,而AVCC则是通过在NALU前都加上一个大端格式的前缀,表示NALU的长度。
AnnexB格式的start code有两种:
①3字节 0x000001 单帧多[slice](即单帧多个NALU)之间间隔
②4字节 0x00000001 帧之间,或者SPS等之前
而AVCC格式的长度前缀一般设置为4个字节。SPS和PPS的位置不同:AnnexB是将SPS和PPS直接嵌入到视频流中的,SPS和PPS也是一种NALU。即每一个GOP的起始位置都有一个SPS和PPS,所以解码器可以从AnnexB格式的视频流随机点开始进行解码。而AVCC格式格式是将视频的元数据和SPS和PPS等内容统一放到文件的头部,这部分内容通常称为extradata或者sequence header。所以AVCC格式主要用于视频存储,MP4、MKV通常用AVCC格式来存储。
这里主要介绍AnnexB格式和AVCC格式的区别,想要了解H264-NALU的结构,可以参考:H264基础简介【转载】-CSDN博客,这篇博客以AnnexB格式为例,介绍了h264的格式。
FLV和MP4格式介绍
- FLV格式
FLV封装格式是由⼀个⽂件头(file header)和 ⽂件体(file Body)组成。其中,FLV body由⼀对对的(Previous Tag Size字段 + tag)组成。Previous Tag Size字段 排列在Tag之前,占⽤4个字节。Previous Tag Size记录了前⾯⼀个Tag的⼤⼩,⽤于逆向读取处理。FLV header后的第⼀个Pervious Tag Size的值为0。 参考下图
这里只是对flv格式的简单介绍,详情参考:FLV文件格式分析【转载】-CSDN博客
- MP4格式
MP4协议本身没有多复杂,没啥特别难理解的地方,关键的“复杂”点就在于其“大”,嵌套的各种各样的子box。详情参考:整理mp4协议重点【转载】-CSDN博客
FFmpeg解码解封装实战
数据包和数据帧(AVPacket/AVFrame)
AVPacket/AVFrame的引用计数问题
在FFmepg中,数据包对应的结构体为AVPacket
,数据帧对应的结构体为AVFrame
。一个AVPacket/AVFrame就表示一帧视频数据或音频数据。
特别的是,AVPacket/AVFrame的内存模型比较特殊,因为可能出现多个AVPacket/AVFrame对应同一帧数据的情况,所以FFmepg采用了一种引用计数的方式,以避免内存浪费。
参考下图理解:
AVPacket/AVFrame变量本身并不直接存储数据,而是指向一块缓存空间AVBuffer,由缓冲区自身来维护引用计数和真正的媒体数据。以AVPacket为例,对于多个AVPacket共享同一个缓存空间的情况, FFmpeg引用计数的机制如下 :(AVFrame也是如此)
- 初始化时引用计数的指为0,只有真正分配AVBuffer的时候,引用计数的值才加至1。
- 当有新的Packet引用共享的缓存空间时, 就将引用计数+1。
- 当释放了引用共享空间的Packet时,就将引用计数-1;
- 引用计数减至0时,就释放掉引用的缓存空间AVBuffer。
API介绍
AVPacket:
AVPacket *av_packet_alloc();
为AVPacket申请空间,此时并未创建AVBuffer。void av_init_packet(AVPacket *pkt);
初始化pkt中的相关字段,例如将整型数据设为0,将指针为null等操作。int av_new_packet(AVPacket *pkt, int size);
创建数据包,申请一个size字节大小的AVBuffer,并让pkt的AVBufferRef指向它。此时才是真正的创建了AVBuffer。int av_packet_ref(AVPacket *dst, const AVPacket *src);
对给定数据包设置一个新的引用。其作用是将dst的AVBufferRef指向src的AVBuffer,即让dst也关联到src的AVBuffer。此时对应的AVBuffer的引用计数加一。void av_packet_unref(AVPacket *pkt);
擦除一个数据包。取消pkt和它对应AVBuffer的关联,并使其引用计数减一。如果AVBuffer的引用计数减为0了,则FFmpeg会释放掉这块AVBuffer的空间。av_packet_unref会有安全检查,所以不用担心uref一个空指针的情况。void av_packet_move_ref(AVPacket *dst, AVPacket *src);
将src中的每个字段移动到dst,并重置(清空)src。此时src与AVBuffer的关联断掉,转移到dst上面,AVBuffer的引用计数不变。AVPacket *av_packet_clone(const AVPacket *src);
AVPacket克隆,相当于av_packet_alloc + av_packet_ref。创建一个和src一样的AVPacket,并作为返回值返回给上层。此时对应的AVBuffer的引用计数加一。void av_packet_free(AVPacket **pkt);
释放AVPacket,要和av_packet_alloc搭配使用,成对出现。
AVFrame:
AVFrame *av_frame_alloc();
为AVFrame 申请空间,作用与av_packet_alloc一样。int av_frame_ref(AVFrame *dst, const AVFrame *src);
对给定数据包设置一个新的引用。作用与av_packet_ref一样。void av_frame_unref(AVFrame *frame);
擦除一个数据包。作用与av_packet_unref一样。void av_frame_move_ref(AVFrame *dst, AVFrame *src);
将src中的每个字段移动到dst,并重置(清空)src。作用与av_packet_move_ref一样。int av_frame_get_buffer(AVFrame*frame,int align);
为媒体数据分配新的缓冲区,根据AVFrame分配内存。AVFrame *av_frame_clone(const AVFrame *src);
作用与av_packet_clone一样。void av_frame_free(AVFrame **frame);
释放AVFrame,要和av_frame_alloc搭配使用,成对出现。
注意事项
- AVPacket/AVFrame和AVBuffer是两回事,AVBuffer是真实的数据缓冲空间,AVPacket/AVFrame并不直接存储媒体数据,而是有能够访问到AVBuffer的引用字段。所以AVPacket/AVFrame和AVBuffer都需要为其分配空间,就好像指针需要4/8字节空间,而它指向的数据也需要分配空间。
- av_init_packet会将字段下的所有指针置为null,所以如果此时的AVPacket字段中还关联的AVBuffer数据而没有释放,在其指针置为null后就会失去关联,此时的AVBuffer就永远无法得到释放了,就会造成内存泄漏。所以av_init_packet函数不能滥用,很容易导致内存泄漏。
FFmpeg解复用实战
解复用是指将一个复合的音视频文件或流中的不同数据流分离出来。
分离流
- 用到的结构体与API
AVFormatContext *avformat_alloc_context();
申请一个AVFormatContext结构内存,并进行简单的初始化。此时AVFormatContext中还没有数据。其中AVFormatContext
是解复用器上下文结构体。void avformat_free_context(AVFormatContext *s);
释放 AVFormatContext 及其所有流。int avformat_open_input(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);
打开输入的媒体文件,同时还会读取文件的头部信息。参数ps为解复用器上下文对象的地址,如果 *ps 为空avformat_open_input内部就会自动调用avformat_alloc_context;url表示输入文件的路径或者网络地址;fmt表示设置输入格式,为null则表示自动识别(一般都设为null);options表示选项,一般也设为null。void avformat_close_input(AVFormatContext **s);
关闭打开的AVFormatContext。释放它及其所有内容并将 *s 置为null。其函数中已经包含了avformat_free_context操作,所以调用了avformat_close_input之后就就不用再调用avformat_open_input了。通过看源码发现,avformat_close_input之所以要传入一个二级指针,主要是为了在函数内部将原来的指针置为null,仅此而已。
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
读取媒体文件的数据包以及流信息等,用以填充AVFormatContext
结构体信息,options选项一般设为null。由于需要读取数据包,所以avformat_find_stream_info接口会带来很大的延迟。
int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, const struct AVCodec **decoder_ret, int flags);
查找指定流的下标,其返回值为对应的流所在format->streams数组下的下标。参数ic表示指定的解复用器上下文,type就表示要查找的流(AVMEDIA_TYPE_AUDIO表示音频流、AVMEDIA_TYPE_VIDEO表示视频流等等),wanted_stream_nb和related_stream一般设为-1表示自动选择,decoder_ret是输出型参数,返回所选流的解码器,可以为null;flags,暂时未定义(“flags; none are currently defined”)。int av_read_frame(AVFormatContext *s, AVPacket *pkt);
读取一帧音视频包,返回流的下一个帧。这个函数会自动读取下一帧数据。返回值为0表示成功,如果为AVERROR_EOF则表示读到末尾结束了。
- 解复用流程
先用avformat_alloc_context
分配一个解复用器上下文AVFormatContext,接着用avformat_open_input
打开媒体文件。随后可以用avformat_find_stream_info
读取媒体到AVFormatContext中,进而分离流,或者可以直接用av_find_best_stream
分离流。
avformat_find_stream_info接口之所以不是必须的,这是因为avformat_open_input接口在调用时不只是打开了媒体文件,并且还会读取文件的头部信息并初始化AVFormatContext。所以即使在不调用avformat_find_stream_info的情况下,AVFormatContext中还是会有媒体文件的元数据的,可以保证正常的分离流操作。
分离流之后,就开始在一个循环中不断调用av_read_frame
读取数据包并处理数据包,直到读完媒体文件。注意,虽然函数叫read frame,但读取的其实是packet。在此期间,根据packet->stream_index和av_find_best_stream的返回值匹配,区分处理音频数据和视频数据。
提取流
由于不同容器的封装格式不同,有些容器在分离流之后读取的packet中是裸流数据,即不包含头部信息,只有媒体流数据,音频和视频配置信息通常存储在元数据中。而有些容器在分离流之后读取的packet是包含头部信息的。所以对于不包含头部信息的媒体流数据,在提取流时就要为其加上头部信息再写入,而包含头部信息的就可以直接写入。
例如ts文件分离流之后读取的packet就是包含头部信息的,在提取的时候就可以直接写入,不用做其它处理。而mp4和flv等格式分离流之后读取的packet却是不包含头部信息的,其packet只有裸流数据,在这种情况下,就需要额外先为其写入头部信息,再写入packet(裸流数据)
如下是一些常见的格式:
- packet为裸流数据的格式:FLV、MP4、MKV、WebM
- packet为带有头部的媒体流数据的格式:TS、MPEG-2 PS、AVI、WMV / ASF
音频流 - aac
音频流以提取aac流为例,需要在写入packet->data之前,手动绘制一个7字节的adts头部数据,并将其写入。
视频流 - h264
而视频流的则比较麻烦了,以h264格式为例,由于h264在存储时通常是AVCC格式,而播放的话需要转为AnnexB格式。简单来说就是在提取h264流数据时并不能简单的手动写入头部数据,而是需要让FFmepg中的过滤器代为处理,将数据转为标准的Annex B格式的数据。大致需要用到如下内容:
AVBitStreamFilter
过滤器的结构体。AVBSFContext
过滤器上下文的结构体,BSF即为BitStreamFilter的简写。const AVBitStreamFilter *av_bsf_get_by_name(const char *name);
根据名字查找指定的过滤器,不同的过滤器对应着不同的功能。- “h264_mp4toannexb”,一个过滤器的名字,其功能是将MP4格式转换成AnnexB格式。
int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);
为过滤器分配上下文,即将过滤器与过滤器上下文之间进行绑定。int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
复制编码器参数,以便过滤器正常运行。int av_bsf_init(AVBSFContext *ctx);
初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用)int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
将pkt发送给ctx对应的那个过滤器,过滤器会将处理好的packet放到对应的缓冲区中。int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
从对应的缓冲区中取出一个packet。
demo样例
解复用一个mp4文件,提取出mp4媒体文件中的aac和h264两个流的文件。
#include <iostream> #include <fstream> #include <string> using namespace std; // FFmepg-7.0头文件引入 extern "C" { #include "libavutil/error.h" #include "libavformat/avformat.h" #include "libavcodec/bsf.h" } // sampling_frequencies,用于获取sampling_frequency_index const int sampling_frequencies[] = { 96000, // 0x0 88200, // 0x1 64000, // 0x2 48000, // 0x3 44100, // 0x4 32000, // 0x5 24000, // 0x6 22050, // 0x7 16000, // 0x8 12000, // 0x9 11025, // 0xa 8000 // 0xb // 0xc 0xd 0xe 0xf 是保留的 }; /** * 填充aac-ADTS协议头 * * @param adts_header_buf 自定义ADTS-header的缓冲区 * @param data_length aac-body的长度(packet->size) * @param profile AAC规范类型 * @param sample_rate 采样率 * @param channels 声道数 */ bool fill_ADTS_header(char* adts_header_buf, const int data_length, const int profile, const int sample_rate, const int channels) { int sampling_frequency_index = 3; // 默认48000hz int adtsLen = data_length + 7; // data_length + adts_header_len int frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]); for(int i = 0; i < frequencies_size; i++) { // 找到了对应的采样率,填充adts-header if(sampling_frequencies[i] == sample_rate) { sampling_frequency_index = i; // syncword:0xfff - 12bits adts_header_buf[0] = 0xff; // 高8bits adts_header_buf[1] = 0xf0; // 低4bits // ID=0(MPEG-4) - 1bit adts_header_buf[1] |= (0 << 3); // Layer:0 - 2bits adts_header_buf[1] |= (0 << 1); // protection_absent=1(no CRC) - 1bit adts_header_buf[1] |= 1; // profile:${profile} - 2bits adts_header_buf[2] = (profile) << 6; // sampling frequency index=${sampling_frequency_index} - 4bits adts_header_buf[2] |= (sampling_frequency_index & 0x0f)<<2; //private bit:0 - 1bit adts_header_buf[2] |= (0 << 1); //channel configuration:channels 高1bit adts_header_buf[2] |= (channels & 0x04)>>2; //channel configuration:channels 低2bits adts_header_buf[3] = (channels & 0x03)<<6; //original:0 - 1bit adts_header_buf[3] |= (0 << 5); //home:0 - 1bit adts_header_buf[3] |= (0 << 4); //copyright id bit:0 - 1bit adts_header_buf[3] |= (0 << 3); //copyright id start:0 - 1bit adts_header_buf[3] |= (0 << 2); //frame length:value - 高2bits adts_header_buf[3] |= ((adtsLen & 0x1800) >> 11); //frame length:value - 中间8bits adts_header_buf[4] = (uint8_t)((adtsLen & 0x7f8) >> 3); //frame length:value - 低3bits adts_header_buf[5] = (uint8_t)((adtsLen & 0x7) << 5); //buffer fullness:0x7ff - 高5bits adts_header_buf[5] |= 0x1f; //buffer fullness:0x7ff - 低6bits adts_header_buf[6] = 0xfc; //11111100 // number_of_raw_data_blocks_in_frame: // 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。 return true; } } // 没找到对应的采样率 cerr << "unsupport samplerate: " << sample_rate << endl; return false; } // usage: process <in_file> <out_audio> <out_video> int main(int argc, char* argv[]) { // 打开文件 ofstream out_audio(argv[2], ios_base::out | ios_base::binary); ofstream out_video(argv[3], ios_base::out | ios_base::binary); // 解复用器上下文 AVFormatContext* fmt_ctx = nullptr; // 打开一个输入流并读取其header avformat_open_input(&fmt_ctx, argv[1], nullptr, nullptr); // 获取媒体流信息(index) int audio_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); // 指定过滤器:h264_mp4toannexb过滤器的功能是将MP4格式转换成AnnexB const AVBitStreamFilter* avbsf = av_bsf_get_by_name("h264_mp4toannexb"); // 为过滤器分配上下文 AVBSFContext* avbsf_ctx = nullptr; av_bsf_alloc(avbsf, &avbsf_ctx); // 复制编码器参数,以便过滤器正常运行(为过滤器填充音频流的编码器参数) avcodec_parameters_copy(avbsf_ctx->par_in, fmt_ctx->streams[video_index]->codecpar); // 初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用) av_bsf_init(avbsf_ctx); // packet alloc AVPacket* packet = av_packet_alloc(); av_init_packet(packet); // 提取流 ; while(av_read_frame(fmt_ctx, packet) != AVERROR_EOF) { if(packet->stream_index == audio_index) // 音频流(暂定aac格式) { // 手动添加aac-adts header // header 信息 int profile = fmt_ctx->streams[audio_index]->codecpar->profile; int sample_rate = fmt_ctx->streams[audio_index]->codecpar->sample_rate; int channels = fmt_ctx->streams[audio_index]->codecpar->ch_layout.nb_channels; // 填充adts-header char buf[7] = {0}; // adts-header的大小就为7字节 fill_ADTS_header(buf, packet->size, profile, sample_rate, channels); // 写入aac-adts_header out_audio.write(buf, 7); // 写入aac-body out_audio.write((char*)packet->data, packet->size); } else if(packet->stream_index == video_index) //视频流(暂定h264格式) { // send av_bsf_send_packet(avbsf_ctx, packet); // receive // 一个输入数据包可能被过滤器拆分成多个输出数据包,所以这里要用循环 while(av_bsf_receive_packet(avbsf_ctx, packet) == 0) { out_video.write((char*)packet->data, packet->size); // 写入文件 av_packet_unref(packet); // 释放packet,防止内存泄漏 } } // 及时清理buf,防止内存泄漏 av_packet_unref(packet); } // clear and exit out_video.close(); out_audio.close(); avformat_close_input(&fmt_ctx); av_bsf_free(&avbsf_ctx); av_packet_free(&packet); return 0; }
FFmpeg解码实战
所谓解码就是指将压缩的音视频数据恢复为可播放的原始数据格式的过程。
用到的结构体与API
AVCodec
解码器的结构体。AVCodecParserContext
解析器上下文的结构体。const AVCodec *avcodec_find_decoder(enum AVCodecID id);
根据指定的id查找匹配的解码器。AVCodecParserContext *av_parser_init(int codec_id);
初始化id对应的AVCodecParserContext。AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
为AVCodecContext分配内存。int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
打开解码器(将解码器和解码器上下文进行关联)int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx, uint8_t **poutbuf, int *poutbuf_size, const uint8_t *buf, int buf_size, int64_t pts, int64_t dts, int64_t pos);
解析⼀个Packet。从buf中读取一个数据包到poutbuf中,并设置poutbuf_size,返回值为读取的字节数。int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
将AVPacket压缩数据给解码器,解码器会自动解码之后放到对应的缓冲区中。int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
获取到解码后的AVFrame数据(从对应的解码器缓冲区中取走一个frame)int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
获取每个样本sample中的字节数。
解码流程
对于解码操作而言,音频解码和视频解码的操作流大致一样,只不过在最后保存frame数据时要根据不同的格式采取不同的方式。只需要注意,在解码之前需要用解析器对packet进行解析才能进行解码。
在解析时有些内容可能包含头部信息header,所以在解码开始时出现一些send出错的情况这是正常的。
解码流程如下
首先创建环境:先用
avcodec_find_decoder
查找解码器,接着用av_parser_init
初始化裸流的解析器,用avcodec_alloc_context3
分配解码器上下文,用avcodec_open2
将解码器和解码器上下文进行关联。然后循环处理:先用
av_parser_parse2
解析一个数据包,接着用avcodec_send_packet
将packet发送给解码器,然后用avcodec_receive_frame
接收编码后的frame,最后写入解析帧,生成PCM数据。
demo样例
对一个媒体文件中的音频流进行解码,音频流为aac格式。
注意,如果输入文件为mp3格式,解码刚开始的时候会报错这是正常的。这是因为mp3格式是包含头部信息的,而decoder只认识帧数据,无法识别mp3的头部信息。
#include <iostream> #include <fstream> #include <string> #include <algorithm> using namespace std; // FFmepg-7.0头文件引入 extern "C" { #include <libavutil/frame.h> #include <libavutil/mem.h> #include <libavcodec/avcodec.h> } // 解码器ID const AVCodecID codec_id = AV_CODEC_ID_AAC; // 数据包缓冲区大小 const int in_buf_size = 25600; // 缓冲区阈值 const int threshold_size = 1024; // 解码操作 void decode(AVCodecContext *codec_ctx, AVPacket *packet, AVFrame *frame, ofstream &out_file) { // 将带有压缩数据的数据包发送到解码器 avcodec_send_packet(codec_ctx, packet); // 读取所有输出帧(在文件中,一般可能有任意数量的输出帧) while (avcodec_receive_frame(codec_ctx, frame) == 0) { // 获取每个样本的字节数 int data_size = av_get_bytes_per_sample(codec_ctx->sample_fmt); // 写入文件 for (int i = 0; i < frame->nb_samples; i++) { // if(av_sample_fmt_is_planar()) // 判断是否为平面格式 // 交错的方式写入 for (int j = 0; j < frame->ch_layout.nb_channels; j++) { out_file.write((char *)frame->data[j] + data_size * i, data_size); } } } } // 播放范例: ffplay -ar 48000*2 out.pcm // Usage: <input file> <output file> int main(int argc, char *argv[]) { // 解码器 const AVCodec *codec = avcodec_find_decoder(codec_id); // 裸流的解析器上下文 AVCodecParserContext *parser_ctx = av_parser_init(codec->id); // 解码器上下文 AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); // 打开解码器(将解码器和解码器上下文进行关联) avcodec_open2(codec_ctx, codec, nullptr); // 打开io文件 ifstream in_file(argv[1], ios_base::in | ios_base::binary); ofstream out_file(argv[2], ios_base::out | ios_base::binary); // 获取输入文件的长度 in_file.seekg(0, ios_base::end); int in_file_len = in_file.tellg(); in_file.seekg(0); // 从文件中读取一次 uint8_t *in_buf = new uint8_t[in_buf_size + AV_INPUT_BUFFER_PADDING_SIZE]{0}; in_file.read((char *)in_buf, in_buf_size); uint8_t *data = in_buf; int data_size = in_file.tellg(); // 解析+解码 AVPacket *packet = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); while (in_file.tellg() != in_file_len || data_size > 0) { // 解析packet int parse_size = av_parser_parse2(parser_ctx, codec_ctx, &packet->data, &packet->size, data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); // 更新数据信息 data += parse_size; data_size -= parse_size; // 进行解码 decode(codec_ctx, packet, frame, out_file); // 边界检查 if (data_size < threshold_size && in_file.tellg() != in_file_len) { memmove(in_buf, data, data_size); int read_count = min(in_buf_size - data_size, in_file_len - (int)in_file.tellg()); if (!in_file.read((char *)in_buf + data_size, read_count)) { cerr << "file read error! " << "[" << __FILE__ << ":" << __LINE__ << "]" << endl; } data_size += read_count; data = in_buf; } } // over: clear and exit in_file.close(); out_file.close(); av_parser_close(parser_ctx); avcodec_free_context(&codec_ctx); av_packet_free(&packet); av_frame_free(&frame); return 0; }
内容补充
int av_strerror(int errnum, char *errbuf, size_t errbuf_size);
将错误码转换为错误信息的函数。EAGAIN
是一个预定义的错误码,通常用于指示某种资源暂时不可用。AVPacket
数据包不仅可以包含视频数据,还可以包含其他元数据。- 虽然FFmpeg是C语言写的,但FFmpeg的使用却体现着面向对象思想。例如AVCodec表示解码器方法,AVCodecContext表示对应的解码器上下文,数据并不会直接保存在AVCodec中,而是保存在AVCodecContext中的。AVCodec就相当于class中的函数/方法,AVCodecContext就相当于class中的成员变量/数据。像这种AVxxx + AVxxxContext就类似于方法+成员变量的用法,体现了FFmpeg的面向对象思想。
- FFmpeg的send + receive用法:解复用器、解码器、解析器等,都没有直接提供对应的方法,而是先通过一个send函数,将要处理的数据发送过去,在FFmpeg后台自动处理,待处理好了之后就会放到对应的缓冲取区中,然后就可以通过receive函数从缓冲区中取出处理好的数据。也就是说,这些音视频处理组件并不会直接暴漏给用户,而是间接的使用。receive操作一般都需要放在循环中,这是因为操作之后的经过处理之后可能会生成多个packet/frame。
- 解复用器是用于分离流和处理packet的,其直接处理媒体文件,经解复用器处理之后的packet就是媒体的裸流数据了。数据包packet并不能直接交给解码器处理,而是要先经过解析器的解析才能交由解码器处理。而解码器只能读取媒体的裸流数据,在解析时有些内容可能包含头部信息header,所以在解码开始时出现一些send出错的情况这是正常的。
参考资料: