本例子实现的是将视频域 YUV 数据编码为压缩域的帧数据,编码格式包含了 H.264/H.265/MPEG1/MPEG2 四种 CODEC 类型。

实现的过程,可以大致用如下图表示:

从图中可以大致看出视频编码的流程:

首先要有未压缩的 YUV 原始数据。

其次要根据想要编码的格式选择特定的编码器。

最后编码器的输出即为编码后的视频帧。

根据流程可以推倒出大致的代码实现:

存放待压缩的 YUV 原始数据。此时可以利用 FFMpeg 提供的 AVFrame 结构体,并根据 YUV 数据来填充 AVFrame 结构的视频宽高、像素格式;根据视频宽高、像素格式可以分配存放数据的内存大小,以及字节对齐情况。

获取编码器。利用想要压缩的格式,比如 H.264/H.265/MPEG1/MPEG2 等,来获取注册的编解码器,编解码器在 FFMpeg 中用 AVCodec 结构体表示,对于编解码器,肯定要对其进行配置,包括待压缩视频的宽高、像素格式、比特率等等信息,这些信息,FFMpeg 提供了一个专门的结构体 AVCodecContext 结构体。

存放编码后压缩域的视频帧。FFMpeg 中用来存放压缩编码数据相关信息的结构体为 AVPacket。最后将 AVPacket 存储的压缩数据写入文件即可。

AVFrame 结构体的分配使用av_frame_alloc()函数,该函数会对 AVFrame 结构体的某些字段设置默认值,它会返回一个指向 AVFrame 的指针或 NULL指针(失败)。

AVFrame 结构体的释放只能通过av_frame_free()来完成。

注意,该函数只能分配 AVFrame 结构体本身,不能分配它的 data buffers 字段指向的内容,该字段的指向要根据视频的宽高、像素格式信息手动分配,本例使用的是av_image_alloc()函数。

代码实现大致如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

//allocate AVFrame struct

AVFrame *frame = NULL;

frame = av_frame_alloc();

if(!frame){

printf("Alloc Frame Fail\n");

return -1;

}

//fill AVFrame struct fields

frame->width = width;

frame->height = height;

frame->pix_fmt = AV_PIX_FMT_YUV420P;

//allocate AVFrame data buffers field point

ret = av_image_alloc(frame->data, frame->linesize, frame->width, frame->height, frame->pix_fmt, 32);

if(ret < 0){

printf("Alloc Fail\n");

return -1;

}

//write input file data to frame->data buffer

fread(frame->data[0], 1, frame->width*frame->height, pInput_File);

...

av_frame_free(frame);

编解码器相关的 AVCodec 结构体的分配使用avcodec_find_encoder(enum AVCodecID id)完成,该函数的作用是找到一个与 AVCodecID 匹配的已注册过得编码器;成功则返回一个指向 AVCodec ID 的指针,失败返回 NULL 指针。

该函数的作用是确定系统中是否有该编码器,只是能够使用编码器进行特定格式编码的最基本的条件,要想使用它,至少要完成两个步骤:

根据特定的视频数据,对该编码器进行特定的配置;

打开该编码器。

针对第一步中关于编解码器的特定参数,FFMpeg 提供了一个专门用来存放 AVCodec 所需要的配置参数的结构体 AVCodecContext 结构。

它的分配使用avcodec_alloc_context3(const AVCodec *codec)完成,该函数根据特定的 CODEC 分配一个 AVCodecContext 结构体,并设置一些字段为默认参数,成功则返回指向 AVCodecContext 结构体的指针,失败则返回 NULL 指针。

分配完成后,根据视频特性,手动指定与编码器相关的一些参数,比如视频宽高、像素格式、比特率、GOP 大小等。最后根据参数信息,打开找到的编码器,此处使用avcodec_open2()函数完成。

代码实现大致如下:

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

AVCodec *codec = NULL;

AVCodecContext *codecCtx = NULL;

//register all encoder and decoder

avcodec_register_all();

//find the encoder

codec = avcodec_find_encoder(codec_id);

if(!codec){

printf("Could Not Find the Encoder\n");

return -1;

}

//allocate the AVCodecContext and fill it's fields

codecCtx = avcodec_alloc_context3(codec);

if(!codecCtx){

printf("Alloc AVCodecCtx Fail\n");

return -1;

}

codecCtx->bit_rate = 4000000;

codecCtx->width = frameWidth;

codecCtx->height = frameHeight;

codecCtx->time_base= (AVRational){1, 25};

//open the encoder

if(avcodec_open2(codecCtx, codec, NULL) < 0){

printf("Open Encoder Fail\n");

}

存放编码数据的结构体为 AVPacket,使用之前要对该结构体进行初始化,初始化函数为av_init_packet(AVPacket *pkt),该函数会初始化 AVPacket 结构体中一些字段为默认值,但它不会设置其中的 data 和 size 字段,需要单独初始化,如果此处将 data 设为 NULL、size 设为 0,编码器会自动填充这两个字段。

有了存放编码数据的结构体后,我们就可以利用编码器进行编码了。

FFMpeg 提供的用于视频编码的函数为avcodec_encode_video2,它作用是编码一帧视频数据,该函数比较复杂,单独列出如下:

1

2

int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,

const AVFrame *frame, int *got_packet_ptr);

它会接收来自 AVFrame->data 的视频数据,并将编码数据放到 AVPacket->data 指向的位置,编码数据大小为 AVPacket->size。

其参数和返回值的意义:

avctx: AVCodecContext 结构,指定了编码的一些参数;

avPkt: AVPacket对象的指针,用于保存输出的码流;

frame:AVFrame结构,用于传入原始的像素数据;

got_packet_ptr:输出参数,用于标识是否已经有了完整的一帧;

返回值:编码成功返回 0, 失败返回负的错误码;

编码完成后就可将AVPacket->data内的编码数据写到输出文件中;代码实现大致如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

AVPacket pkt;

//init AVPacket

av_init_packet(&pkt);

pkt.data = NULL;

pkt.size = 0;

//encode the image

ret = avcodec_encode_video2(codecCtx, &pkt, frame, &got_output);

if(ret < 0){

printf("Encode Fail\n");

return -1;

}

if(got_output){

fwrite(pkt.data, 1, pkt.size, pOutput_File);

}

编码的大致流程已经完成了,剩余的是一些收尾工作,比如释放分配的内存、结构体等等。