网站首页 > 技术文章 正文
写在前面
最近在做和转码有关的项目,接触到ffmpeg这个神器。从一开始简单的写脚本直接调用ffmpeg的可执行文件做些转码的工作,到后来需要写程序调用ffmpeg的API。虽然上网搜了别人的demo稍微改改顺利完成了工作,但是对于ffmpeg这个黑盒子,还是有些好奇心和担心(项目中使用不了解的代码总是不那么放心),于是抽空翻了翻ffmpeg的源码,整理成文章给大家分享分享。
由于我并非做音频出身,对于音频一窍不通。ffmpeg整个也非常庞大,所以这篇文章从ffmpeg提供的转码的demo开始,侧重于讲清楚整个输入->转码->输出的流程,并学习ffmpeg如何做到通用和可扩展性。
注:本文基于ffmpeg提供的transcode_aac.c样例。
三个重点
转码的过程是怎么样的?简单来说就是从输入读取数据,解析原来的数据格式,转成目标数据格式,再将最终数据输出。这里就涉及到三个点:数据输入和输出方式,数据的编码方式及数据的容器格式(容器是用来区分不同文件的数据类型的,而编码格式则由音视频的压缩算法决定,一般所说的文件格式或者后缀名指的就是文件的容器。对于一种容器,可以包含不同编码格式的一种视频和音频)。
ffmpeg是一个非常非常通用的工具,支持非常广的数据输入和输出,包括:hls流,文件,内存等,支持各类数据编码格式,包括:aac,mp3等等,同时支持多种容器格式,包括ts,aac等。另外ffmpeg是通过C语言实现的,如果是C++,我们可以通过继承和多态来实现。定义一个IO的基类,一个Format的基类和一个Codec的基类,具体的输入输出协议继承IO基类实现各自的输入输出方法,具体的容器格式继承Format基类,具体的编码格式继承Codec基类。这篇文章也会简单讲解ffmpeg如何用C语言实现类似C++的继承和多态。
基本数据结构
ffmpeg转码中最基本的结构为AVFormatContext和AVCodecContext。AVCodecContext负责编码,AVFormatContext负责IO和容器格式。
我从AVFormatContext类抽离出三个基本的成员iformat,oformat,pb。分别属于AVInputFormat,AVOutputFormat,AVIOContext类。iformat为输入的数据格式,oformat为输出的数据格式,pb则负责输入输出。
我把这三个类的定义抽离了出来简化了下,可以看出AVInputFormat声明了read_packet方法,AVOutputFormat声明了write_packet方法,AVIOContext声明了read_packet, write_packet方法。同时AVInputFormat和AVOutputFormat还有一个成员变量name用以标识该格式的后缀名。
下一节我们会看到Input/OutputForm的read/write packet方法和IOContext的关系。
输入函数调用图
下面是初始化输入的整个过程的函数调用图。
首先从调用open_input_file开始,首先解析输入的protocol。avio_open2函数会调用一系列helper函数(ffurl_open,ffio_fdopen)分析输入的协议,设置AVFormatContext的pb变量的read_packet方法。而av_probe_input_buffer2函数则会分析输入文件的格式(从文件名解析或输入数据做判断),设置AVFormatContext的iformat的read_packet方法。
两个read_packet有什么关系呢?第二个函数调用图可以看出,iformat的read_packet最终会调用pb的read_packet方法。意思就是数据本身由pb的read_packet方法来读取,而iformat则会在输入的数据上做些格式相关的解析操作(比如解析输入数据的头部,提取出输入数据中真正的音频/视频数据,再加以转码)。
相关视频推荐
如何设计一个RTMP-RTSP-WebRTC流媒体服务器【音视频开发】
学习地址:https://ke.qq.com/course/3202131?flowToken=1039205
需要更多ffmpeg/webrtc..音视频流媒体开发学习资料加群812855908领取
IO相关代码
直接看上面的图不太直观,这一节我把源码中各个步骤截图下来进行分析。
转码开始步骤,调用open_input_file函数,传入文件名。
avformat_open_input函数会调用init_input()来处理输入文件。
init_input函数主要做两个事情,一是解析输入协议(如何读取数据?hls流?文件?内存?),二是解析输入数据的格式(输入数据为aac?ts?m4a?)
avio_open2函数首先调用ffurl_open函数,根据文件名来推断所属的输入协议(URLProtocol)。之后再调用ffio_fdopen设置pb的read_packet方法。
上面几段代码的逻辑为:根据文件名查找对应的URLProtocol->把该URLProtocol赋值给URLContext的prot成员变量->创建AVIOContext实例,赋值给AVFormatContext的pb成员变量。
这里设置了AVIOContext实例的read_packet为ffurl_read方法。
ffurl_read方法其实就是调用URLContext的prot(上面赋值的)的url_read方法。通过函数指针去调用具体的URLContext对象的prot成员变量的url_read方法。
接下来看看解析输入数据格式的代码。av_probe_input_buffer2函数调用av_probe_input_format2函数来推断数据数据的格式。从之前的图我们知道*fmt其实就是&s->iformat。因此这里设置了AVFormatContext的iformat成员变量。
至此AVFormatContext对象的iformat和pb成员变量就设置好了。接下来看看如何读取输入开始转码。
av_read_frame函数调用read_frame_internal函数开始读取数据。
read_frame_internal会调用ff_read_packet,后者最终调用的是iformat成员变量的read_packet方法。
拿aac举例,aac的read_packet方法实际上是ff_raw_read_partial_packet函数。
ff_raw_read_partial_packet会调用ffio_read_partial,后者最终调用的是AVFormatContext的pb成员变量的read_packet方法。而我们知道pb成员的read_packet其实就是ffurl_read,也就是具体输入URLProtocl的read_packet方法。
至此已经走完了整个输入的流程,输出也是类似的代码,这里就不再赘述。
转码函数调用图
上面关于IO的介绍我从输入的角度进行分析。接下来的转码过程我则从输出的角度进行分析。下图是转码过程的函数调用图(做了简化)。load_encode_and_write调用encode_audio_frame, encode_audio_frame调用avcodec_encode_audio2来做实际的编码工作,最后调用av_write_frame将编码完的数据写入输出。
转码相关代码
首先需要设置输出目标编码格式,下面的代码为设置编码格式(aac)的片段:
在这里设置了output_codec_context(AVCodecContext类对象)之后,从前面的函数调用图,我们知道是avcodec_encode_audio2函数执行的转码过程:
这里看到调用了avctx(AVCodecContext类对象)的codec(AVCodec类对象)成员变量的encode2方法去做编码操作。
转码这里专业性比较强,我并没有细读,因此这里简单带过。
总结
可以看出ffmpeg大量使用函数指针来实现类似C++的继承/多态的效果。并且ffmpeg具有非常好的扩展性。如果我需要自定义一个新的输入协议,只需要自己定义一个新的URLProtocol对象,实现read_packet方法即可。如果需要自定义一个新的容器格式,只需要定义一个新的AVInputFormat对象,实现read_packet方法即可。如果需要自定义一个新的编码格式,只需要定义一个新的AVCodec对象,实现encode2方法即可。真是非常赞的代码架构设计!
- 上一篇: Linux驱动基础篇:hello驱动
- 下一篇: 如何将Python函数输出内容同时打印到屏幕和文件
猜你喜欢
- 2024-11-22 正点原子I.MX6U嵌入式Linux C应用编程:第二章《文件I/O基础》
- 2024-11-22 如何将Python函数输出内容同时打印到屏幕和文件
- 2024-11-22 Linux驱动基础篇:hello驱动
- 2024-11-22 Python自带的库(open函数)读写txt、csv、json、XML、Excel文件
- 2024-11-22 UG NX OPEN二次开发实例:UF,C语言编程,创建圆柱体,API文档翻译
- 2024-11-22 openGauss SEQUENCE函数
- 2024-11-22 Python文件操作的步骤
- 2024-11-22 Python读取与写入Excel模块:openpyxl
- 2024-11-22 PHP imap_open函数任意命令执行漏洞
- 2024-11-22 linux C语言之文件操作
- 1507℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 506℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 486℃MySQL service启动脚本浅析(r12笔记第59天)
- 466℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 464℃启用MySQL查询缓存(mysql8.0查询缓存)
- 444℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 423℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 419℃MySQL server PID file could not be found!失败
- 最近发表
-
- netty系列之:搭建HTTP上传文件服务器
- 让deepseek教我将deepseek接入word
- 前端大文件分片上传断点续传(前端大文件分片上传断点续传怎么操作)
- POST 为什么会发送两次请求?(post+为什么会发送两次请求?怎么回答)
- Jmeter之HTTP请求与响应(jmeter运行http请求没反应)
- WAF-Bypass之SQL注入绕过思路总结
- 用户疯狂点击上传按钮,如何确保只有一个上传任务在执行?
- 二 计算机网络 前端学习 物理层 链路层 网络层 传输层 应用层 HTTP
- HTTP请求的完全过程(http请求的基本过程)
- dart系列之:浏览器中的舞者,用dart发送HTTP请求
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)