二十秒
0:00一秒。
两秒。
三秒。
一条摄像头画面还在等待下一帧。
负责等待的不是人。它是 Frigate 里的一段 watchdog。Frigate 是一款开源的网络录像与视频检测软件;在它的 Python 代码里,这个 watchdog 每秒醒来一次,检查摄像头最近有没有送来新的画面。
十九秒过去。
第二十秒,时间戳仍然没有变化。
代码把摄像头标成离线,清空帧率,然后写下一行日志:二十秒没有收到画面,准备退出 FFmpeg。
[201]
引用 [201]
固定源码支撑检查频率、二十秒条件和 offline 动作;这是代码路径,不声称已取得某次真实摄像头事故录音。
frigate/video.py at revision df27e04c, CameraWatchdog.run: one-second loop and current_frame older than 20 seconds branch
接下来的动作很像一套排练过的急救程序。
Frigate 先向正在运行的 FFmpeg 子进程发送 terminate,请它自己收尾。
然后等待。
最多三十秒。
如果进程仍然没有退出,Frigate 不再请求。它直接 kill 掉这个进程,收走残留输出,再启动一个新的 FFmpeg。
重启之前,它还会留下旧进程最后一百行日志。重启之后,它不会因为看见一个新进程号就宣布恢复。下一帧必须真的从标准输出管道里读出来,新的录制分片也必须继续出现。
[202]
引用 [202]
源码分别支撑 terminate、三十秒等待、kill、最后一百行日志和 stdout 取帧;口播把相关控制路径连续化,不表示每次重启都必然走到 force kill。
frigate/video.py at revision df27e04c, stop_ffmpeg lines 62-73, reset_capture_thread log dump, and capture_frames stdout read path
奇怪的地方就在这里。
Frigate 把最关键的视频路径交给了 FFmpeg。摄像头的网络流由它打开,视频由它解码,画面由它缩放,录制文件也由它封装。
可 Frigate 又不敢把这件事完全交出去。它在进程外面数秒,记住最后一帧,检查分片,保存日志,还要决定什么时候结束、什么时候重来。
如果直接调用 FFmpeg 的动态库,应用可以自己握住 decoder、frame 和错误状态。
Frigate 没有这样做。它启动了一个命令。
二零二六年八月四日,FFmpeg 发布了九点零。
官方发布说明的标题,在版本号后面放了一个很短的代号。
Lei。
五个月前,FFmpeg 开发邮件列表正在讨论新版本的名字。中国开发者刘歧提议用 Lei 纪念雷霄骅。他列出的贡献,不是一项写进 FFmpeg 主仓库的著名功能,而是 FFmpeg API 示例、源码分析、编解码与封装格式讲解,还有许多可以直接跑起来的最小程序。
雷霄骅自己也写过,他整理这些 demo,是希望帮助音视频技术的初学者。
这封邮件不是一份完整的命名表决记录。但当九点零最终真的叫作 Lei,雷霄骅的名字,也就被留在了 FFmpeg 的大版本旁边。
一边,是帮助人进入 FFmpeg API 和源码的最小程序。
另一边,是 Frigate 交给机器长期运行、还要由 watchdog 守住的生产命令。
它们不是同一种工作。雷霄骅的示例让开发者可以越过命令,看见底层 API 怎样运转;Frigate 却把自己的边界,停在了 FFmpeg 进程之外。
九点零的代号没有替我们给出答案。它只是提醒我们,眼前这项选择,不能简单归结为底层接口无人讲解。
[223]
引用 [223]
官方发布材料只支撑 9.0 与 Lei 代号;邮件支撑刘歧的纪念提议与理由,作者页支撑帮助初学者的目标,不把提议写成完整命名决议或用传播数字量化影响。
- FFmpeg 9.0 Lei release
- Liu Qi proposal to name an FFmpeg release Lei
- Lei Xiaohua's audio and video learning projects
- OSCHINA report on FFmpeg 9.0 Lei
FFmpeg 9.0 RELEASE_NOTES title and 2026-08-04 release identity; Liu Qi ffmpeg-devel proposal dated 2026-03-15; Lei Xiaohua homepage purpose and example categories; OSCHINA rendered 2026-08-04 Chinese news entry
命令不是人写的
4:05先沿 Frigate 的代码,回到进程启动之前。
用户配置的是摄像头地址、检测尺寸、帧率、录制方式和硬件加速。Frigate 要把这些产品概念翻译成 FFmpeg 能理解的参数。
树莓派有自己的解码器。Intel 可以走 Quick Sync。NVIDIA 可以使用 CUDA、NVDEC 和 NVENC。Jetson、VAAPI、纯软件解码,又是另外几组参数。RTSP 输入怎样超时,检测画面怎样缩放,录制输出是否复制原始码流,也都要进入同一组 argv。
argv 是操作系统传给可执行文件的一串参数。它不是把整行文本扔给 shell 再解释一次。Frigate 先在 Python 里选出 preset,拼成参数数组,然后用 Popen 启动 FFmpeg。
从这一刻起,摄像头配置不再是一组 Python 对象。它变成一份等待执行的媒体计划。
[203]
引用 [203]
源码支撑多硬件 preset、参数数组和 Popen 执行;不证明跨设备便利就是 Frigate 最初选型时的主观理由。
frigate/ffmpeg_presets.py at revision 4c7ea011, hardware decode / scale / encode preset maps; frigate/video.py start_or_restart_ffmpeg Popen paths
把摄像头暂时放在一边,去看一款桌面软件。
LosslessCut 用 Electron 做界面。用户拖动时间线、选择片段、点击导出,界面背后的 Node 主进程会找到打包在应用里的 FFmpeg,再用 execa 把参数数组交给它。软件甚至保留最近一次命令,让用户可以复制、修改,再回到终端里重跑。
换成 Go 写的照片服务,结构也没有消失。PhotoPrism 在自己的源码里维护一层 internal/ffmpeg。它对这层代码的描述很直接:用可预测、可测试的方式包装 FFmpeg CLI。软件编码器、Apple VideoToolbox、Intel Quick Sync、NVIDIA、VAAPI 和 ARM 设备分别有 command builder,但 CLI、后台 worker 和测试共享同一组 option structs。
再把尺度放大。
二零二六年三月,Meta 的视频基础设施团队公开说,他们每天执行 ffmpeg 和 ffprobe 二进制数百亿次,每天处理超过十亿次视频上传。
规模没有消灭命令。它只是让命令不再由人手写。
图形界面、服务配置、队列任务和内部 worker,都可以生成同一种东西:一组参数,一个进程,一份输出。
[204]
引用 [204]
三项采用者材料共同支撑桌面、Go 服务和超大规模 worker 的 CLI 执行;案例为目的性选择,数百亿与十亿均为 Meta 自述,不用于估算行业占比。
- LosslessCut FFmpeg process layer
- PhotoPrism FFmpeg command builders
- FFmpeg at Meta: Media processing at scale
LosslessCut README recent-command feature and src/main/ffmpeg.ts runFfmpegProcess; PhotoPrism internal/ffmpeg README Overview, Encoders and Testing; Meta Engineering article opening on binary executions and upload scale
这改变了“命令行”三个字的含义。
它不一定是用户界面。
它可以是一个编译目标。
产品先用自己的语言描述任务:裁掉哪一段,检测多大画面,生成什么预览,输出哪些清晰度。command builder 再把这些领域对象翻译成 FFmpeg 参数。
于是,一个 Electron 应用、一段 Python 服务、一个 Go worker,不需要共享类定义,不需要共享异常体系,也不需要把 C 语言里的 packet 和 frame 暴露给各自的运行时。
它们只要学会生成 argv,找到正确的二进制,然后等待。
但一组字符串为什么足以承载这么多媒体工作?
答案藏在 FFmpeg 对一条命令的解释方式里。
一条可执行的媒体计划
7:12我最初把 FFmpeg CLI 想成一层薄包装。
底下是真正工作的 libavcodec、libavformat、libavfilter;上面的命令只负责读取几个选项,再调用这些库。
照这个模型,专业系统迟早应该绕过命令。直接调用库,可以少启动一次进程,也可以得到更细的错误和内存控制。
这个直觉只对了一半。
FFmpeg 的确是一组库。应用也确实可以链接它们。
但 ffmpeg 可执行文件包装的不是某一个 decoder,也不是某一个转换函数。它接管的是一项媒体任务从输入到输出的完整生命周期。
看一份视频文件。
它首先是一个 container。MP4、Matroska 或其他容器里,可以同时放视频、音频、字幕和元数据。
demuxer 打开输入,把容器里的流拆开。拆出来的还不是可以直接显示的画面,而是一包包压缩数据,也就是 packet。
decoder 接住 packet,把它还原成 frame。视频 frame 是像素,音频 frame 是一段采样。filter 可以缩放画面、改变帧率、混合声音、叠加字幕,也可以把多路输入接成一张更复杂的图。
处理完成以后,encoder 把 frame 再次压缩成 packet。muxer 最后把视频、音频和字幕按时间交错,写进输出文件或网络流。
一条命令可以有多个输入,也可以有多个输出。-map 决定哪一路进入哪个输出,filtergraph 决定中间怎样连接。
输入、demux、decode、filter、encode、mux、输出。
这不是一个函数调用。它是一条完整生产线。
[205]
引用 [205]
官方当前文档支撑数据流模型;口播为概念化简,不覆盖全部自动选择、option 作用域、streamcopy 与错误规则。
FFmpeg doc/ffmpeg.texi, Detailed description and Stream selection: demuxers, packets, decoders, frames, filtergraphs, encoders, muxers, multiple inputs / outputs and -map
如果应用直接调用 libav,这条生产线不会凭空出现。
应用要自己打开输入,找到流,选择 decoder,循环读取 packet,把 packet 送去解码,再接住一帧或多帧输出。它要处理 time base,知道视频的九十毫秒和音频的九十毫秒是不是同一个播放位置。它还要决定何时刷新 decoder,何时结束 filter,何时让 encoder 输出最后几包数据,失败时又应该按什么顺序释放对象。
这些控制很有价值。播放器需要它,实时渲染器需要它,要把 frame 直接交给 GPU 或模型的程序也需要它。
可对另一类任务,应用真正想说的只是:
这是输入。
这是我要的变换。
这是输出。
开始。完成以后告诉我。
FFmpeg CLI 把中间那条生产线藏进独立进程。它没有让 demux、时间戳和对象生命周期消失,只是让宿主应用不再亲自拥有它们。
回到 Frigate,就能看到这个边界为什么诱人。
摄像头可以从 RTSP 或 HTTP 进入。FFmpeg 负责连接、探测、解码、缩放和像素格式转换。检测路径不需要理解 H.264 或 H.265 的 packet,它只从 stdout 读取固定大小的 rawvideo frame,再把这些 frame 放进共享内存和检测队列。
录制路径可以是另一项 FFmpeg job。它关心的是持续生成 segment,尽量保留原始码流,或者按配置转码。
对 Frigate 来说,decoder 内部怎样管理参考帧不是产品问题。二十秒没有新画面,才是产品问题。
FFmpeg 接走了媒体数据面。Frigate 留下了业务判断。
[206]
引用 [206]
源码支撑进程角色、rawvideo pipe 和 preset;‘数据面 / 业务判断’是节目对责任边界的工程归纳,不冒充项目方选型原话。
frigate/video.py at revision df27e04c, start_or_restart_ffmpeg, capture_frames stdout read, detect / record roles; ffmpeg_presets.py input and record preset maps
但这条边界不在命令结束时消失。它会出现在每一帧画面上。
Frigate 先算出一帧 rawvideo 应该有多少字节,再从 FFmpeg 的 stdout 读取这个固定长度。字节被写进一块命名共享内存,队列里传递的只是内存名和时间戳。检测进程根据名字取回画面,再做运动检测和物体跟踪。
队列满了,Frigate 会跳过这一帧。读不到完整长度,捕获线程会记录错误并停下。
FFmpeg 交付的是一段连续字节。Frigate 把它变成有名字、有时间、会堵塞、也会被丢弃的产品状态。
[220]
引用 [220]
固定源码支撑 pipe、共享内存、队列和丢帧路径;‘产品状态’是对应用接管时间戳与背压语义的归纳。
frigate/video.py at revision df27e04c, capture_frames lines 103-175 and process_frames shared-memory lookup path: fixed frame_size stdout read, named frame buffer, timestamp queue and queue-full skip
当命令已经变成一份完整计划,“专业系统最后都会换成 API”就不再是必然结论。
二零二六年五月,Lex Fridman 发布了一场关于 FFmpeg 的长篇访谈。两位嘉宾 Jean-Baptiste Kempf 和 Kieran Kunhya 都长期参与 FFmpeg 及其周边工作。访谈在这里谈到两种确实并存的专业路径:有些公司使用 API,有些公司生成很长的 command line。
Jean-Baptiste Kempf(原声): It's a language. It's an actual language.
Lex Fridman(原声): It's an actual, yeah, you could think of it as a programming language.
Jean-Baptiste Kempf(原声): Yeah, of course, I'm sure. Because, so most of the people, they're going to take FFmpeg, file in, file out, and specify the format, right? But you can, we've seen thousands of characters, and we've seen also, like, people doing programming, generation of command lines to make FFmpeg.
[207]
引用 [207]
短片段内容、句界、v3 master 转场、完整上下文与节目 / 说话人已由主播复听确认;当前中文版按技术评论使用 22.800 秒最短必要连续边界,发布方 transcript 和 ASR 只用于定位。
Lex #496 YouTube-aligned media 00:31:53.40-00:32:16.20; publisher transcript speaker turns at 00:31:53-00:31:57; independent ASR context 23.44-46.12s
一条命令如果能描述整项任务,调用者就不必和 FFmpeg 共享内部对象。
它只需要和一个进程打交道。
可操作系统给进程的合同很窄:一组参数,标准输入、标准输出、标准错误,几个 signal,最后再给一个退出状态。
媒体生产线被封装起来以后,产品仍然要回答另一组问题。
命令是谁生成的?
二进制从哪里来?
进度怎样计算?
进程活着,是否等于任务健康?
进程退出,是否等于产品已经得到正确结果?
这些问题,全都留在命令之外。
命令外面重新长出一套系统
13:02先从那一个可执行文件开始。
“运行 FFmpeg”听起来像是在操作系统里找一个叫 ffmpeg 的程序。产品真正面对的,却是平台、架构、版本和编译选项。
LosslessCut 要区分 Windows、macOS 和 Linux,也要区分 Intel、Apple Silicon 与其他架构。在 Linux 发布包里,它还会设置 LD_LIBRARY_PATH,让随应用带来的动态库能被正确找到。
PhotoPrism 默认依赖系统里的 FFmpeg,但允许调用者替换路径。它为软件编码和多种硬件编码器生成不同命令,同时明确提醒:选择了某个 hardware builder,不代表当前 FFmpeg build 一定包含对应 encoder,也不代表机器上存在那块设备。
调用一个程序,避开了把 C ABI 接进每种宿主语言的工作。
交换条件是,二进制本身成为产品供应链的一部分。
[208]
引用 [208]
支撑平台路径、动态库环境、system binary 与硬件 build 条件;许可和所有分发策略不在此段展开。
LosslessCut src/main/ffmpeg.ts getFfPath and Linux LD_LIBRARY_PATH handling; PhotoPrism internal/ffmpeg README Constraints, Encoders and Configuration notes
然后是进度。
FFmpeg 的 stderr 很热闹。版本、输入信息、stream mapping、warning、错误和处理进度,往往从同一条通道出来。
LosslessCut 想在图形界面里显示百分比,就要逐行读取 stderr,把人类可读的进度文本重新解析成数字。用户取消导出时,它维护一个运行中进程集合,为每个任务保存 AbortController。ffprobe 如果长时间没有回答,它还要自己设置计时器,三十秒后 kill 掉探测进程。
这里没有一个统一的“媒体任务对象”把状态返回给 Electron。
只有一条不断吐出文字的管道,和宿主自己补上的状态机。
[209]
引用 [209]
固定源码支撑进度解析、取消集合和 ffprobe 默认三十秒超时;stderr 具体格式仍可能随 FFmpeg build 和调用参数变化。
LosslessCut src/main/ffmpeg.ts handleProgress stderr reader, runningFfmpegs / abortFfmpegs, runFfmpegProcess and runFfprobe timeout
连“把命令记录下来”也没有想象中简单。
PhotoPrism 的一条 issue 里,用户把日志显示的 FFmpeg 命令放进 shell,怀疑 filter 表达式少了引号。项目维护者重新检查代码后发现,原来的错误与引号无关,而是 VAAPI 设备或 FFmpeg build 不支持这条硬件路径。
关键在于,PhotoPrism 用 Go 的 exec.Command 传递 argv,中间没有 shell。给 filter 手动加上引号,反而可能把引号本身也传给 FFmpeg。而日志里那条看起来完整的命令,只是 Go 为调试生成的人类可读描述。Cmd.String 的文档特意警告:它不适合再作为 shell 的输入。
同一次执行有两种表述:一种是实际传入的 argv,另一种是日志为人显示的命令。要让一条生产命令真正可重放,调用者还要保存原始参数,或者重新按目标 shell 的规则转义。
[219]
引用 [219]
项目维护者评论直接支撑实际 argv、日志字符串和 shell 重放的区别;不把这个历史 issue 表述为当前 PhotoPrism bug。
PhotoPrism issue #4604, maintainer comments 2025-01-08 at 17:20:31Z and 19:01:38Z: no shell in execution path, VAAPI device/build diagnosis, log replay caveat and Go os/exec Cmd.String warning
再看 yt-dlp。
它是一款 Python 下载工具。下载完成以后,合并音视频、转封装、提取音频、写入元数据,都可能继续交给 FFmpeg。
所以它不只寻找 ffmpeg 和 ffprobe 在哪里。它还探测版本,读取编译特性,根据 build 判断某些 filter 和 bitstream filter 能不能用。
连文件名都不能原样交出去。
一个本地文件名如果含有冒号,FFmpeg 可能把冒号前面的部分当成 protocol。文件名如果以连字符开头,又可能看起来像 option。yt-dlp 的处理方式,是在本地路径前明确加上 file:,告诉 FFmpeg:这不是协议,也不是下一项参数,这就是文件。
即使调用者已经使用 argv array,没有经过 shell,字符串仍然会进入 FFmpeg 自己的语法世界。
[210]
引用 [210]
源码直接说明冒号会被解释为 protocol、前导连字符会被解释为 option,并展示 file: 规范化;不能外推所有 wrapper 使用相同规则。
yt_dlp/postprocessor/ffmpeg.py at revision 2c28ee5d, _determine_executables, _get_ffmpeg_version feature detection, run_ffmpeg_multiple_files and _ffmpeg_filename_argument
最后,回到仍在运行的进程。
Frigate 不能只检查 FFmpeg 有没有退出。
一个进程可能活着,却二十秒没有输出新帧。它也可能继续生成录制文件,却一百二十秒没有新的有效 segment。相反,一个进程已经崩溃,捕获线程也会先发现 stdout 再也读不到完整 frame。
因此 watchdog 看的是产品结果,不只是进程状态。
检测路径看最近一帧、实际帧率和捕获线程。录制路径看最近一个缓存分片、最近一个有效分片,以及无效分片之后有没有恢复。重启还要遵守间隔,避免一个坏输入让系统不停拉起新进程。
FFmpeg 知道自己是否还在处理 packet。
Frigate 才知道用户的摄像头是否在线。
[211]
引用 [211]
源码支撑多维健康判定和重启节流;各条件属于不同 role / branch,口播没有把它们写成单次故障必然依次发生。
frigate/video.py at revision df27e04c, CameraWatchdog.run: capture thread, fps overflow, 20-second frame test, 90-second recording grace period, 120-second cache / valid segment tests and restart pacing
到这里,复杂性已经分成两半。
FFmpeg 里面是数据面:container、packet、frame、time base、filter、encoder、muxer。
FFmpeg 外面是产品控制面:command builder、二进制分发、版本探测、进度解析、超时、取消、watchdog、重试和结果验收。
调用动态库,应用得到逐帧控制,也接管数据面的对象生命周期。
调用 CLI,FFmpeg 接管大部分数据面,应用则围绕进程重新建立控制面。
多媒体处理的复杂性没有减少。
它只是换了地址。
而边界一旦画在字符串和进程上,新的错误也会沿着这条线出现。
进程结束,任务没有完成
18:22二零一八年,PeerTube 的 issue 里出现了一条看起来合理的 FFmpeg 参数。
PeerTube 是一款联邦视频平台。它通过 Node.js 里的 fluent-ffmpeg 组织转码命令。当时,开发者想设置关键帧间隔,传入一个表达式:当时间达到下一次目标时,强制插入关键帧。
表达式中间有一个逗号。
FFmpeg 最后报告:关键帧时间无效。错误信息显示,它只看见了逗号前面的半句话。
issue 参与者很快把怀疑指向 fluent-ffmpeg。应用以为自己传入了一个完整表达式,wrapper 却可能把逗号当成参数分隔符,再把改变后的结果交给 FFmpeg。
命令在日志里仍然像那条命令。
语义已经不再是原来的语义。
[212]
引用 [212]
历史 issue 支撑当时观察到的参数截断与维护者判断;不声称已经取得独立根因复现,也不暗示当前 PeerTube 仍存在此问题。
PeerTube issue #1147 body and 2018-09-30 comments by Nutomic and rigelk: force_key_frames expression, invalid duration ending at comma and fluent-ffmpeg suspicion
三年后,同一个项目出现了另一种错位。
一位用户在转码尚未结束时停止 PeerTube,然后重新启动。系统回来以后,job 被标记为完成,视频却不能播放。
讨论一开始卡在 stderr 上。PeerTube 维护者提醒:FFmpeg 会把普通日志写进 stderr,所以 stderr 有内容,不代表命令失败。真正应该看的,是 FFmpeg 有没有返回错误状态。
可继续复现以后,问题变得更复杂。
重启让第一个 HLS job 停滞。队列管理器继续处理后续清晰度,后面的 job 又依赖第一份最高分辨率文件,于是产出被破坏。队列最后还会回头处理那项停滞的工作。
维护者总结,这个案例之所以特殊,是因为 FFmpeg 并不认为自己的操作失败了。
进程、wrapper、队列和 HLS 输出,各自都拥有一部分状态。没有任何一个退出码,能替整个产品回答“这支视频可不可以播放”。
[213]
引用 [213]
支撑特定 PeerTube 3.0.1 复现与维护者分析;问题同时涉及 Bull queue,不归因成 FFmpeg 单方 bug,也不表示当前版本仍受影响。
PeerTube issue #3596 body and comments dated 2021-01-14 to 2021-01-21, especially Chocobozzz reproduction comment on stalled first HLS job, dependent jobs and FFmpeg not considering the operation failed
这就是退出状态的边界。
零只能表示:从这个进程自己的视角,它完成了被要求的执行路径。
零不能保证输出文件存在合理时长,不能保证 playlist 引用的 segment 全部存在,不能保证音视频轨道都在,也不能保证一个依赖链中的上游 job 已经准备好。
在 PeerTube 那次复现里,被后续 job 当作输入的一份 fragmented MP4 只剩音轨,没有视频轨。后面的 FFmpeg 仍然可以把这条音轨复制进 HLS 输出,由于它完成了这一次局部操作,并不认为自己失败。用户真正看到的结果是:播放器里只有声音,切换清晰度时,选项也只剩下“仅音频”。
所以,成熟的媒体服务不能把“FFmpeg 没报错”直接翻译成“视频成功”。
它还要重新探测输出:该有的视频和音频 stream 是否存在,duration 是否接近输入,playlist 引用的 segment 是否齐全且时间连续。再往上一层,产品还可能真正尝试播放,确认用户得到的不只是几个格式正确的文件。
CLI 把任务包装成一个进程。
业务合同通常比进程更大。
[221]
引用 [221]
历史复现直接支撑缺少视频轨、仅音频播放和 FFmpeg 未判失败;stream、duration、segment 与播放检查是从这一产品合同归纳的验收层次。
PeerTube issue #3596 comments 2021-01-14 to 2021-01-21: generated fragmented MP4 without video stream, audio-only HLS output, completed jobs, player result and maintainer statement that FFmpeg did not consider the operation failed
参数边界还有更危险的一面。
二零二六年六月,Jellyfin 发布一份高危安全公告。Jellyfin 是一款自托管媒体服务器;在字幕转换路径里,一段字幕文件路径会进入 FFmpeg 参数。
问题不在于把整条命令交给 shell。公告描述的是 FFmpeg argument injection:路径没有按照 FFmpeg 参数语义完成规范化,攻击者可以让其中一部分被 FFmpeg 当成新的参数解释。
公告把影响限定为任意文件写入和信息泄露,修复版本是 Jellyfin 十点十一点十。
一个文件路径,在应用眼里是数据。
进入 FFmpeg 以后,它可能变成 option、protocol、filter expression,或者另一个输出位置。
使用参数数组,只消除 shell 那一层解释。调用者仍然必须理解 FFmpeg 自己的语言。
[214]
引用 [214]
官方公告支撑漏洞路径、修复版本及文件写入 / 信息泄露影响;不扩写为 shell command injection 或 shell RCE。
Jellyfin GHSA-wwwm-px48-fpvq, Impact and Patches: SubtitleEncoder path interpolation, FFmpeg argument injection and fix in 10.11.10
于是,CLI 的优势和代价来自同一个地方。
它把巨大的媒体系统压进一个很窄的接口。
窄,意味着不同语言都能接。意味着命令可以记录、比较、复制和重放。意味着一个 decoder 崩溃时,首先倒下的是子进程,而不是宿主的整个地址空间。
但窄不等于结构化,不等于类型安全,也不等于沙箱。
字符串会被多层解释。stderr 不是错误对象。退出状态不是产物证明。
子进程的独立地址空间,能改变一次崩溃传播的方式:宿主可以终止它,再换上新进程。可如果 FFmpeg 可以读入媒体目录、写入输出目录、连接网络,decoder 中被利用的代码也在这些权限里运行。使用受限账号,或者限制文件系统、网络和系统调用,需要另外的沙箱机制。Popen 本身不会自动创造这些限制。
进程边界是故障边界的一部分,不是对不可信媒体的完整安全答案。
[222]
引用 [222]
来源分别支撑子进程可替换性与 decoder 缺陷的下游可达性;权限与沙箱的区分是条件化工程判断,不声称所有案例都缺少额外隔离。
- Frigate FFmpeg process and camera watchdog
- PixelSmash: Critical FFmpeg vulnerability turns media files into weapons
Frigate subprocess terminate / kill / restart path; JFrog PixelSmash report affected-project integration table and server-side untrusted-upload attack paths through FFmpeg subprocess and linked-library consumers
什么时候不要启动 FFmpeg
23:45二零一九年,LinkedIn 工程团队公开介绍了 LiTr,一款为 Android 设计的轻量音视频转码器。
他们面对的任务很窄:用户上传视频以前,降低分辨率或者码率,减少不必要的数据。大多数目标视频来自 Android 设备,通常已经是 H.264。团队更看重实时帧率、更低的电池消耗和移动端体验。
FFmpeg 的 Android port 能支持更多 codec、container 和编辑操作。但采用者文章明确写道,软件编码可能大量消耗 CPU 和电池。经过实验,他们认为硬件 encoder 更符合自己的约束。
LiTr 最终使用 Android 的 MediaCodec 访问硬件编解码器,用 MediaExtractor 读取输入,用 MediaMuxer 写出结果;需要缩放画面时,再让 OpenGL 连接 decoder 和 encoder 的 surface。
FFmpeg 在这里没有输给另一个更完整的媒体系统。
它输给了一项已经足够确定的任务。
[215]
引用 [215]
采用者文章明确支撑电池、CPU、实时帧率、H.264 假设与平台 API 选择;不能外推所有手机任务或把 FFmpeg 等同于只能软件编码。
LinkedIn Engineering LiTr article, paragraphs on software versus hardware encoders, experimentation and constraints, MediaCodec, OpenGL, MediaExtractor and MediaMuxer
还有一些产品,从一开始就需要把 frame 留在自己的进程里。
播放器要让解码画面进入渲染循环。录制与直播软件要在同一张 frame 上叠加场景、字幕和实时效果。逐帧交互的工具,要在媒体数据经过时立刻改变应用状态。
二零二六年的 PixelSmash 安全研究在追踪同一个 FFmpeg decoder 缺陷时,也记录了两类不同集成。Jellyfin、PhotoPrism 等产品会启动 FFmpeg 或 ffprobe;mpv、Kodi、ffmpegthumbnailer 和 OBS Studio 则把 libavcodec 链接在进程内。
这组安全研究样本不是行业调查。它不能告诉我们哪种方式更多。
它只证明:专业软件从来没有收敛到唯一接口。需要直接拿到 packet 和 frame 的产品,会选择库;适合被包装成完整 job 的产品,常常选择进程。
[216]
引用 [216]
研究团队实测支撑这九项产品路径;样本由漏洞可达性形成,不是随机抽样,不用于推断全行业占比或产品最初动机。
JFrog PixelSmash report affected-project integration table and per-project paths for Jellyfin / PhotoPrism subprocesses and mpv / Kodi / ffmpegthumbnailer / OBS Studio libavcodec integration
任务也可以不以“开始一个 job,等待一个输出”存在。
GStreamer 选择了另一种外形。它把 source、filter、codec 和 sink 做成可以混合、连接的 element,再由应用把它们组织成 pipeline。官方应用手册强调 plugin、数据流、媒体类型协商和同步;这张图本身就是宿主应用运行时的一部分。
如果媒体图需要长期存在,持续改变连接,和应用状态一起演化,那么“启动一个外部进程,等它结束”就可能不是最自然的控制方式。
这里没有无条件优胜者。
FFmpeg CLI 擅长把一张复杂图封装成有输入、有输出、可以结束的 job。
GStreamer 或进程内库,更适合让应用持续握住图里的节点、frame 和状态。
[217]
引用 [217]
官方手册支撑 framework / plugin / pipeline 模型;与 FFmpeg CLI 的任务边界比较是条件化工程判断,本轮未做同任务性能比较。
GStreamer Application Development Manual, What is GStreamer: streaming applications, pluggable components, arbitrary pipelines, data flow, media type negotiation and synchronization
所以,真正的分界不是“业余还是专业”。
也不是“多媒体太复杂,所以只能用 FFmpeg”。
要看复杂性长成了什么形状。
输入可以在任务开始前命名,输出可以在任务结束后验证,中间过程可以交给独立进程,失败可以按整个 job 重试。这样的任务,很容易落到 FFmpeg CLI。
如果应用必须逐帧回调,必须共享 GPU 内存,必须让媒体图长期变化,或者任务窄到平台硬件已经给出更省电的路径,库、binding、GStreamer 或平台 API 会更自然。
FFmpeg 被反复选择,不是因为所有媒体问题都一样。
是因为很多完全不同的产品,最终都会产生一种相同的工作单位:给定输入,执行一张媒体图,交付输出。
操作系统已经知道怎样启动、等待、限制、终止和替换一个进程。
FFmpeg 把媒体任务装进了这个单位。
命令之外
27:37现在,再回到开头的第二十秒。
Frigate 发现最近一帧停住,把摄像头标成离线,结束旧的 FFmpeg,再拉起新的进程。
一开始,这个 watchdog 很像在替一个不可靠的程序收拾残局。
现在,它更像这项架构的另一半。
FFmpeg 负责打开未知的媒体输入,处理 container、packet、frame、filter、encoder 和 muxer。它知道一包数据能不能解码,知道某个输出能不能写下去。
Frigate 负责定义产品里的成功。二十秒没有画面,摄像头就是离线;一百二十秒没有有效分片,录制就是失效;进程退出以后,输出仍然要继续验收。
它们不是在重复管理同一件事。
它们管理的是同一项任务的两种真相。
[218]
引用 [218]
源码支撑 Frigate 对 frame、segment、进程和产品状态的判断;‘两种真相’是节目收束性表达,不是项目原话。
frigate/video.py at revision df27e04c, CameraWatchdog frame / recording health branches, stop / restart paths and status updates
这也解释了,为什么设备、语言和规模改变以后,那条命令仍然会出现。
开发者保留的不是终端里手敲参数的动作。
他们保留的是一条跨语言的进程协议。
Electron 可以生成它,Python 可以生成它,Go 可以生成它,服务端队列也可以生成它。FFmpeg 在进程里面解释完整媒体图,操作系统在进程外面提供调度和故障边界。
应用因此不用把 libav 的 packet、frame、time base 和对象生命周期接进自己的运行时。
代价是,它要在外面重新建立 command builder、二进制供应链、进度解析、watchdog、安全约束和产物验证。
CLI 没有让媒体处理变简单。
它让采用者选择,自己愿意负责哪一半复杂性。
一条 FFmpeg 命令结束时,屏幕上通常只剩下一行退出状态。
可在那一行之外,还有下载器检查文件名,有桌面软件解析进度,有照片服务选择硬件,有视频平台核对播放列表,也有一个 watchdog,继续等待下一帧。
FFmpeg 的流行,不只体现在多少产品带着它。
还体现在这些产品为了使用它,在命令之外长出了多少代码。
片尾
29:52你刚刚收听的是《原代码》第八期,《命令之外:为什么大家都启动 FFmpeg?》
这一期,我们从 Frigate 二十秒没有新画面的 watchdog 出发,跟着一项媒体任务穿过命令生成、FFmpeg 数据流、子进程监督、错误语义和产物验证,最后回到同一个问题:命令替应用接走了什么,又把什么留在了外面。
你可以访问《原代码》的官方网站。网站地址是,原代码三个字的全拼,点 X Y Z。在那里,你可以查看本期逐字稿、证据引用关系和证据快照,也可以收听往期节目。
感谢收听。