FFmpeg 实现视频编码、解码
本例子实现的是将视频域 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);
}
编码的大致流程已经完成了,剩余的是一些收尾工作,比如释放分配的内存、结构体等等。