【FFmpeg】编码器流程分析

news2024/11/23 16:00:03

目录

  • 1 编译
  • 2 调用关系
    • 2.1 第一帧没有获取到码流,第二帧获取到一帧码流
    • 2.2 送帧为NULL,刷新编码器获取剩余的全部码流
  • 3 总结
  • 4 感受

1 编译

参考文件doc/examples/encode_video.c,使用x264作为编码器,需要先安装x264,编译方法:

# 先编译FFmpeg
./configure --prefix=/usr/local/ffmpeg --enable-shared --enable-yasm --enable-libx264 --enable-gpl --enable-pthreads --extra-cflags=-I/usr/local/x264/include --extra-ldflags=-L/usr/local/x264/lib
make
make install
# 然后编译example
make examples

编译生成的文件在doc/examples目录下,使用以下命令执行,编码yuv软件内部自动生成,只用设定输出文件名即可

./encode_video test.h264 libx264

2 调用关系

参考文件libavcodec/encode.c,分析送帧函数avcodec_send_frame,该函数会送一帧然后尝试启动编码器编码,伪代码如下,仅摘抄了主要流程

avcodec_send_frame(frame)
 |-- 如果frame为NULL
 |    |-- avci->draining = 1                 // 排水,即刷新编码器
 |-- 否则
 |    |-- encode_send_frame_internal(frame)
 |         |-- AVFrame *dst = avci->buffer_frame
 |         |-- av_frame_ref(dst, src)        // src = frame,获取src的引用
 |-- ret = encode_receive_packet_internal(&avci->buffer_pkt)
 |    |-- 如果avci->draining_done为1,返回EOF
 |    |-- ret = encode_simple_receive_packet(avpkt)
 |    |    |-- while (!avpkt->data)
 |    |         |-- ret = encode_simple_internal
 |    |         |    |-- AVFrame *frame = avci->in_frame;
 |    |         |    |   /* 初始化in_frame->buf[0]为NULL */
 |    |         |    |-- 如果frame->buf[0]NULL,并且avci->draining为0
 |    |         |    |    |-- av_frame_unref(frame);
 |    |         |    |    |-- ret = ff_encode_get_frame(avctx, frame);
 |    |         |    |    |    |-- 如果avci->draining等于1,返回EOF
 |    |         |    |    |    |-- 如果avci->buffer_frame->buf[0]NULL,返回AGAIN
 |    |         |    |    |    |   // 转移buffer_frame引用,然后重置buffer_frame,
 |    |         |    |    |    |   // 即buffer_frame->buf[0]置为了NULL */
 |    |         |    |    |    |-- av_frame_move_ref(frame, avci->buffer_frame)
 |    |         |    |    |-- 如果ret失败且不等于EOF,返回ret
 |    |         |    |-- got_packet = 0
 |    |         |    |-- ret = ff_encode_encode_cb(avpkt,frame, &got_packet)
 |    |         |    |    |-- ret = codec->cb.encode(avpkt, frame, got_packet)
 |    |         |    |    |-- 如果ret成功,并且got_packet大于0
 |    |         |    |    |    |-- 如果avpkt->data不为NULL
 |    |         |    |    |    |    |   // 检查是否使用AVBufferRef进行管理
 |    |         |    |    |    |    |   // 如果没有则进行相关处理
 |    |         |    |    |    |    |-- encode_make_refcounted(avpkt)
 |    |         |    |    |    |-- 否则
 |    |         |    |    |         |-- av_packet_unref(avpkt);
 |    |         |    |-- 如果frame不为NULL
 |    |         |    |    |  // 这里因为第7行已经增加了引用计数,因此yuv buf不会被释放
 |    |         |    |    |  // 但是ref会被释放,也就是frame->buf[0]会被释放
 |    |         |    |    |-- av_frame_unref(frame)
 |    |         |    |-- 如果avci->draining为1,并且got_packet为0
 |    |         |    |    |-- avci->draining_done = 1
 |    |         |    |-- 返回ret
 |    |         |-- ret小于0返回ret
 |    |-- 返回ret
 |-- 如果ret失败,并且不为EOF和AGAIN,返回ret
 |-- 返回0

启动编码之后,编码的结果会存在buffer_pkt中,然后调用avcodec_receive_packet获取编码的码流,该函数也会启动编码器,伪代码如下

avcodec_receive_packet(avpkt)
 |-- 如果buffer_pkt->data不为空
 |    |-- av_packet_move_ref(avpkt, buffer_pkt)
 |-- 否则
 |    |-- ret = encode_receive_packet_internal(avpkt)
 |    |-- 如果ret失败,返回ret
 |-- 返回0

2.1 第一帧没有获取到码流,第二帧获取到一帧码流

第一次送帧的流程:

  1. 调用encode_send_frame_internal,将帧信息存到avci->buffer_frame中,此操作增加buffer_frame的引用计数
  2. 调用encode_receive_packet_internal送帧编码,由于avpkt没有分配内存,因此avpkt->data为NULL,进入while循环,循环中调用encode_simple_internal进行编码,如果获取到码流会将avpkt->data赋值,从而退出循环,另外一种情况是ret为失败,退出循环
  3. 调用encode_simple_internal,获取到in_frame,判断frame->buf[0]是否为NULL,in_frame是在编码器初始化申请的,内部没有分配内存,因此为NULL,然后调用ff_encode_get_frame获取帧信息
  4. 在函数ff_encode_get_frame中,buffer_frame就是第一步获取的帧信息,此处调用了一个move_ref将其转移到in_frame中,转移之后buffer_frame会被清空
  5. 此时frame已经获取到了帧信息,然后调用ff_encode_encode_cb回调底层编码器接口encode,此时由于帧信息刚送进去,还没有完成一帧编码,因此返回encode返回0,但是got_packet为0,表示没有获取到码流,然后unref这个pkt,保证其中没有引用
  6. 返回到34行,判断frame不为NULL,然后调用unref减掉in_frame的引用计数,并且释放掉ref,表示已经被消耗,最后返回ret,回到while循环中
  7. 由于第5步没有获取到码流,因此avpkt->data还是为NULL,循环不退出,再一次调用encode_simple_internal,获取in_frame,由于其中的ref已经被释放了,因此frame->buf[0]为NULL,再次进入判断
  8. 其中再调用ff_encode_get_frame,因为上一次已经将buffer_frame转移到in_frame,已经消耗掉了,因此buffer_frame->buf[0]为NULL,返回AGAIN,回到while循环中
  9. while判断ret小于0,然后返回AGAIN,最后在avcodec_send_frame一层,判断返回值为AGAIN,最终返回0成功
    第一次收流的流程:
  10. 由于送帧没有编码出码流,因此buffer_pkt->data为NULL,会再次调用encode_receive_packet_internal函数来启动编码,in_frame同样为NULL,这一次由于没有送帧,buffer_frame->buf[0]还是为NULL
  11. encode_simple_receive_packet返回AGAIN,avcodec_receive_packet最终返回AGAIN
    由于收流返回AGAIN,因此开始送第二帧:
  12. 拿到新的一帧,首先在encode_send_frame_internal函数中,将buffer_frame赋值,然后调用encode_receive_packet_internal开始编码
  13. 此后的流程和第一次送帧一致,但由于送这一帧时收到上一帧编码的码流,got_packet为1,因此会进入27行的分支
  14. 判断avpkt->data有数据,执行encode_make_refcounted函数,假设返回的pkt在encode内部是使用了AVBufferRef管理的,这里直接返回成功
  15. 如果frame不为NULL,释放减frame的引用计数,释放掉frame的ref,然后返回0,退出到while循环
  16. while循环判断avpkt->data不为NULL,退出循环,返回0成功,最终encode_receive_packet_internal返回成功,码流已经被存放到buffer_pkt当中
    第二次和第三次收流流程:
  17. 由于送帧时已经收到码流,因此buffer_pkt->data不为NULL,调用av_packet_move_ref将buffer_pkt转移到avpkt中, 然后buffer_pkt置空,直接返回成功
  18. 由于第二次收流成功,会进行第三次收流,此时过程和第一次类似,由于没有送帧,第三次返回AGAIN

2.2 送帧为NULL,刷新编码器获取剩余的全部码流

  1. 首先draining置为1,然后调用encode_receive_packet_internal进行编码,由于draining为1,不会进入15行的分支来获取yuv buffer,直接调用ff_encode_encode_cb进行编码
  2. ff_encode_encode_cb中调用encode进行编码,传入frame为NULL,直接获取avpkt,如果不是最后一帧,此处ret应该为0,got_packe为1,返回while循环,avpkt->data不为NULL,退出循环,最终avcodec_receive_packet返回成功
  3. 由于第一次收流成功,会再进行第二次收流,如果不是最后一帧,此时和第二次流程一致,收到第二帧码流,然后继续进行第三次收流
  4. 如果此时是最后一帧了,那么encode接口会返回0,got_packet也为0,avci->draining_done被置为1,最终avcodec_receive_packet返回成功
  5. 由于第三次收流成功,此时再进行第四次收流,此时在第9行被拦截,直接返回EOF
  6. 收到avcodec_receive_packet返回EOF,编码完成退出程序

3 总结

  1. 调用encode之前,会调用av_frame_ref接口将帧信息的引用拷贝到buffer_frame中,并且增加引用计数,此时buffer_frame引用计数为2,编码之前会调用av_frame_move_ref将buffer_frame转移到in_frame中,之后buffer_frame的ref为NULL,in_frame的引用计数是2,编码之后会调用av_frame_unref减引用in_frame计数,减之后引用计数为1
  2. avcodec_receive_packet传入的pkt没有分配buffer,分配buffer在encode接口中完成,分配之后引用计数为1,在avcodec_receive_packet获取到pkt之后,会对pkt进行写文件等操作,完成之后会调用av_packet_unref减pkt的引用计数,此时引用计数减为0,释放内存
  3. 当encode传入的frame为NULL时,需要刷新编码器,若编码比较慢,没有码流准备好,encode返回0并且got_packet为0,此时由于avci->draining为1,avci->draining_done会被置为1,然后在while循环中下一次执行encode_simple_internal,检查avci->draining_done为1,会直接返回EOF,此时encode中还有未编码完成的帧未输出。因此如果frame为NULL,encode应该是阻塞状态,直到新的一包码流准备好才返回

4 感受

减引用计数的函数,如果减到0则释放其中的buffer,大佬甚至不愿单独写个free函数,直接复用replace,传参也是二重指针,提高阅读难度
在这里插入图片描述
像这种复用在ffmpeg中很多,比如encode_simple_internal接口中检查之前有没有送帧实际依靠的是ff_encode_get_frame接口的返回值,该返回值将会影响整个encode_simple_internal接口的返回值,为了可读性这种检查建议显式的写在encode_simple_internal函数中而不是隐含在ff_encode_get_frame返回值中
在这里插入图片描述
再比如ff_encode_encode_cb函数中会调用encode_make_refcounted函数来对encode返回的pkt是否使用了ref进行检查,如果使用了ref即avpkt->buf不为NULL,直接返回成功,这个返回值直接影响了是否有122行的数据拷贝,可能影响零拷贝的设计,这种重要的特性建议也是直接写到ff_encode_encode_cb中,而不是依赖encode_make_refcounted函数的检查,encode_make_refcounted只实现单一的申请新的ref然后数据拷贝功能即可
在这里插入图片描述
另外很多buffer ref相关的接口有隐含的副作用,比如av_frame_move_ref函数将src转移到dts后会重置src的值

在这里插入图片描述
av_frame_unref除了减ref的引用计数之外,还会把ref整个释放掉

在这里插入图片描述
这种副作用通过函数名无法得知,只有通过打开函数看实现或者查API reference才知道。
似乎作者的理念似乎趋向于All in one的设计,即一个函数完成尽可能多的功能,不愿意设计单一功能的接口,也不愿意多写一行重复代码,这样其实提高了理解的难度。

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

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

相关文章

如何用ChatGPT写毕业论文?

老早之前就听说有同学用ChatGPT写论文了 但是一直不觉得人工智能够真的替代人类 正好最近毕业论文开始降重了,超高的重复率愁得我快睡不着了 刷微博突然看到这个有关ChatGPT的问题。 出于好奇的我决定先来试试这个ChatGPT到底能不能帮我解决降重问题哈哈哈 点击…

shell脚本基础之处理脚本输入

处理输入命令行参数读取参数读取脚本名测试参数特殊参数变量参数统计获取所有参数移动变量处理选项查找选项处理简单选项分离选项和参数处理带值的选项getopt 命令命令格式在脚本中使用getoptgetopts命令脚本选项标准化获取用户的输入基本的读取超时隐藏式读取文件中读取在此之…

Linux命令·ifconfig

许多windows非常熟悉ipconfig命令行工具,它被用来获取网络接口配置信息并对此进行修改。Linux系统拥有一个类似的工具,也就是ifconfig(interfaces config)。通常需要以root身份登录或使用sudo以便在Linux机器上使用ifconfig工具。依赖于ifconfig命令中使…

OpenFeign#1 - FeignClient 是如何注册的?

文章目录EnableFeignClientsFeignClientsRegistrarregisterDefaultConfigurationregisterFeignClientsFeignClientFeignClientFactoryBeanFeignContextfeign(FeignContext)EnableFeignClients 该注解会导致 FeignClientsRegistrar 的注入. Retention(RetentionPolicy.RUNTIME…

Redis AOF

一、Redis AOF 1.简介 目前,redis的持久化主要应用AOF(Append Only File)和RDB两大机制。AOF以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令全部记录下来(读操作不记录)。只许追加文件,但不可以改写文件。redis启动之初会读取该文…

自动化测试学习(七)-正则表达式,你真的会用吗?

目录 一、正则表达式在python中如何使用 二、用正则表达式匹配更多模式 三、常用字符分类的缩写代码 总结 所谓正则表达式(regex),就是一种模式匹配,学会用正则匹配,就可以达到事半功倍的效果。 一、正则表达式在…

工程管理系统软件 自主研发,工程行业适用

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下: 首页 工作台:待办工作、消息通知、预警信息,点击可进入相应的列表 项目进度图表:选择(总体或单个)项目显示…

在 Web3 里如何寻找靠谱的创新路径——Solv V3 阶段性复盘(上)

作者:Solv 核心团队Solv V3 是 3 月 21 日发布的,到这周一整整三个星期。三周时间产生的实际交易额超过 6,000 万美元,预计在本月内能够破亿。而且从我们手上的 pipeline 来看,这个增长的势头还将持续下去。在几个月内&#xff0c…

「Long HK, Long Crypto」主题酒会在香港圆满举行!Web3er齐聚一堂共叙行业发展

「2023 香港 Web3 嘉年华」是香港有史以来规模最大的加密货币活动之一,邀请了300多位行业领袖、区块链项目创始人、投资人、监管机构代表、专家学者,为每一位参与者带来前沿的 Web3 技术,共同探讨 Web3 未来发展的关键问题。 Web3相关的各大…

OpenCV实战(19)——特征描述符

OpenCV实战(19)——特征描述符0. 前言1. 特征描述符2. 提升匹配集质量2.1 交叉检查匹配2.2 比率测试2.3 距离阈值3. 完整代码小结系列链接0. 前言 SURF 和 SIFT 关键点检测算法为每个检测到的特征计算位置、方向和比例,比例因子信息可用于定…

RestClient查询文档

文章目录1、RestClient查询文档----快速入门2、查询文档--match、term、range、bool3、查询文档-排序和分页4、高亮1、RestClient查询文档----快速入门 基本步骤 1.先创建SearchRequest对象,调用source方法(DSL)——>相当于红框内的大jso…

最前端|什么是低代码?与传统开发的区别是什么?

目录一、低代码介绍二、背景趋势三、低代码与传统代码开发(一)低代码能否替代传统开发低代码页面传统开发页面(二)相同业务不同方式对比1.低代码开发(1)优点(2)缺点2.传统代码开发&a…

实现一个登录功能方案设计2

需求MySQL表实现方案index页面home页面需求 实现一个登录功能 实现的功能 注册(邮箱注册)登录(邮箱密码)重置密码查看操作记录(登录, 注册, 重置密码, 登出. 都算操作)登出在第一版的基础上进行优化:\ 优化点: 存操作信息请求的post使用中间件进行储存操作信息.避免重复代码 因…

【面试】如何设计SaaS产品的数据权限?

文章目录前言数据权限是什么?设计原则整体方案RBAC模型怎么控制数据权限?1. 数据范围权限控制2. 业务对象操作权限控制3. 业务对象字段权限控制总结前言 一套系统的权限可以分为两类,数据权限和功能权限,今天我们从以下几个点&am…

阿里云服务器安装宝塔面板搭建网站全流程(一步步详解)

阿里云服务器安装宝塔面板教程,云服务器吧以阿里云Linux系统云服务器安装宝塔Linux面板为例,先配置云服务器安全组开放宝塔所需端口8888、888、80、443、20和21端口,然后执行安装宝塔面板命令脚本,最后登录宝塔后台安装LNMP&#…

UML 简易使用教程

最近刚好有空,遂决定对应 UML 常用的一些图进行整理,供自己以及需要的人查阅。 UML 分为静态模型与动态模型。静态模型描述一个系统的静态特征,固定的框架结构。包括用例图、类图、对象图、组件图、部署图;动态模型包括时序图、协…

TensorFlow 智能移动项目:11~12

原文:Intelligent mobile projects with TensorFlow 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 不要担心自己的形象,只…

Flutter 第一个界面

第一个页面 app首页 入口函数 一个Flutter工程的入口函数与Dart命令行工程一样是main,不同的是在Flutter中执行runApp(ArticleApp()) 就能够在手机屏幕上展示这个Widget。 import package:flutter/material.dart; void main() > runApp(new ArticleApp()); Ar…

OpenAI不能访问有什么方法解救呢?试试这方法吧

最近发现国内不挂代理是不能访问到openAI的接口的,为了解决这个问题,我一直在github上需在解决方案,今天终于被我找到一个大神开源了一个解决方案。下面就来看看如何做吧。 整个项目的代码很简单只有几行代码: {"rewrites&q…

几种在Python中List添加、删除元素的方法

嗨害大家好鸭!我是爱摸鱼的芝士❤ 一、python中List添加元素的几种方法 List 是 Python 中常用的数据类型, 它一个有序集合, 即其中的元素始终保持着初始时的定义的顺序 (除非你对它们进行排序或其他修改操作)。 …