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

FFmpeg进行视频编码所需要的结构

AVCodec:AVCodec结构保存了一个编解码器的实例,实现实际的编码功能。通常我们在程序中定义一个指向AVCodec结构的指针指向该实例。
AVCodecContext:AVCodecContext表示AVCodec所代表的上下文信息,保存了AVCodec所需要的一些参数。对于实现编码功能,我们可以在这个结构中设置我们指定的编码参数。通常也是定义一个指针指向AVCodecContext。
AVFrame:AVFrame结构保存编码之前的像素数据,并作为编码器的输入数据。其在程序中也是一个指针的形式。
AVPacket:AVPacket表示码流包结构,包含编码之后的码流数据。该结构可以不定义指针,以一个对象的形式定义

在编程过程中,我们将这些结构整合到一个结构体中:

  1. /*************************************************
  2. Struct: CodecCtx
  3. Description: FFMpeg编解码器上下文
  4. *************************************************/
  5. typedef struct
  6. {
  7. AVCodec *codec; //指向编解码器实例
  8. AVFrame *frame; //保存解码之后/编码之前的像素数据
  9. AVCodecContext *c; //编解码器上下文,保存编解码器的一些参数设置
  10. AVPacket pkt; //码流包结构,包含编码码流数据
  11. } CodecCtx;

FFMpeg编码的主要步骤

1)输入编码参数
这一步我们可以设置一个专门的配置文件,并将参数按照某个规则写入这个配置文件中,再在程序中解析这个配置文件获得编码的参数。如果参数不多的话,我们可以直接使用命令行将编码参数传入即可。

2).按照要求初始化需要的FFMpeg结构
首先,所有涉及到编解码的功能,都必须要注册音视频编解码之后才能使用。注册编解码用下面的函数:

  1. avcodec_register_all();

编解码注册完成之后,根据指定的CODEC_ID查找指定的codec实例。CODEC_ID通常指定了编解码器的格式,在这里我们使用当前应用最为广泛的H.264格式为例。查找codec调用的函数为avcodec_find_encoder,其声明格式为:

  1. AVCodec *avcodec_find_encoder(enum AVCodecID id);

该函数的输入参数为一个AVCodecID的枚举类型,返回值为一个指向AVcodec结构的指针,用于接收找到的编解码器实例。如果没有找到,那么该函数会返回一个空指针。调用方法如下:

  1. /* find the mpeg1 video encoder */
  2. ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根据CODEC_ID查找编解码器对象实例的指针
  3. if (!ctx.codec)
  4. {
  5. fprintf(stderr,"Codec not found\n");
  6. return false;
  7. }

AVCodec查找成功后,下一步是分配AVCodecContext实例。分配AVCodecContext实例需要我们前面查找到的AVCodec作为参数,调用的是avcodec_alloc_context3函数,其声明方式为:

  1. AVCodecContext *avcodec_alloc_context3(const AVCodec*codec);

其特点同avcodec_find_encoder类似,返回一个指向AVCodecContext实例的指针。如果分配失败,会返回一个空指针。调用方式为:

  1. ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext实例
  2. if (!ctx.c)
  3. {
  4. fprintf(stderr,"Could not allocate video codec context\n");
  5. return false;
  6. }
  1. 需要注意的是,在分配成功之后,应将编码的参数设置赋值给AVCodecContext的成员。
  2. 现在,AVCodecAVCodecContext的指针都已经分配好了,然后以这两个对象的指针作为参数打开编码器对象。调用的函数为avcodec_open2,声明方式为:
  1. int avcodec_open2(AVCodecContext *avctx, const AVCodec*codec, AVDictionary **options);

该函数的前两个参数是我们刚刚建立的两个对象,第三个参数为一个字典类型对象,用于保存函数执行过程中未能识别的AVCodecContext和另外一些私有设置选项。函数的返回值表示编码器是否打开成功,若成功返回0,失败返回一个负数。调用方式为:

  1. if (avcodec_open2(ctx.c, ctx.codec, NULL) < 0) //根据编码器上下文打开编码器
  2. {
  3. fprintf(stderr,"Could not open codec\n");
  4. exit(1);
  5. }
  1. 然后,我们需要处理AVFrame对象。AVFrame表示视频原始像素数据的一个容器,处理该类型数据需要两个步骤,其一是分配AVFrame对象,其二是分配实际的像素数据的存储空间。分配对象空间类似new操作符一样,只是需要调用函数av_frame_alloc。如果失败,那么函数返回一个空指针。AVFrame对象分配成功后,需要设置图像的分辨率和像素格式等。实际调用过程如下:
  1. ctx.frame = av_frame_alloc(); //分配AVFrame对象
  2. if (!ctx.frame)
  3. {
  4. fprintf(stderr,"Could not allocate video frame\n");
  5. return false;
  6. }
  7. ctx.frame->format = ctx.c->pix_fmt;
  8. ctx.frame->width = ctx.c->width;
  9. ctx.frame->height = ctx.c->height;

分配像素的存储空间需要第哦啊用av_image_alloc函数,其声明方式为:

  1. int av_image_alloc(uint8_t *pointers[4], intlinesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);

该函数的四个参数分别表示AVFrame结构中的缓存指针、各个颜色分量的宽度、图像分辨率(宽、高)、像素格式和内存对齐的大小。该函数会返回分配的内存的大小,如果失败则返回一个负值。具体调用方式如下:

  1. ret = av_image_alloc(ctx.frame->data,ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt,32);
  2. if (ret < 0)
  3. {
  4. fprintf(stderr,"Could not allocate raw picture buffer\n");
  5. return false;
  6. }

3)编码循环体
到此为止,我们的准备工作已经大致完成,下面开始执行实际编码的循环过程。用伪代码大致表示编码的流程为:

  1. while (numCoded < maxNumToCode)
  2. {
  3. read_yuv_data();
  4. encode_video_frame();
  5. write_out_h264();
  6. }

其中,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,也就是说,像素保存区可能是带有一定宽度的无效边区的,在读取数据时需注意。

  1. 编码前另外需要完成的操作是初始化AVPacket对象。该对象保存了编码之后的码流数据。对其进行初始化的操作非常简单,只需要调用av_init_packet并传入AVPacket对象的指针。随后将AVPacket::data设为NULLAVPacket::size赋值为0.
  2. 成功将原始的YUV像素值保存到了AVframe结构中之后,便可以调用avcodec_encode_video2函数进行实际的编码操作。该函数可谓是整个工程的核心所在,其声明方式为:
  1. 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。具体调用方式如下:

  1. /* encode the image */
  2. ret = avcodec_encode_video2(ctx.c, &(ctx.pkt),ctx.frame, &got_output); //将AVFrame中的像素信息编码为AVPacket中的码流
  3. if (ret < 0)
  4. {
  5. fprintf(stderr,"Error encoding frame\n");
  6. exit(1);
  7. }
  8. if (got_output)
  9. {
  10. //获得一个完整的编码帧
  11. printf("Write frame %3d (size=%5d)\n", frameIdx,ctx.pkt.size);
  12. fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
  13. av_packet_unref(&(ctx.pkt));
  14. }

因此,一个完整的编码循环提就可以使用下面的代码实现:

  1. /* encode 1 second of video */
  2. for (frameIdx = 0; frameIdx < io_param.nTotalFrames;frameIdx++)
  3. {
  4. av_init_packet(&(ctx.pkt)); //初始化AVPacket实例
  5. ctx.pkt.data =NULL; // packet datawill be allocated by the encoder
  6. ctx.pkt.size =0;
  7. fflush(stdout);
  8. Read_yuv_data(ctx, io_param, 0); //Y分量
  9. Read_yuv_data(ctx, io_param, 1); //U分量
  10. Read_yuv_data(ctx, io_param, 2); //V分量
  11. ctx.frame->pts = frameIdx;
  12. /* encode theimage */
  13. ret =avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //将AVFrame中的像素信息编码为AVPacket中的码流
  14. if (ret < 0)
  15. {
  16. fprintf(stderr, "Error encoding frame\n");
  17. exit(1);
  18. }
  19. if (got_output)
  20. {
  21. //获得一个完整的编码帧
  22. printf("Write frame %3d (size=%5d)\n", frameIdx, ctx.pkt.size);
  23. fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
  24. av_packet_unref(&(ctx.pkt));
  25. }
  26. } //for (frameIdx = 0; frameIdx <io_param.nTotalFrames; frameIdx++)

4)收尾处理
如果我们就此结束编码器的整个运行过程,我们会发现,编码完成之后的码流对比原来的数据少了一帧。这是因为我们是根据读取原始像素数据结束来判断循环结束的,这样最后一帧还保留在编码器中尚未输出。所以在关闭整个解码过程之前,我们必须继续执行编码的操作,直到将最后一帧输出为止。执行这项操作依然调用avcodec_encode_video2函数,只是表示AVFrame的参数设为NULL即可:

  1. /* get the delayed frames */
  2. for (got_output = 1; got_output; frameIdx++)
  3. {
  4. fflush(stdout);
  5. ret =avcodec_encode_video2(ctx.c, &(ctx.pkt), NULL, &got_output); //输出编码器中剩余的码流
  6. if (ret < 0)
  7. {
  8. fprintf(stderr, "Error encoding frame\n");
  9. exit(1);
  10. }
  11. if (got_output)
  12. {
  13. printf("Write frame %3d (size=%5d)\n", frameIdx,ctx.pkt.size);
  14. fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
  15. av_packet_unref(&(ctx.pkt));
  16. }
  17. } //for (got_output = 1; got_output; frameIdx++)
  1. 此后,我们将可以按计划关闭编码器的各个组件,结束整个编码的流程。编码器组件的释放流程可类比建立流程,需要关闭AVCodec、释放AVCodecContext、释放AVFrame中的图像缓存和对象本身:
  1. avcodec_close(ctx.c);
  2. av_free(ctx.c);
  3. av_freep(&(ctx.frame->data[0]));
  4. 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对YUV视频序列进行编码 - 图1