【FFMpeg视频开发与应用基础】二、调用FFmpeg SDK对YUV视频序列进行编码

  1. 云栖社区>
  2. 博客>
  3. 正文

【FFMpeg视频开发与应用基础】二、调用FFmpeg SDK对YUV视频序列进行编码

jerry.yin 2016-06-07 22:22:11 浏览1577
展开阅读全文

《FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK》视频教程已经在“CSDN学院”上线,视频中包含了从0开始逐行代码实现FFMpeg视频开发的过程,欢迎观看!链接地址:FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK

工程代码地址:FFmpeg_Tutorial


视频由像素格式编码为码流格式是FFMpeg的一项基本功能。通常,视频编码器的输入视频通常为原始的图像像素值,输出格式为符合某种格式规定的二进制码流。


1、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;

2、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], int linesizes[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: AVCodecContext结构,指定了编码的一些参数;
  • 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 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);
    }

    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++) 

此后,我们就可以按计划关闭编码器的各个组件,结束整个编码的流程。编码器组件的释放流程可类比建立流程,需要关闭AVCocec、释放AVCodecContext、释放AVFrame中的图像缓存和对象本身:

avcodec_close(ctx.c);
av_free(ctx.c);
av_freep(&(ctx.frame->data[0]));
av_frame_free(&(ctx.frame));

3、总结

使用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

网友评论

登录后评论
0/500
评论
jerry.yin
+ 关注