FFmpeg开发——基础篇(一) 前言 核心结构体 总结

前言

书接上回,我们介绍了ffmpeg的一些基础知识,使用方法,接下来介绍如何使用ffmpeg进行开发,所谓使用ffmpeg进行开发,就是依赖它的基础库,调用它的API来实现我们的功能。

当然要看懂文章需要一些C++的基础知识,能看懂基本语法,了解指针(一级指针/二级指针)的基本知识。

image.png

我们在上篇文章中也介绍了ffmpeg对于音视频操作的主要流程:

image.png

其实差不多每个阶段都通过对应的关键函数以及关键结构体来对应,因此,接下来先重点介绍一下这些重要的结构体,以及它的用法。

核心结构体

AVFormatContext

媒体信息存储结构,同时管理了IO音视频流对文件进行读写,相当于保存了音视频信息的上下文。

它在ffmpeg中的作用是非常重要的,在封装/解封装/编解码过程中都需要用到它。在程序中关于某个音视频的所有信息归根结底都来自于AVFormatContext。

结构体信息

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
objectivec复制代码
typedef struct AVFormatContext {
// 针对输入逻辑的结构体
const struct AVInputFormat *iformat;
// 针对输出逻辑的结构体
const struct AVOutputFormat *oformat;

//字节流IO操作 结构体
AVIOContext *pb;
...
...
unsigned int nb_streams; // 视音频流的个数

AVStream **streams; // 视音频流


char *url; //输入或输出地址 替换原有的filename

int64_t duration; // 时长,微秒(1s/1000_000)

int64_t bit_rate; // 比特率(单位bps,转换为kbps需要除以1000)

...
...
} AVFormatContext;

以上只是一个简略的结构体成员信息展示,但是已经能体现它管理IO,数据流,保存媒体信息的的功能了,对于初学者而言,只需要关注nb_streams和streams这两个成员,表示流的数量以及流数组。后面我们需要通过流来获取对应的信息。

使用的基本方法

输入过程

  • 申请内存创建结构体
1
2
ini复制代码// 可以,但一般没必要
AVFormatContext *formatContext = avformat_alloc_context();

但是一般在开发中并不需要开发者手动创建结构体,而是在读取文件的接口中通过库自动创建即可

1
2
3
4
5
6
7
8
c复制代码AVFormatContext *formatContext = NULL;


// 注意 即使传入的formatContext为NULL,avformat_open_input内部也会为formatContext申请内存空间的
if (avformat_open_input(&formatContext, "xxx.mp4", NULL, NULL) != 0) {
fprintf(stderr, "Failed to open input file\n");
return 1;
}

注意我们需要在avformat_open_input函数中传入正确的媒体文件路径,这样才能正确读取到文件头的信息。

  • 为防止获取不到文件头信息,可以尝试进一步获取音视频流的信息
1
2
3
4
c复制代码if (avformat_find_stream_info(formatContext, NULL) < 0) {
fprintf(stderr, "Failed to find stream information\n");
return 1;
}

avformat_open_input即使成功调用,也不一定能获取到文件头信息,因为可能有的媒体格式没有文件头?哈哈,所以一般继续调用avformat_find_stream_info可以获取正确的信息

  • 读取数据包:确认有音视频数据之后,可以通过av_read_frame把数据流读取到AVPacket中
1
2
3
4
5
6
7
8
scss复制代码// 此处主要涉及解码过程,可以略过,知道解码过程也需要传递formatContext信息即可
AVPacket packet;
while (av_read_frame(formatContext, &packet) == 0) {
// 处理 packet 中的数据

// 在使用完 packet 后释放引用
av_packet_unref(&packet);
}
  • 关闭输入
1
scss复制代码avformat_close_input(&formatContext);

avformat_close_input会关闭输入流,同时释放AVFormatContext结构体

输出过程

上面介绍的主要输入过程中使用AVFormatContext的基本方式,那么输出过程是否一致呢?函数调用上略有区别。

  • 创建输出音视频格式的AVFormatContext
1
2
3
4
5
objectivec复制代码// 通常在你需要进行音视频编码并生成一个新的音视频文件时使用
AVFormatContext *output_format_context = NULL;

//
avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
  • 写入数据
1
2
3
4
5
6
7
8
scss复制代码//先写入头文件
ret = avformat_write_header(output_format_context, &opts);

//再写入帧数据
ret = av_interleaved_write_frame(output_format_context, &packet);

// 写入收尾(同时刷新缓冲区)
av_write_trailer(output_format_context);
  • 释放avformatcontext结构体
1
scss复制代码  avformat_free_context(output_format_context);

AVStream

AVStream是AVFormatContext结构体中的一个成员(数组结构),它表示媒体文件中某一种数据的流以及对应的媒体信息,比如该流表示视频流,则同时也会含有视频相关的宽高,帧率等信息,以及time_base等基础信息。

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
objectivec复制代码
typedef struct AVStream {


int index; /**< stream index in AVFormatContext */
// stream ID
int id;

// 与流关联的编解码器的参数结构
AVCodecParameters *codecpar;

//time_base AVRational结构体有两个成员,组成一个分数(有理数)
AVRational time_base;

...
...

int64_t duration;

int64_t nb_frames; ///< number of frames in this stream if known or 0
...
...
/**
* sample aspect ratio (0 if unknown)
* - encoding: Set by user.
* - decoding: Set by libavformat.
*/
AVRational sample_aspect_ratio;
...
...
} AVStream;

对于初学者而言,可以先重点关注time_base和codecpar这两个成员,time_base不用讲,是ffmpeg中的时间基本单位,codecpar则表示了当前流的解码信息。

我们可以看一下AVCodecParameters这个结构体的成员情况

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
39
40
41
42
43
44
45
46
47
48
objectivec复制代码typedef struct AVCodecParameters {
/**
* General type of the encoded data.
*/
enum AVMediaType codec_type;
/**
* Specific type of the encoded data (the codec used).
*/
enum AVCodecID codec_id;
...

/**
* - video: the pixel format, the value corresponds to enum AVPixelFormat.
* - audio: the sample format, the value corresponds to enum AVSampleFormat.
*/
int format;
...
...
/**
* 视频帧相关的一些参数
* Video only. The dimensions of the video frame in pixels.
*/
int width;
int height;

AVRational sample_aspect_ratio;

enum AVColorRange color_range;
enum AVColorPrimaries color_primaries;
enum AVColorTransferCharacteristic color_trc;
enum AVColorSpace color_space;
enum AVChromaLocation chroma_location;

/**
* Audio only. The number of audio samples per second.
*/
int sample_rate;

// Audio only. Audio frame size
int frame_size;

// 声道配置情况(音频)
AVChannelLayout ch_layout;


AVRational framerate;

} AVCodecParameters;

可以看到AVCodecParameters是把音频和视频这两种信息混合在一起了,在流属于不同类型时使用不同的字段,或者同一个字段表达不同的含义。比如format,如果是视频,则表示AVPixelFormat枚举类型,如果是音频,则表示AVSampleFormat枚举类型。

  • 如何获取AVStream
1
2
3
4
5
6
7
8
9
10
ini复制代码// formatContext->nb_streams表示流的个数
int stream_size = formatContext->nb_streams;

for(int i=0;i<stream_size;i++){
//获取一个AVStream
AVStream *in_stream = formatContext->streams[i];
// 从AVStream中获取AVCodecParameters
AVCodecParameters *av_in_codec_param = in_stream->codecpar;
...
}

AVCodec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
objectivec复制代码//编解码器
typedef struct AVCodec {
// 编解码器的名称
const char *name;
const char *long_name;
enum AVMediaType type; // 媒体类型(视频,音频,字幕等)
enum AVCodecID id; // 编解码器的ID

// 编解码器所支持的一些参数
const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1

/**
* Array of supported channel layouts, terminated with a zeroed layout.
*/
const AVChannelLayout *ch_layouts;
} AVCodec;

AVCodec可以表示一个编解码器,里面包含了编解码的一些基本信息。

一般而言,媒体文件中的音频流,视频流中都保存有解码器ID等信息,通过这个ID可以获取对应AVCodec,从而获取该解码器的比较全面的信息。

  • 获取AVCodec
1
2
3
4
5
6
rust复制代码// formatContext即 AVFormatContext的结构体对象,此时应该已经创建并读取了信息
// codecpar是AVStream中的结构体成员,表示该流数据对应的解码器信息
// 从流中找到对应编解码器信息和id
enum AVCodecID id = formatContext->streams[videoStreamIndex]->codecpar->codec_id
// 通过ID找到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);

获取到AVCodec之后,需要通过它构建一个可用编解码器上下文(提供编解码过程中待解码数据的背景和配置)

AVCodecContext

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
objectivec复制代码
typedef struct AVCodecContext {

enum AVMediaType codec_type;// 数据类型(音频、视频、字幕、等)
const struct AVCodec *codec; // 对应的编解码器
enum AVCodecID codec_id; //编解码器ID

// time_base,编码时必须设置
    AVRational time_base;
   
    /*视频使用*/
int width, height;
// 像素格式,告诉解码器你想要把数据解码成哪个像素格式,不设置的话ffmpeg会有默认值
enum AVPixelFormat pix_fmt;

    /* audio only */
    int sample_rate; ///< samples per second
...
...
enum AVSampleFormat sample_fmt; ///< 采样格式

// AVFrame中每个声道的采样数,音频时使用
int frame_size;
//也是time_base,解码时设置
    AVRational pkt_timebase;
}

AVCodecContext就是我们前面说的编解码上下文,主要包含待解码数据的一些特性,便于在解码过程中解码器正确解析数据。比如等待解码的是视频数据,那么解码器需要知道time_base(关于time_base的概念不懂可以看前一篇文章)统一时间单位;每帧图片的宽高;视频帧的像素格式(关于像素格式见(移动开发中关于视频的一些基本概念),了解像素排列方式….

有了以上信息,解码器才可以正确的对数据进行解码。

AVCodecContext中音频的的frame_size,在解码器中可能不存在,因此在解码过程避免使用这个字段,可以找decoded_frame中的nb_samples来替代

  • 创建(解码为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码// id 是从前文中通过AVStream中获取的
// 获取到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);

// 创建AVCodecContext的结构,此时还没有对应的参数(都是默认参数)
AVCodecContext *pCodecCtx = avcodec_alloc_context3(av_codec);

// 从数据流AVStream中得到的AVCodecParameter中的相关信息复制到AVCodecContext
// 此时AVCodecContext就有了正确的信息了
if(avcodec_parameters_to_context(pCodecCtx,av_codec_parameters) < 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context
}
//初始化并启动解码器
if(avcodec_open2(pCodecCtx, pCodec, NULL)<0){
return -1; // Could not open codec
}

利用AVCodec构建AVCodecContext,然后把AVStream中已知的一些信息复制到AVCodecContext中,接着初始化并开启编解码器。

  • 销毁
1
scss复制代码avcodec_free_context(&pCodecCtx)

AVCodecContext在编解码过程中都会被用到。

AVPacket

读取文件获取AVFormatContext结构体,并且获取了解码器上下文

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

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码
typedef struct AVPacket {
int64_t pts; // 显示时间戳
int64_t dts; // 解码时间戳
uint8_t *data; // 压缩编码的数据
int size; // data的大小
int stream_index; // 当前packet所属的流(视频流或者音频流等)
...
...
...

AVRational time_base;
}

AVPacket的成员主要包括time_base,pts,dts等一些在解码时可能被用到的参数以及编码数据data。

创建过程

  • 在正常的编解码过程中,AVPacket手动申请内存,则需要手动释放内存,如果自动申请内存则不需要。
  • 从编解码器中接收数据放到packet中,使用完之后,需要释放引用av_packet_unref 即可
1
2
3
4
5
6
7
8
scss复制代码AVPacket pkt; // 自动申请出内存
// 此时只是申请了AVPacket结构体的内存空间,其所指向的数据内存区域还没有创建
AVPacket *pkt2 = av_packet_alloc(); // 如果手动申请内存,泽需要和后续av_packet_free释放
...
// do something with avpacket
...
// 在使用完 pkt 后释放内存
av_packet_free(&pkt2);

使用方式

1
2
3
4
5
6
7
8
9
scss复制代码// 从AVFormatContext中读取数据到avpacket中
// 创建avpacket指向的数据内存区域的函数是av_new_packet
if (av_read_frame(formatContext, &pkt) == 0) {
...
//解码器解码处理 pkt中的数据
...
//在使用完 pkt 后释放引用(引用数到0),从而释放其指向的数据内存区域
av_packet_unref(&pkt);
}

AVFrame

AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),除此之外就是数据对应的一些属性:时长,格式等

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
39
40
41
42
43
44
45
46
47
48
arduino复制代码视频和音频共用一个结构体,因此有的属性是双方公用,有的可能主要用于一方
typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
// *data[]是一个成员为指针的数组
// 原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
uint8_t *data[AV_NUM_DATA_POINTERS];

// data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽
int linesize[AV_NUM_DATA_POINTERS];

uint8_t **extended_data;

int width, height;

/**
* number of audio samples (per channel) described by this frame
*/
// 音频类型中,AVFrame包含的多少个采样
int nb_samples;


// 音视频的格式,
int format;

//帧类型,I帧,P帧,B帧等
enum AVPictureType pict_type;
...

/**
* Presentation timestamp in time_base units (time when frame should be shown to user).
*/
int64_t pts;

/**
* DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
* This is also the Presentation time of this AVFrame calculated from
* only AVPacket.dts values without pts values.
*/
int64_t pkt_dts;

AVRational time_base;

int sample_rate;

AVChannelLayout ch_layout;

int64_t duration;
} AVFrame;

data与linesize

关于data和linesize这两个字段,分别表示原始数据存储数组和每一行的大小。但是数据是如何排列的我们并不清楚。

之前讲视频的基础知识时,我们讲到YUV的数据排列有多种方式,因此想要知道data中的YUV数据排列,我们还需要知道AVFrame的format,这个format来自于AVCodecContext->pix_fmt,这个解码器的参数设置成什么,最终解码出来的杨素格式就是什么。假如不指定的话,默认会解码为YUV420p。

我们假设视频数据解码出来的AVFrame,format是YUV420P,那么data和linesize的数据在ffmpeg中的内存示意图可能是这样的:

image.png

类似的音频数据解码出来的AVFrame,format是AV_SAMPLE_FMT_FLTP,双声道,那么对应的数据在ffmpeg中的内存示意图可能是这样的:

image.png

以上都是planar的存储模式,如果是packed(关于planar/packed的解释见文章)存储模式呢?

YUV422 packed存储格式的视频,ffmpeg中的内存示意图大概是这样的:

image.png

当然,其实对于初学者而言,一般不需要直接操作data和linesize,但是能够把ffmpeg中的数据结构和所学的音视频知识做一个对应理解会更深刻。

对于linsize,音频类型。一般只有linesize[0]会被设置;视频则需要看存储方式的不同,常用的planar模式下,linsize数组一般会用到前三个。
对于data指针数组而言,音频数据占用数组几个的指针要看声道数和存储格式(palnar/packed);视频则只看存储格式,planar一般占3个,packed占

使用方式

  • 创建,申请内存空间
1
2
ini复制代码  // 申请内存空间
AvFrame *pFrame = av_frame_alloc();
  • 解码

解码就是AVPakcet=>AVFrame的过程。

1
2
3
4
5
6
7
scss复制代码/********一次循环************/
// 从输入文件的流中读取数据到packet中
av_read_frame(pFormatCtx, &packet)
// 把AVPacket中的数据发送到解码器
avcodec_send_packet(pCodecCtx,&packet);
// 从解码器中读取数据到AVFrame中
avcodec_receive_frame(pCodecCtx,pFrame);
  • 编码

编码则是AVFrame=>AVPacket的过程(解码的逆过程)。

1
2
3
4
5
6
scss复制代码// av_encode_ctx 编码器的上下文
// pFrame 已获得的原始数据帧
int ret = avcodec_send_frame(av_encode_ctx,pFrame); // 把原始数据发送到编码器

// 从编码器中读取编码后的数据到av_out_packet
ret = avcodec_receive_packet(av_encode_ctx,av_out_packet);
  • 销毁
1
2
scss复制代码// 使用完之后
av_frame_free(&pFrame);

手动填充AVFrame->data

ffmpeg中,我们是通过av_frame_alloc函数来获得AVFrame,但是这个函数只是开辟了AVFrame结构体空间,而avframe->data是一个成员为指针的数组,这些成员指针和它们指向的内存空间并未被开辟出来。我们从源码实现中也能看到:

1
2
3
4
5
6
7
8
9
10
11
12
objectivec复制代码AVFrame *av_frame_alloc(void)
{
// 为AVFrame的结构体开辟空间
AVFrame *frame = av_malloc(sizeof(*frame));

if (!frame)
return NULL;
// 未某些成员赋默认值值(不包括data)
get_frame_defaults(frame);

return frame;
}

是因为在编解码过程中编解码器会帮助我们开辟这块空间,所以我们不必管。

但是假如我们在编解码之外使用AVFrame,比如把YUV类型的AVFrame转换为RGB类型的AVFrame,那么AVFrame->data的空间就需要我们自己开辟了,也需要我们进行释放。

  • 填充AVFrame->data

以视频帧为例

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();

// 通过宽高以及像素格式来计算获得新的帧所需要的缓冲区大小
numBytes= av_image_get_buffer_size(AV_PIX_FMT_RGB24, width,height,1);

// 假设 buffer = 1024byte 表示buffer是指向一个1024个uint_8数据的内存区域的指针
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

// 让pFrameRGB->data数组中几个指针分别指向buffer这块空间(不同位置),
// 然后可以向这块空间填充数据
av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize, buffer,AV_PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height,1);

总结

本文主要详细介绍了ffmpeg中比较重要的几个结构体,他们都伴随着音视频处理的某个阶段而存在的,因此了解他们有助于我们理解音视频的处理流程。

image.png

我们把前面的音视频解码播放流程图添加关键API和搭配关键结构体就会发现ffmpeg的处理流程还是比较简洁的。接下来我们尝试用一个较完整的demo来熟悉ffmpeg的使用方式。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%