视频由像素格式编码为码流格式是FFMpeg的一项基本功能。通常,视频编码器的输入视频通常为原始的图像像素值,输出格式为符合某种格式规定的二进制码流。
FFmpeg进行视频编码所需要的结构
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结构
首先,所有涉及到编解码的功能,都必须要注册音视频编解码之后才能使用。注册编解码用下面的函数:
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的成员。
现在,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], intlinesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
该函数的四个参数分别表示AVFrame结构中的缓存指针、各个颜色分量的宽度、图像分辨率(宽、高)、像素格式和内存对齐的大小。该函数会返回分配的内存的大小,如果失败则返回一个负值。具体调用方式如下:
ret = av_image_alloc(ctx.frame->data,ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt,32);
if (ret < 0)
{
fprintf(stderr,"Could not allocate raw picture buffer\n");
return false;
}
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.
成功将原始的YUV像素值保存到了AVframe结构中之后,便可以调用avcodec_encode_video2函数进行实际的编码操作。该函数可谓是整个工程的核心所在,其声明方式为:
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket*avpkt, const AVFrame *frame, int *got_packet_ptr);
其参数和返回值的意义:
avctx:AVCodeContext结构,指定了编码的一些参数
avpkt:AVPacket对象的指针,用于保存输出码流
frame:AVframe结构,用于传入元素的像素数据
got_packet_ptr:输出参数,用于标识AVPacket中是否已经有了完整的一帧
返回值:编码是否成功。成功返回0,失败则返回负的错误码
通过输出参数*got_packet_ptr,我们可以判断是否应有一帧完整的码流数据包输出,如果是,那么可以将AVpacket中的码流数据输出出来,其地址为AVPacket::data,大小为AVPacket::size。具体调用方式如下:
/* 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);
}
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));
}
因此,一个完整的编码循环提就可以使用下面的代码实现:
/* encode 1 second of video */
for (frameIdx = 0; frameIdx < io_param.nTotalFrames;frameIdx++)
{
av_init_packet(&(ctx.pkt)); //初始化AVPacket实例
ctx.pkt.data =NULL; // packet datawill 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 theimage */
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);
}
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 (frameIdx = 0; frameIdx <io_param.nTotalFrames; frameIdx++)
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++)
此后,我们将可以按计划关闭编码器的各个组件,结束整个编码的流程。编码器组件的释放流程可类比建立流程,需要关闭AVCodec、释放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