【FFmpeg】自定义编码器适配

news2024/10/5 9:27:26

目录

  • 1 编码流程
    • 1.1 整体流程
    • 1.2 内部流程
  • 2 适配接口
    • 2.1 init、close
    • 2.2 option
    • 2.3 receive
    • 2.4 encode
    • 2.5 零拷贝的设计

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

编码一帧的相关代码如下:

static void encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,
                   FILE *outfile)
{
    int ret;

    /* send the frame to the encoder */
    if (frame)
        printf("Send frame %3"PRId64"\n", frame->pts);

    ret = avcodec_send_frame(enc_ctx, frame);
    if (ret < 0) {
        fprintf(stderr, "Error sending a frame for encoding\n");
        exit(1);
    }

    while (ret >= 0) {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error during encoding\n");
            exit(1);
        }

        printf("Write packet %3"PRId64" (size=%5d)\n", pkt->pts, pkt->size);
        fwrite(pkt->data, 1, pkt->size, outfile);
        av_packet_unref(pkt);
    }
}

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

1.2 内部流程

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

typedef struct AVCodecInternal {
	...
    /**
     * The input frame is stored here for encoders implementing the simple
     * encode API.
     *
     * Not allocated in other cases.
     */
    AVFrame *in_frame;

    /**
     * Temporary buffers for newly received or not yet output packets/frames.
     */
    AVPacket *buffer_pkt;
    AVFrame *buffer_frame;
    ...
} AVCodecInternal;

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

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

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

static int encode_receive_packet_internal(AVCodecContext *avctx, AVPacket *avpkt)
{
	...
	if (ffcodec(avctx->codec)->cb_type == FF_CODEC_CB_TYPE_RECEIVE_PACKET) {
        ret = ffcodec(avctx->codec)->cb.receive_packet(avctx, avpkt);
        if (ret < 0)
            av_packet_unref(avpkt);
        else
            // Encoders must always return ref-counted buffers.
            // Side-data only packets have no data and can be not ref-counted.
            av_assert0(!avpkt->data || avpkt->buf);
    } else
        ret = encode_simple_receive_packet(avctx, avpkt);
    ...
}

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

static int encode_simple_internal(AVCodecContext *avctx, AVPacket *avpkt)
{
    AVFrame          *frame = avci->in_frame;
    const FFCodec *const codec = ffcodec(avctx->codec);
    int got_packet;

	...
	/* 拷贝buffer_frame到in_frame */
	...

    if (CONFIG_FRAME_THREAD_ENCODER && avci->frame_thread_encoder) {
        /* This will unref frame. */
        ret = ff_thread_video_encode_frame(avctx, avpkt, frame, &got_packet);
    } else {
        ret = ff_encode_encode_cb(avctx, avpkt, frame, &got_packet);
#if FF_API_THREAD_SAFE_CALLBACKS
        if (frame) {
            av_frame_unref(frame);
        }
#endif
    }
    ...

    return ret;
}
  • 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,这是英伟达的硬件编码器接口,自定义一个编码器只需实现以下结构体

const FFCodec ff_h264_nvenc_encoder = {
    .p.name         = "h264_nvenc",
    .p.long_name    = NULL_IF_CONFIG_SMALL("NVIDIA NVENC H.264 encoder"),
    .p.type         = AVMEDIA_TYPE_VIDEO,
    .p.id           = AV_CODEC_ID_H264,
    .init           = ff_nvenc_encode_init,
    FF_CODEC_RECEIVE_PACKET_CB(ff_nvenc_receive_packet),
    .close          = ff_nvenc_encode_close,
    .flush          = ff_nvenc_encode_flush,
    .priv_data_size = sizeof(NvencContext),
    .p.priv_class   = &h264_nvenc_class,
    .defaults       = defaults,
    .p.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_HARDWARE |
                      AV_CODEC_CAP_ENCODER_FLUSH | AV_CODEC_CAP_DR1,
    .caps_internal  = FF_CODEC_CAP_INIT_CLEANUP,
    .p.pix_fmts     = ff_nvenc_pix_fmts,
    .p.wrapper_name = "nvenc",
    .hw_configs     = ff_nvenc_hw_configs,
};

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

static const AVOption options[] = {
#ifdef NVENC_HAVE_NEW_PRESETS
    { "preset",       "Set the encoding preset",            OFFSET(preset),       AV_OPT_TYPE_INT,   { .i64 = PRESET_P4 },     PRESET_DEFAULT, PRESET_P7,          VE, "preset" },
#else
    { "preset",       "Set the encoding preset",            OFFSET(preset),       AV_OPT_TYPE_INT,   { .i64 = PRESET_MEDIUM }, PRESET_DEFAULT, PRESET_LOSSLESS_HP, VE, "preset" },
#endif
    { "default",      "",                                   0,                    AV_OPT_TYPE_CONST, { .i64 = PRESET_DEFAULT },             0, 0, VE, "preset" },
    { "slow",         "hq 2 passes",                        0,                    AV_OPT_TYPE_CONST, { .i64 = PRESET_SLOW },                0, 0, VE, "preset" },
    { "medium",       "hq 1 pass",                          0,                    AV_OPT_TYPE_CONST, { .i64 = PRESET_MEDIUM },              0, 0, VE, "preset" },
...
};

static const AVClass h264_nvenc_class = {
    .class_name = "h264_nvenc",
    .item_name = av_default_item_name,
    .option = options,
    .version = LIBAVUTIL_VERSION_INT,
};

2.1 init、close

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

int (*init)(struct AVCodecContext *)

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

int (*close)(struct AVCodecContext *)

2.2 option

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

ypedef struct NvencContext
{
    ...
    // 队列相关的定义
    ...

    // 编码相关的配置信息
    int preset;
    int profile;
    int level;
    int tier;
    int rc;
    int cbr;
	...
} NvencContext;

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

typedef struct AVOption {
    const char *name;

    /**
     * short English help text
     * @todo What about other languages?
     */
    const char *help;

    /**
     * The offset relative to the context structure where the option
     * value is stored. It should be 0 for named constants.
     */
    int offset;
    enum AVOptionType type;

    /**
     * the default value for scalar options
     */
    union {
        int64_t i64;
        double dbl;
        const char *str;
        /* TODO those are unused now */
        AVRational q;
    } default_val;
    double min;                 ///< minimum valid value for the option
    double max;                 ///< maximum valid value for the option

    int flags;
    const char *unit;
} AVOption;

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

av_opt_set(c->priv_data, "preset", "slow", 0);

2.3 receive

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

int ff_nvenc_receive_packet(AVCodecContext *avctx, AVPacket *pkt)
{
    NvencSurface *tmp_out_surf;
    int res, res2;
    NvencContext *ctx = avctx->priv_data;
    AVFrame *frame = ctx->frame;		// 这个是init中申请的

    if (!frame->buf[0]) {
    	// 将buffer_frame引用拷贝到frame中
        res = ff_encode_get_frame(avctx, frame);	
        if (res < 0 && res != AVERROR_EOF)
            return res;
    }

	// 编码一帧,推测是阻塞的,nv相关的函数没有找到介绍,其中存在拷贝
    res = nvenc_send_frame(avctx, frame);		
    if (res < 0) {
        if (res != AVERROR(EAGAIN))
            return res;
    } else
        av_frame_unref(frame);

    if (output_ready(avctx, avctx->internal->draining)) {
    	// 从ready队列中取编码好的surface
        av_fifo_read(ctx->output_surface_ready_queue, &tmp_out_surf, 1);

        res = nvenc_push_context(avctx);
        if (res < 0)
            return res;
		// 拷贝到pkt中
        res = process_output_surface(avctx, pkt, tmp_out_surf);

        res2 = nvenc_pop_context(avctx);
        if (res2 < 0)
            return res2;

        if (res)
            return res;
		// surface再放回unused队列
        av_fifo_write(ctx->unused_surface_queue, &tmp_out_surf, 1);
    } else if (avctx->internal->draining) {
        return AVERROR_EOF;
    } else {
        return AVERROR(EAGAIN);
    }

    return 0;
}

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减引用计数

void av_packet_unref(AVPacket *pkt)

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

typedef struct AVPacket {
    /**
     * A reference to the reference-counted buffer where the packet data is
     * stored.
     * May be NULL, then the packet data is not reference-counted.
     */
    AVBufferRef *buf;
    ...
} AVPacket;

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

struct AVBuffer {
    ...
    void (*free)(void *opaque, uint8_t *data);
	...
};

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

AVBufferRef *av_buffer_create(uint8_t *data, size_t size,
                              void (*free)(void *opaque, uint8_t *data),
                              void *opaque, int flags)

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/421538.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Adobe Illustrator2023(AI2023)图文安装教程

Adobe Illustrator2023(AI2023)简称AI&#xff0c;是一种应用于出版、多媒体和在线图像的工业标准矢量插画的软件。该软件主要应用于印刷出版、海报书籍排版、专业插画、多媒体图像处理和互联网页面的制作等&#xff0c;也可以为线稿提供较高的精度和控制&#xff0c;适合生产任…

【Camunda】 -- Docker 安裝及使用

【Camunda】 -- Docker 安裝及使用1. Docker install Camunda platform1.1 Web2. Big Data -- Postgres1.1 Big Data -- Postgres3.Awakening1.1 Big Data -- PostgresCamunda platform 是一個任務監控的平台。 Camunda Modeler是建模工具。 1. Docker install Camunda platfor…

【超详细教程】解决libxxx.so: cannot open shared object file: No file or directory

一、参考资料 error while loading shared libraries的解决方案 libascend_hal.so: cannot open shared object file:No such…解决办法-云社区-华为云 (huaweicloud.com) 二、相关介绍 1. -lxxx.so命名 lxxx means lib lib-name .solc means libc.so, lltdl means lib…

C++学习从基础到高阶(基于黑马程序员教程)

视频链接&#xff1a;黑马程序员匠心之作|C教程从0到1入门编程,学习编程不再难&#xff08;52个小时&#xff09; C语言中文网&#xff1a;http://c.biancheng.net/cplus/ Visual Studio 2022 下载地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/downloads/ Visu…

软件设计模式 | 动态代理模式

文章目录一、动态代理概述1.1 代理的概述和作用1.2 动态代理的优点1.3 代理对象的创建1.4 代理对象调用方法的执行流程二、动态代理举例2.1 歌手经纪人2.2 业务功能的性能统计2.3 动态代理在 Spring 框架中的应用三、基于子类的动态代理一、动态代理概述 1.1 代理的概述和作用…

英语语法——动词

文章目录分类时态一般时态三单过去式进行时态现在分词完成时态过去分词完成进行时态情态语态否定强调非谓语动词分类 时态 时态用于表示动作发生的时间和状态&#xff0c;它通过谓语动词的形式变化来体现&#xff0c;时态是每个动词必须要有的。在英语中有以下十六种时态&…

MySQL怎么卸载干净

卸载干净MySQL的步骤&#xff1a;首先关闭MySQL服务&#xff1b;然后卸载MySQL软件&#xff1b;接着删除MySQL在电脑中的残存文件&#xff1b;最后删除MySQL注册表信息即可。 很多人都遇到过一个问题&#xff0c;那就是在安装MySQL的时候操作错误&#xff0c;最后结果不是自己想…

C++【深入理解继承】

文章目录一、继承概念与定义二、基类和派生类对象赋值转换三、派生类的默认成员函数&#xff08;1&#xff09;构造函数&#xff08;2&#xff09;拷贝构造函数&#xff08;3&#xff09;赋值重载&#xff08;4&#xff09;析构函数四、复杂的菱形继承及菱形虚拟继承&#xff0…

python 不指定参数个数---args基础用法

前言&#xff1a; 在有些时候&#xff0c;设计函数的时候&#xff0c;可能不知道要传入的参数类型或者参数个数&#xff0c;此时args可以很好地解决。 一、*args的基本用法 1。传入不指定个数的参数&#xff0c; 2。参数的类型也不指定&#xff0c;可以是任意类型数据&…

k8s学习(三十五)飞腾2000+麒麟V10离线部署metrics-server

文章目录1、下载metrics-server配置文件2、下载推送metrics-server镜像3、修改metrics-server配置4、启动metrics-server1、下载metrics-server配置文件 在有网机器上从网站https://github.com/kubernetes-incubator/metrics-server下载 拷贝其到离线机器K8S的master节点。 2…

九龙证券|又3个涨停,退市风险急升!

*ST新海退市危险急剧上升&#xff01; 到4月14日&#xff0c;*ST新海收盘价接连14个买卖日低于1元/股。按照退市新规&#xff0c;若*ST新海在接下来6个买卖日收盘价继续低于1元/股&#xff0c;将触及买卖类强制退市景象而终止上市&#xff0c;公司股票将不进入退市整理期。 面…

Android Audio音量设置原理流程分析

Android Audio音量设置原理流程分析 简介 本篇文章主要介绍Android音量设置从App应用层到framework层执行流程&#xff0c;以及相关的细节和原理分析&#xff0c;建议在阅读此文章前去看博主的混音理论篇的声音的音量属性和声音相关公式的推导章节&#xff0c;这对阅读时理解音…

2023年泰迪杯数据挖掘挑战赛B题完整数据分析与预测(5.针对完整数据的组合预测-机器学习+深度学习)

背景 2023年泰迪杯完整数据最新出炉&#xff0c;博主根据最新完整数据对原来的预测方案进行了调整&#xff0c;采用机器学习深度学习的组合预测来实现最终预测 全部数据已经出炉&#xff0c;可以看出训练样本和预测样本都增加了十倍&#xff0c;这对于数据的处理复杂程度也有…

linux驱动开发 - 04_Linux 设备树学习 - DTS语法

文章目录Linux 设备树学习 - DTS语法1 什么是设备树&#xff1f;2 DTS、DTB和DTC3 DTS 语法3.1 dtsi 头文件3.2 设备节点3.3 标准属性1、compatible 属性2、model 属性3、status 属性4、#address-cells 和#size-cells 属性5、reg 属性6、ranges 属性7、name 属性8、device_type…

FreeRTOS 任务切换

文章目录一、PendSV 异常二、FreeRTOS 任务切换场合1. 执行系统调用 taskYIELD()2. 系统滴答定时器(SysTick)中断 SysTick_Handler三、PendSV 中断服务函数 PendSV_Handler()四、查找下一个要运行的任务 vTaskSwitchContext()五、FreeRTOS 时间片调度六、时间片调度实验RTOS 系…

ECF机制:信号 (Signal)

&#x1f4ad; 写在前面&#xff1a;ECF (异常控制流) 机制是存在于系统的所有层级中的&#xff0c;所以这一块的知识我们需要系统地去学习。前几章我们探讨过了异常 (Exceptions)&#xff0c;由硬件触发&#xff0c;在内核代码中处理。讲解了进程的上下文切换 (Process Contex…

Shiro整合SpringBoot项目实战

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

阿里入局,通义千问备受期待

目录官宣内测体验内容鸟鸟分鸟后言继百度文心一言发布三周之后&#xff0c;4月7日阿里通义大模型终于推出通义千问&#xff0c;阿里正式加入ChatGPT战局。下午市场一片大热&#xff0c;对于深耕NLP多年的阿里&#xff0c;大家有足够的期待。 官宣内测 “你好&#xff0c;我叫通…

【SpringBoot】springboot启动热部署

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ SpringBoot——手工启动热部署一、pom.xml导入…

Kotlin 是后端开发的未来

Kotlin 是后端开发的未来 严格类型、命名参数、多范式语言 您今天遇到的每个后端开发人员都会说他们使用 JavaScript、Python、PHP 或 Ruby 编写代码。近年来&#xff0c;您会遇到一小部分人转而使用 Kotlin 作为他们创建 Web 服务器的语言选择。由于我在学习Ktor&#xff0c;所…