前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FFmpeg自定义编码器适配

FFmpeg自定义编码器适配

原创
作者头像
colourfate
发布2024-02-20 16:54:13
2830
发布2024-02-20 16:54:13

1 编码流程

FFmpeg是一个开源的多媒体框架,底层可对接实现多种编解码器,下面参考文件doc/examples/encode_video.c分析编码一帧的流程

1.1 整体流程

统一的编码流程如下图所示

FFmpeg使用的是引用计数的思想,对于一块buffer,刚申请时引用计数为1,每有一个模块进行使用,引用计数加1,使用完毕后引用计数减1,当减为0时释放buffer。

此流程中需要关注buffer的分配,对于编码器来说,输入buffer是yuv,也就是上图中的frame,输出buffer是码流包,也就是上图中的pkt,下面对这两个buffer进行分析

  1. frame:这个结构体是由av_frame_alloc分配的,但这里并没有分配yuv的内存,yuv内存是av_frame_get_buffer分配的,可见这里输入buffer完全是来自外部的,不需要编码器来管理,编码器只需要根据所给的yuv地址来进行编码就行了
  2. pkt:这个结构体是由av_packet_alloc分配的,也没有分配码流包的内存,可见这里pkt仅仅是一个引用,pkt直接传到了avcodec_receive_packet接口进行编码,完成之后将pkt中码流的内容写到文件,最后调用av_packet_unref接口减引用计数,因此这里pkt是编码器内部分配的,分配完成之后会减pkt的引用计数加1,然后输出到外部,外部使用完毕之后再减引用计数来释放buffer 编码一帧的相关代码如下:

其中avcodec_receive_packet返回EAGAIN表示送下一帧,返回EOF表示编码器内部已经没有码流。

1.2 内部流程

此处分析编码一帧的内部流程,首先看FFmpeg内部编码器的上下文,其中有三个重要结构体

下面结合送帧和收流的接口进行介绍

  • avcodec_send_frame: 送帧接口,将yuv的帧信息赋值到buffer_frame,然后触发一帧编码,将编码出的码流赋值到buffer_pkt
  • avcodec_receive_packet: 收流接口,检查上下文中是否有已经编码好的码流buffer_pkt,如果有则将其返回,如果没有再触发一帧编码,将编码好的码流返回

可见send和receive接口均可触发一帧编码,此处触发一帧编码分为两个流程,receive流程和simple流程,代码片段如下:

如果是receive流程,则直接调用receive_packet接口的回调,该接口中注册定制编码器的接口,完成一帧编码。如果是simple流程,则调用的是encode_simple_receive_packet,这是FFmpeg封装的一个简易流程,其中调用的是encode接口,代码片段如下,详细分析可参考文章:

  • simple流程中会把buffer_frame的引用拷贝到in_frame,然后将in_frame送帧编码,意味着其内部只能缓存一帧,不支持多帧缓存。并且simple流程中,调用send之后,如果调用receive成功获取到一包码流,下一次调用receive将会返回EAGAIN,且不会调用encode接口,因此对于不支持多帧缓存的编码器而言,如果send一帧后,需要receive两包码流,那么获取到一包码流之后receive接口会返回EAGAIN,循环退出进行下一次send,此时上一帧未编码的yuv会被覆盖
  • receive流程中没有该限制,直接调用了receive_packet接口,因此如果需要在ffmpeg适配层做多帧缓存,可以使用receive的流程。另外receive流程没有上述限制,在成功收到一帧码流之后,仍然会调用receive,比较灵活,可以做一些定制化的操作

2 适配接口

适配接口参考ffmpeg/libavcodec/nvenc_h264.c,这是英伟达的硬件编码器接口,自定义一个编码器只需实现以下结构体

这里面最重要三个接口是init、close和receive,还有一个比较重要的数据结构是option,此处写明了编码器支持的具体配置

2.1 init、close

init是初始化编码器的接口,在avcodec_open2中调用,定义接口如下,此接口一般是根据用户的option配置,来对编码器进行相应的初始化

close是关闭编码器的接口,在avcodec_free_context中调用,定义接口如下,该接口完成编码器内部的一些资源释放操作

2.2 option

每个编码器有一个自定义的上下文,其作用是在编码器初始化之前对上下文进行配置,编码器初始化的时候就可以按照用户的配置来初始化,以nvenc为例该上下文的定义为

该上下文在avcodec内部使用,对外不可见,因此需要option的方式开放对外配置的接口,使用一个AVOption来描述一个编码器的配置

其中关键的是offsettype成员,offset描述了这个option在上下文中的偏移量,type描述了成员占据的长度,有这两个信息就可以在不对外暴露内部上下文的情况下,修改其中的值,用户配置option的示例如下

2.3 receive

nvenc在avcodec层实现了多帧缓存,因此他实现的是receive接口,代码片段如下,需要注意这里输入输出都存在拷贝

2.4 encode

nvenc没有实现encode接口,这里参考libavcodec/libx264.c的实现,libx264的流程比较繁琐,总结为流程图如下,x264_encoder_encode为非阻塞接口,内部存在yuv的拷贝,调用后不一定会获取到一帧编码好的码流,但获取到之后,同样需要拷贝到输出pkt中

2.5 零拷贝的设计

通过以上分析,发现两种编码器的实现都存在拷贝,下面分析零拷贝实现的可能性

首先是输入零拷贝,输入yuv是外部申请的,编码器只是使用,对于一个阻塞的编码器(即送帧后需要阻塞等待该帧编码完成),这个设计是相对简单的,只需要将frame的地址告诉编码器即可,从编码开始到结束只有一个yuv buffer,编码完成后意味这一帧也消耗完了;如果是非阻塞的编码器涉及多个buffer缓存在编码器中,该设计过于复杂此处不讨论

然后是输出零拷贝,输出的码流buffer是编码器自己申请的,要实现零拷贝,上层使用完毕之后就需要将该buffer还给编码器,参考FFmpeg的example是有这个动作的,即调用unref减引用计数

AVPacket中实际的码流buffer在buf成员中

该接口将buf的引用计数减到零之后,会进行释放操作,对于AVBufferRef而言,释放操作是可以定制的,只需要将free赋值即可

FFmpeg有相关接口可以生成一个定制的AVBufferRef

这里data是已经分配好的buffer的地址,size是已经分配的buffer的大小,free是对应的释放函数

因此,输出buffer零拷贝可以这样实现,通过相关编码器接口获取到一包码流之后,通过av_buffer_create来生成AVBufferRef,传入的是这包码流的地址和大小,注册free函数为还码流buffer给编码器的函数,将生成的AVBufferRef赋值到AVPacket中返回给上层,上层使用完毕后,调用av_packet_unref即可向编码器还码流。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 编码流程
    • 1.1 整体流程
      • 1.2 内部流程
      • 2 适配接口
        • 2.1 init、close
          • 2.2 option
            • 2.3 receive
              • 2.4 encode
                • 2.5 零拷贝的设计
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档