FFMpeg SDK使用2调用FFmpeg SDK实现视频编码
Posted 叮咚咕噜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FFMpeg SDK使用2调用FFmpeg SDK实现视频编码相关的知识,希望对你有一定的参考价值。
一、FFMpeg进行视频编码所需要的结构
为了实现调用FFMpeg的API实现视频的编码,以下结构是必不可少的:
- AVCodec:AVCodec结构保存了一个编解码器的实例,实现实际的编码功能。通常我们在程序中定义一个指向AVCodec结构的指针指向该实例。
- AVCodecContext:AVCodecContext表示AVCodec所代表的上下文信息,保存了AVCodec所需要的一些参数。对于实现编码功能,我们可以在这个结构中设置我们指定的编码参数。通常也是定义一个指针指向AVCodecContext
- AVFrame:AVFrame结构保存编码之前的像素数据、图像的宽高格式,并作为编码器的输入数据。其在程序中也是一个指针的形式。
- AVPacket:AVPacket表示码流包结构,包含编码之后的码流数据。该结构可以不定义指针,以一个对象的形式定义。
在我们的程序中,我们将这些结构整合在了一个结构体中:
/*************************************************
Struct: CodecCtx
Description: FFMpeg编解码器上下文
*************************************************/
typedef struct
{
AVCodec *codec; //指向编解码器实例
AVFrame *frame; //保存解码之后/编码之前的像素数据
AVCodecContext *c; //编解码器上下文,保存编解码器的一些参数设置
AVPacket pkt; //码流包结构,包含编码码流数据
} CodecCtx;
二、FFMpeg编码的主要步骤:
1、解析输入参数
2、按要求初始化需要的FFMpeg结构
3、编码过程循环
4、写出码流数据
5、收尾工作
2.1输入编码参数
这一步我们可以设置一个专门的配置文件,并将参数按照某个事写入这个配置文件中,再在程序中解析这个配置文件获得编码的参数。如果参数不多的话,我们可以直接使用命令行将编码参数传入即可。
2.2按照要求初始化需要的FFMpeg结构
首先,所有涉及到编解码的的功能,都必须要注册音视频编解码器之后才能使用。注册编解码调用下面的函数:
avcodec_register_all();
编解码器注册完成之后,根据指定的CODEC_ID查找指定的codec实例。CODEC_ID通常指定了编解码器的格式,在这里我们使用当前应用最为广泛的H.264格式为例。查找codec调用的函数为avcodec_find_encoder,其声明格式为:
AVCodec *avcodec_find_encoder(enum AVCodecID id);
该函数的输入参数为一个AVCodecID的枚举类型,返回值为一个指向AVCodec结构的指针,用于接收找到的编解码器实例。如果没有找到,那么该函数会返回一个空指针。调用方法如下:
/* find the mpeg1 video encoder */
ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根据CODEC_ID查找编解码器对象实例的指针
if (!ctx.codec)
{
fprintf(stderr, "Codec not found\\n");
return false;
}
AVCodec查找成功后,下一步是分配AVCodecContext实例。分配AVCodecContext实例需要我们前面查找到的AVCodec作为参数,调用的是avcodec_alloc_context3函数。其声明方式为:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特点同avcodec_find_encoder类似,返回一个指向AVCodecContext实例的指针。如果分配失败,会返回一个空指针。调用方式为:
ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext实例
if (!ctx.c)
{
fprintf(stderr, "Could not allocate video codec context\\n");
return false;
}
需注意,在分配成功之后,应将编码的参数设置赋值给AVCodecContext的成员。
void setContext(CodecCtx &ctx, IOParam io_param)
{
/* put sample parameters */
ctx.c->bit_rate = io_param.nBitRate;
/* resolution must be a multiple of two */
ctx.c->width = io_param.nImageWidth;
ctx.c->height = io_param.nImageHeight;
/* frames per second */
AVRational rational = {1,25}; //编码帧率,一个分子,一个分母
ctx.c->time_base = rational;
ctx.c->gop_size = io_param.nGOPSize;
ctx.c->max_b_frames = io_param.nMaxBFrames;
ctx.c->pix_fmt = AV_PIX_FMT_YUV420P;
av_opt_set(ctx.c->priv_data, "preset", "slow", 0); //添加一个私有数据
}
现在,AVCodec、AVCodecContext的指针都已经分配好,然后以这两个对象的指针作为参数打开编码器对象。调用的函数为avcodec_open2,声明方式为:
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
该函数的前两个参数是我们刚刚建立的两个对象,第三个参数为一个字典类型对象,用于保存函数执行过程中未能识别的AVCodecContext和另外一些私有设置选项。函数的返回值表示编码器是否打开成功,若成功返回0,失败返回一个负数。调用方式为:
if (avcodec_open2(ctx.c, ctx.codec, NULL) < 0) //根据编码器上下文打开编码器
{
fprintf(stderr, "Could not open codec\\n");
exit(1);
}
然后,我们需要处理AVFrame对象。AVFrame表示视频原始像素数据的一个容器,处理该类型数据需要两个步骤,其一是分配AVFrame对象,其二是分配实际的像素数据的存储空间。分配对象空间类似于new操作符一样,只是需要调用函数av_frame_alloc。如果失败,那么函数返回一个空指针。AVFrame对象分配成功后,需要设置图像的分辨率和像素格式等。实际调用过程如下:
ctx.frame = av_frame_alloc(); //分配AVFrame对象
if (!ctx.frame)
{
fprintf(stderr, "Could not allocate video frame\\n");
return false;
}
ctx.frame->format = ctx.c->pix_fmt;
ctx.frame->width = ctx.c->width;
ctx.frame->height = ctx.c->height;
分配像素的存储空间需要调用av_image_alloc函数,其声明方式为:
int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
该函数的四个参数分别表示AVFrame结构中的缓存指针、各个颜色分量的宽度、图像分辨率(宽、高)、像素格式和内存对其的大小。该函数会返回分配的内存的大小,如果失败则返回一个负值。具体调用方式如:
/*
* 参数1:YUV数据,数组分别表示的是亮度和色度
* 参数2:表示的是一行数据的长度,跨距
* 最后一个参数:对齐32
*/
ret = av_image_alloc(ctx.frame->data, ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt, 32); //32是对齐
if (ret < 0)
{
fprintf(stderr, "Could not allocate raw picture buffer\\n");
return false;
}
2.3编码循环体
到此为止,我们的准备工作已经大致完成,下面开始执行实际编码的循环过程。用伪代码大致表示编码的流程为:
while (numCoded < maxNumToCode)
{
read_yuv_data();
encode_video_frame();
write_out_h264();
}
其中,read_yuv_data部分直接使用fread语句读取即可,只需要知道的是,三个颜色分量Y/U/V的地址分别为AVframe::data[0]、AVframe::data[1]和AVframe::data[2],图像的宽度分别为AVframe::linesize[0]、AVframe::linesize[1]和AVframe::linesize[2]。需要注意的是,linesize中的值通常指的是stride而不是width,也就是说,像素保存区可能是带有一定宽度的无效边区的,在读取数据时需注意。
编码前另外需要完成的操作时初始化AVPacket对象。该对象保存了编码之后的码流数据。对其进行初始化的操作非常简单,只需要调用av_init_packet并传入AVPacket对象的指针。随后将AVPacket::data设为NULL,AVPacket::size赋值0
av_init_packet(&(ctx.pkt)); //初始化AVPacket实例
ctx.pkt.data = NULL; // packet data will be allocated by the encoder
ctx.pkt.size = 0;
成功将原始的YUV像素值保存到了AVframe结构中之后,便可以调用avcodec_encode_video2函数进行实际的编码操作。该函数可谓是整个工程的核心所在,其声明方式为:
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr);
其参数和返回值的意义:
- avctx: AVCodecContext结构,指定了编码的一些参数;
- avpkt: AVPacket对象的指针,用于保存输出码流;
- frame:AVframe结构,用于传入原始的像素数据;
- got_packet_ptr:输出参数,用于标识AVPacket中是否已经有了完整的一帧;
- 返回值:编码是否成功。成功返回0,失败则返回负的错误码
通过输出参数*got_packet_ptr,我们可以判断是否应有一帧完整的码流数据包输出,如果是,那么可以将AVpacket中的码流数据输出出来,其地址为AVPacket::data,大小为AVPacket::size。具体调用方式如下:
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //将AVFrame中的像素信息编码为AVPacket中的码流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\\n");
exit(1);
}
printf("Encode frame index: %d, frame pts: %d.\\n", frameIdx, ctx.frame->pts);
if (got_output)
{
//获得一个完整的码流包
printf("Write packets %3d (size=%5d). Packet pts: %d\\n", packetIdx++, ctx.pkt.size, ctx.pkt.pts);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
因此,一个完整的编码循环提就可以使用下面的代码实现:
for (frameIdx = 0; frameIdx < io_param.nTotalFrames; frameIdx++)
{
av_init_packet(&(ctx.pkt)); //初始化AVPacket实例
ctx.pkt.data = NULL; // packet data will be allocated by the encoder
ctx.pkt.size = 0;
fflush(stdout);
Read_yuv_data(ctx, io_param, 0); //Y分量
Read_yuv_data(ctx, io_param, 1); //U分量
Read_yuv_data(ctx, io_param, 2); //V分量
ctx.frame->pts = frameIdx;
/* encode the image */
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //将AVFrame中的像素信息编码为AVPacket中的码流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\\n");
exit(1);
}
printf("Encode frame index: %d, frame pts: %d.\\n", frameIdx, ctx.frame->pts);
if (got_output)
{
//获得一个完整的码流包
printf("Write packets %3d (size=%5d). Packet pts: %d\\n", packetIdx++, ctx.pkt.size, ctx.pkt.pts);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
}
2.4收尾处理
如果我们就此结束编码器的整个运行过程,我们会发现,编码完成之后的码流对比原来的数据少了一帧。这是因为我们是根据读取原始像素数据结束来判断循环结束的,这样最后一帧还保留在编码器中尚未输出。所以在关闭整个解码过程之前,我们必须继续执行编码的操作,直到将最后一帧输出为止。执行这项操作依然调用avcodec_encode_video2函数,只是表示AVFrame的参数设为NULL即可:
/* get the delayed frames */
for (got_output = 1; got_output; frameIdx++)
{
fflush(stdout);
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), NULL, &got_output); //输出编码器中剩余的码流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\\n");
exit(1);
}
if (got_output)
{
printf("Write frame %3d (size=%5d)\\n", frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
} //for (got_output = 1; got_output; frameIdx++)
此后,我们就可以按计划关闭编码器的各个组件,结束整个编码的流程。编码器组件的释放流程可类比建立流程,需要关闭AVCocec、释放AVCodecContext、释放AVFrame中的图像缓存和对象本身:
avcodec_close(ctx.c);
av_free(ctx.c);
av_freep(&(ctx.frame->data[0]));
av_frame_free(&(ctx.frame));
三、总结
使用FFMpeg进行视频编码的主要流程如:
- 首先解析、处理输入参数,如编码器的参数、图像的参数、输入输出文件;
- 建立整个FFMpeg编码器的各种组件工具,顺序依次为:avcodec_register_all -> avcodec_find_encoder -> avcodec_alloc_context3 -> avcodec_open2 -> av_frame_alloc -> av_image_alloc;
- 编码循环:av_init_packet -> avcodec_encode_video2(两次) -> av_packet_unref
- 关闭编码器组件:avcodec_close,av_free,av_freep,av_frame_free
以上是关于FFMpeg SDK使用2调用FFmpeg SDK实现视频编码的主要内容,如果未能解决你的问题,请参考以下文章
FFMpeg SDK使用8调用FFmpeg SDK实现视频缩放
FFMpeg SDK使用7调用FFmpeg SDK实现视频水印
FFMpeg视频开发与应用基础五调用FFMpeg SDK封装音频和视频为视频文件
FFMpeg SDK使用6调用FFmpeg SDK实现视频文件的转封装