FFMPEG学习笔记

想要从事音视频相关的工作,FFMPEG是绕不开的一道坎,就像吉他里面的大横按,既然我能给征服大横按,相信我也能征服你,FFMPEG!!!

自学音视频相关的内容也有一段时间啦,比如之前的H264,RTMP,FLV,还有最近的AudioUint和AudioQueue,通过这段时间的学习,我总结出来的方法就是,先将他们的数据结构分析清楚,了解主要的api,然后从简单的api开始实践。

所以我们先来看一下FFMPEG的主要数据结构和api…

数据结构

文件读取相关

AVIOContext

AVIOContext是FFMPEG管理输入输出数据的结构体

比较重要的属性:

  • unsigned char *buffer:缓存读取的数据

  • int buffer_size:缓存大小(默认32768)

  • unsigned char *buf_ptr:当前指针读取到的位置

  • unsigned char *buf_end:缓存结束的位置

  • void *opaque:URLContext结构体

协议相关

URLContext、URLProtocol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct URLContext {
const AVClass *av_class; ///< information for av_log(). Set by url_open().
struct URLProtocol *prot;
int flags;
int is_streamed; /**< true if streamed (no seek possible), default = false */
int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */
void *priv_data;
char *filename; /**< specified URL */
int is_connected;
AVIOInterruptCB interrupt_callback;
} URLContext;
typedef struct URLProtocol {
const char *name;
int (*url_open)(URLContext *h, const char *url, int flags);
int (*url_read)(URLContext *h, unsigned char *buf, int size);
int (*url_write)(URLContext *h, const unsigned char *buf, int size);
int64_t (*url_seek)(URLContext *h, int64_t pos, int whence);
int (*url_close)(URLContext *h);
struct URLProtocol *next;
int (*url_read_pause)(URLContext *h, int pause);
int64_t (*url_read_seek)(URLContext *h, int stream_index,
int64_t timestamp, int flags);
int (*url_get_file_handle)(URLContext *h);
int priv_data_size;
const AVClass *priv_data_class;
int flags;
int (*url_check)(URLContext *h, int mask);
} URLProtocol;

URLContext结构体中还有一个结构体URLProtocol,每种协议(rtp,rtmp,file等)对应一个URLProtocol。

封装格式相关

AVFormatContext

这个结构体描述了一个媒体文件或媒体流的构成和基本信息

在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。

看几个主要变量的作用:

  • struct AVInputFormat *iformat:输入数据的封装格式

  • AVIOContext *pb:输入数据的缓存

  • unsigned int nb_streams:视音频流的个数

  • AVStream **streams:音视频流

  • char filename[1024]:文件名

  • int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)

  • int bit_rate:比特率(单位bps,转换为kbps需要除以1000)

  • AVDictionary *metadata:元数据

AVInputFormat

作为输入容器,包含了输入文件的音视频流信息,程序从输入容器从读出音视频包进行解码处理

AVOutputFormat

作为输出容器,程序把编码好的音视频包写入到输出容器中

编解码相关

AVCodecContext

这是一个描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息

看一下关键属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum AVMediaType codec_type:编解码器的类型(视频,音频...)
struct AVCodec *codec:采用的解码器AVCodec(H.264,MPEG2...)
int bit_rate:平均比特率
uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)
AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s)
int width, height:如果是视频的话,代表宽和高
int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)
int sample_rate:采样率(音频)
int channels:声道数(音频)
enum AVSampleFormat sample_fmt:采样格式
int profile:型(H.264里面就有,其他编码标准应该也有)
int level:级(和profile差不太多)
AVCodec

AVCodec是存储编解码器信息的结构体,每一个编解码器对应一个该结构体。

下面说一下最主要的几个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const char *name:编解码器的名字,比较短
const char *long_name:编解码器的名字,全称,比较长
enum AVMediaType type:指明了类型,是视频,音频,还是字幕
enum AVCodecID id:ID,不重复
const AVRational *supported_framerates:支持的帧率(仅视频)
const enum AVPixelFormat *pix_fmts:支持的像素格式(仅视频)
const int *supported_samplerates:支持的采样率(仅音频)
const enum AVSampleFormat *sample_fmts:支持的采样格式(仅音频)
const uint64_t *channel_layouts:支持的声道数(仅音频)
int priv_data_size:私有数据的大小
AVStream

AVStream是存储每一个视频/音频流信息的结构体

AVStream重要的变量如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
int index:标识该视频/音频流
AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们是一一对应的关系)
AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。FFMPEG其他结构体中也有这个字段,但是根据我的经验,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间
int64_t duration:该视频/音频流长度
AVDictionary *metadata:元数据信息
AVRational avg_frame_rate:帧率(注:对视频来说,这个挺重要的)
AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。
AVPacket

AVPacket是存储压缩编码数据相关信息的结构体

重要的变量有以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
-->
uint8_t *data:压缩编码的数据。
int size:data的大小
int64_t pts:显示时间戳
int64_t dts:解码时间戳
int stream_index:标识该AVPacket所属的视频/音频流。
AVFrame

AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。

下面看几个主要变量的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。
int width, height:视频帧宽和高(1920x1080,1280x720...)
int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个
int format:解码后原始数据类型(YUV420,YUV422,RGB24...)
int key_frame:是否是关键帧
enum AVPictureType pict_type:帧类型(I,B,P...)
AVRational sample_aspect_ratio:宽高比(16:9,4:3...)
int64_t pts:显示时间戳
int coded_picture_number:编码帧序号
int display_picture_number:显示帧序号
int8_t *qscale_table:QP表
uint8_t *mbskip_table:跳过宏块表
int16_t (*motion_val[2])[2]:运动矢量表
uint32_t *mb_type:宏块类型表
short *dct_coeff:DCT系数,这个没有提取过
int8_t *ref_index[2]:运动估计参考帧列表(貌似H.264这种比较新的标准才会涉及到多参考帧)
int interlaced_frame:是否是隔行扫描
uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log的

结构体之间的关系可以参考下图:

api

avcodec_init()

初始化libavcodec,一般最先调用该函数

该函数必须在调用libavcodec里的其它函数前调用,一般在程序启动或模块初始化时调用,如果你调用了多次也无所谓,因为后面的调用不会做任何事情.从函数的实现里你可以发现,代码中对多次调用进行了控制.

av_register_all()

初始化 libavformat和注册所有的muxers、demuxers和protocols,

一般在调用avcodec_init后调用该方法

其中会调用avcodec_register_all()注册多种音视频格式的编解码器,并注册各种文件的编解复用器

avformat_alloc_context()

分配一个AVFormatContext结构,负责申请一个AVFormatContext结构的内存,并进行简单初始化

avformat_open_input()

打开一个流媒体文件

avformat_close_input()

关闭一个流媒体文件

avformat_free_context()

释放一个AVFormatContext结构

使用 avformat_alloc_context()分配的结构,采用该函数进行释放,除释放AVFormatContext结构本身内存之外,AVFormatContext中指针所指向的内存也会一并释放

avio_alloc_context()

为I/0缓存申请并初始化一个AVIOContext结构,结束使用时必须使用av_free()进行释放

av_open_input_file()

以输入方式打开一个媒体文件,也即源文件,codecs并没有打开,只读取了文件的头信息.

av_close_input_file()

关闭使用avformat_close_input()打开的输入文件容器,但并不关系它的codecs

使用 av_close_input_file 关闭后,就不再需要使用avformat_free_context 进行释放了

av_find_stream_info(AVFormatContext *ic)

通过读取媒体文件的中的包来获取媒体文件中的流信息,对于没有头信息的文件如(mpeg)是非常有用的

AVCodec *avcodec_find_decoder(enum CodecID id)

通过code ID查找一个已经注册的音视频解码器

查找解码器之前,必须先调用av_register_all注册所有支持的解码器

音视频解码器保存在一个链表中,查找过程中,函数从头到尾遍历链表,通过比较解码器的ID来查找

AVCodec avcodec_find_decoder_by_name (constchar name)

通过一个指定的名称查找一个已经注册的音视频解码器

查找解码器之前,必须先调用av_register_all注册所有支持的解码器

音视频解码器保存在一个链表中,查找过程中,函数从头到尾遍历链表,通过比较解码器的name来查找

avcodec_find_encoder()

avcodec_find_encoder_by_name()

同上。。。

int avcodec_open(AVCodecContext avctx, AVCodec codec) / avcodec_open2()

使用给定的AVCodec初始化AVCodecContext

av_guess_format()

1
2
3
4
5
AVOutputFormat *av_guess_format(constchar *short_name,
constchar *filename,
constchar *mime_type);

返回一个已经注册的最合适的输出格式

void av_init_packet(AVPacket *pkt);

使用默认值初始化AVPacket

定义AVPacket对象后,请使用av_init_packet进行初始化

int av_read_frame(AVFormatContext s, AVPacket pkt)

从输入源文件容器中读取一个AVPacket数据包

该函数读出的包并不每次都是有效的,对于读出的包我们都应该进行相应的解码(视频解码/音频解码),

在返回值>=0时,循环调用该函数进行读取,循环调用之前请调用av_free_packet函数清理AVPacket

avcodec_decode_video2()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
AVPacket *avpkt);
// 解码视频流AVPacket
// 使用av_read_frame读取媒体流后需要进行判断,如果为视频流则调用该函数解码
// 返回结果<0时失败,此时程序应该退出检查原因
// 返回>=0时正常,假设 读取包为:AVPacket vPacket 返回值为 int vLen; 每次解码正常时,对vPacket做
// 如下处理:
// vPacket.size -= vLen;
// vPacket.data += vLen;
// 如果 vPacket.size==0,则继续读下一流包,否则继续调度该方法进行解码,直到vPacket.size==0
// 返回 got_picture_ptr > 0 时,表示解码到了AVFrame *picture,其后可以对picture进程处理

avcodec_decode_audio3()/avcodec_decode_audio4()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int avcodec_decode_audio3(AVCodecContext *avctx, int16_t *samples,
int *frame_size_ptr,
AVPacket *avpkt);
int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
int *got_frame_ptr,
const AVPacket *avpkt);
// 解码音频流AVPacket
// 使用av_read_frame读取媒体流后需要进行判断,如果为音频流则调用该函数解码
// 返回结果<0时失败,此时程序应该退出检查原因
// 返回>=0时正常,假设 读取包为:AVPacket vPacket 返回值为 int vLen; 每次解码正常时,对vPacket做
// 如下处理:
// vPacket.size -= vLen;
// vPacket.data += vLen;
// 如果 vPacket.size==0,则继续读下一流包,否则继续调度该方法进行解码,直到vPacket.size==0