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进行视频编码的主要流程如:

  1. 首先解析、处理输入参数,如编码器的参数、图像的参数、输入输出文件;
  2. 建立整个FFMpeg编码器的各种组件工具,顺序依次为:avcodec_register_all -> avcodec_find_encoder -> avcodec_alloc_context3 -> avcodec_open2 -> av_frame_alloc -> av_image_alloc;
  3. 编码循环:av_init_packet -> avcodec_encode_video2(两次) -> av_packet_unref
  4. 关闭编码器组件: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实现视频文件的转封装

FFMpeg SDK使用4调用FFmpeg SDK解析封装格式的视频为音频流和视频流

FFMpeg SDK使用5调用FFmpeg SDK解析封装格式的视频为音频流和视频流