ffmpreg:用Rust重写FFmpeg的技术拆解与落地路径

发布时间:2026/10/19 2:49:23
ffmpreg:用Rust重写FFmpeg的技术拆解与落地路径
好家伙真有人干这事儿了。今天刷到这条日报标题的时候我第一反应是点进去看看这个ffmpreg到底搞出了什么名堂。把FFmpeg和preg拼在一起preg这个缩写自带“孕育”的味道仿佛FFmpeg肚子里怀了个孩子——说直白点这就是Rust社区那个“用Rust重写一切”的执念终于烧到音视频领域最硬核的那块骨头上了。作为一个常年跟音视频管道打交道、又在Rust这边写了不少解析代码的人我对这种项目的态度一直很复杂一边觉得疯狂一边觉得真该有人干。FFmpeg是C语言世界里最成功的多媒体项目之一几乎所有播放器、转码工具、直播服务、视频剪辑软件底层都能看到它的影子。但它的体量已经把很多想为它做贡献的人挡在了门外。所以这篇我不想做标题党而是想认真拆一拆如果真要用Rust把FFmpeg重写一遍这个工程到底意味着什么第一版应该从哪儿下手会遇到哪些藏在细节里的坑。适合谁看呢对Rust多媒体开发感兴趣的人、做过C/C音视频项目的人还有那些正在考虑要不要加入一个大型重写项目的开发者。1. ffmpreg这个名字为什么在Rust圈是“一眼就能看懂”的信号1.1 FFmpeg的g不发音但preg很会挑事FFmpeg这个名字本身是“fast forward MPEG”的缩写平时大家读的时候把末尾的g当作/j/音也就是“FFmpeg”听起来有一种独特的节奏感。然后你把最后的音节换成preg就成了ffmpreg。preg在英文语境里是pregnant的缩写有孕育的意思。ffmpreg字面上就是“FFmpeg怀孕了”——怀的是一个Rust版本的孩子。这种命名方式在Rust社区非常常见。它不是严谨的学术命名而是一种带着自嘲和挑衅的社区文化你说我用Rust重写一个C项目不现实我偏偏要给它起一个这么不正经的名字来干。从这个名字就能看出来这个项目大概率不是某个大公司的工作任务而是一群真正被Rust“洗脑”了的开发者打算在最难啃的多媒体领域做一次实验。1.2 Rust社区为什么执着于“重写一切”C写的大型项目有共同痛点手动管理内存、头文件接口暴露太多细节、构建系统复杂、缺少统一的包管理。Rust用所有权和生命周期在编译期解决了大部分内存安全问题又用cargo统一了构建和依赖。对FFmpeg这种用C写了几十年、接口包袱巨大的项目来说Rust的吸引力不言而喻。但音视频领域有它特别的地方性能敏感、SIMD密集、状态机多、并发复杂这恰好是Rust想证明自己的场景。FFmpeg代码里到处都是技巧性的内存复用和指针操作每一次改动都像在雷区走路。如果有人能拿出一个Rust实现哪怕只覆盖一小部分功能对很多人来说都是“原来可以这样写”的强有力示范。再说直白点这个标题能在Rust日报里出现本身就说明它触动了社区的情绪。Rust圈一直有一种执念那些重要的C基础设施都应该有一个更好的Rust版本。不管最终能不能全面替换每出现一个这样的尝试都是在给“内存安全的基础设施”多添一块砖。对于ffmpreg这种名字都起得这么挑衅的项目我反而更想看看它第一步到底怎么迈。2. 先别谈“完全重写”看看FFmpeg到底有多大2.1 FFmpeg的真实规模远不止“转个码”FFmpeg不是一个单一程序而是一整套组件。平时用的ffmpeg命令行只是壳底下是libavformat容器解析、libavcodec编解码、libavfilter滤镜、libavdevice设备、libavutil工具、libswscale像素转换、libswresample重采样。我经常听人说“FFmpeg就是一堆解码器”其实只说对了一半。除了编解码器它还有上百种封装格式、几十种网络协议、几百个滤镜。可以这样理解你按下转换按钮之后计算机要从文件里找出视频流和音频流的位置解封装把压缩的数据解码成原始画面解码按需求改变像素格式和分辨率滤镜/缩放再用另一种压缩方式编码编码最后重新打包成目标文件封装。任何一个环节都有天文数字般的细节。2.2 把“完全重写”拆成四个梯队与其对着“完全重写”四个字头皮发麻不如把它拆掉第一梯队解封装/封装格式。MP4、MKV、FLV、TS这些容器本质是二进制结构解析和写入是Rust最擅长的阵地。用纯Rust写一个能读MP4、写MKV的工具只要几千行。第二梯队滤镜。裁剪、缩放、字幕、画中画这些操作很多是像素级处理纯Rust可以做。灰度转换、颜色空间转换要性能还得上SIMD但难度低于编解码器。第三梯队编解码器。这才是硬骨头。光是一个H.264解码器涉及熵解码、预测、变换、环路滤波实现一个能用的版本就要几万行还要处理不同档次和级别。第四梯队网络协议与设备。RTMP、RTSP、SRT、HLS这些协议栈可以纯Rust实现但完整覆盖很耗时且极易踩到协议兼容性的大坑。这样一个“完全重写”的完整工作量不是一个人靠业余时间能做到的。但项目完全可以先做第一和第二梯队编解码器走外部对接再逐步替换。这也是我判断这类项目的最合理路径。我用一张表把规模和难度简单列一下功能组件实际职责重写难度评估纯Rust可行性容器层解析/写入MP4、MKV、FLV等格式中等涉及二进制结构和耗时索引最适合入手的领域协议层处理RTMP、RTSP、HLS等网络流高协议版本和厂商扩展多可以纯Rust但很耗时滤镜层缩放、裁剪、颜色转换等像素操作中高需要SIMD优化部分可做性能是关键编解码层H.264/H.265/AAC等压缩与解压缩极高需要大量汇编级优化当前最不现实的全自研方向2.3 Rust社区手里的牌已经有了什么如果现在有人要做ffmpreg他不是从零开始。Rust生态里已经有一些积累给FFmpeg C API做安全封装的绑定库用起来体验接近原生有团队做过纯Rust的AV1编码器证明了编解码器在Rust里是可以活的纯Rust的FLAC、MP3、WAV等音频解码器也成熟了。视频容器方面MP4/MKV的读写库在Rust生态里也有可用的底盘。我提这些不是在泼冷水而是在说ffmpreg的正确姿势很可能是站在这些已有工作的肩膀上把最缺的那块——统一的、可持续的管道框架——补起来而不是把别人写过的东西再徒手撸一遍。真正难的不是写出一个能解析MP4的函数而是让整个管道在不同输入输出之间平滑切换并且愿意兼容真实世界里那些“脏”样本。3. 如果我来搭这个项目的第一版架构会这样拆模块3.1 先写一个能跑的管道MP4到MKV的remux我认为第一版应该选一个最有边界感、最容易验收的目标一个MP4转MKV的remux工具。不解码、不编码只是把容器换掉。这个目标虽小但能把“输入、解封装、包处理、封装、输出”整条链路跑通把架构的骨架立起来。remux虽然不改编码但仍然要处理时间戳、关键帧标记、音视频轨道映射以及MP4和MKV在sample组织方式上的差异。跑通之后架构的每个接口都经历了真实格式的锤炼后面再对接解码器就有底气了。为什么要选MP4和MKV这对组合因为它们的容器逻辑很不一样。MP4靠box嵌套和sample table组织数据MKV靠EBML元素和cue定位。能把一个MP4文件完整地转成MKV并且音画同步、字幕不移位就已经把很多边缘情况摸了一遍。这个目标对第一版来说依然是够分量的。3.2 用Rust的类型系统表达音视频数据流我建议第一版就把核心数据结构立住。包Packet是链路里最关键的单元pub struct Packet { pub stream_index: u32, pub pts: Rational, pub dts: Rational, pub duration: Rational, pub data: Bytes, pub is_keyframe: bool, }时间戳用有理数分子分母而不是浮点数因为在转封装很多场景里22ms浮点误差会导致音画不同步。流索引区分视频、音频、字幕轨。整个管线的核心可以是一个迭代器风格的接口pub trait Demuxer { fn next_packet(mut self) - ResultOptionPacket, Error; } pub trait Muxer { fn write_header(mut self, tracks: [TrackInfo]) - Result(), Error; fn write_packet(mut self, packet: Packet) - Result(), Error; fn write_trailer(mut self) - Result(), Error; }为什么用迭代器风格而不是回调风格回调风格会把控制流反扣过来Packet的所有权和借用会变得难以追踪迭代器风格下每个环节能清楚地看到“包从哪来、归谁所有、什么时候释放”符合Rust的使用习惯。对于音视频这种高度依赖顺序处理的数据流迭代器模型天然合适。3.3 一张表看清remux demo的工作量我按照常见的实现估算了一下一张表可以看明白模块预估代码量核心难点MP4解封装3000-6000行box嵌套解析、sample table映射、compressed offsetMKV封装3000-5000行EBML元素、cue定位、Timestamp处理命令行界面500行以内参数解析、日志输出测试样本永远干不完真实样本的兼容性验证行数只是工程师写代码的时间不包含调协议、试错的时间。我在模拟项目X里做过类似demo实际花费的时间是预估的两倍主要耗在“看着正常的MP4样本其实藏着各种非常规box”上。一个合格的demuxer就是要能吃掉所有奇怪写法而不是只会读自己生成的干净文件。4. 真正的硬骨头编解码器里的SIMD、汇编与unsafe4.1 解码器的性能热点不是“写个循环”就能搞定的很多人以为解码器只是读比特流然后做像素操作其实真正的解码器里每一个宏块、每一个变换块的处理都要效率。典型的性能热点包括熵解码H.264的CABAC、HEVC的上下文自适应算术编码串行依赖强顺序读比特很难用简单循环向量化反变换与量化矩阵运算密集是SIMD的天然目标运动补偿像素拷贝和插值本质是内存带宽操作SIMD收益非常大环路滤波去块滤波、SAO逻辑复杂、分支出多自动向量化基本帮不上忙需要手工改造数据布局FFmpeg里很多核心热点用的是手写汇编。C版本之外还维护着x86、ARM NEON等不同平台的高性能路径。这在Rust里重新实现不只是翻译语法而是要把所有平台优化都重做一遍甚至要重新考虑数据布局。4.2 Rust的SIMD工具链现状Rust的自动向量化这几年进步明显在简单的逐像素循环上编译器能自动生成SSE或AVX指令性能往往不输C的常规写法。但从FFmpeg这种级别看自动向量化还不够。核心的滤波、运动补偿逻辑需要人手动干预比如通过core::arch里的x86_64 intrinsics或者在ARM上使用NEON intrinsics。Rust的portable SIMD还处在演进中面向未来的写法仍未完全稳定。实际操作中一个办法是给这类核心函数单独开Rust文件用#[target_feature(enable avx2)]标注并且在运行时用is_x86_feature_detected!判断CPU能力来做分派。我就喜欢把集中性能计算放在一个内部模块里暴露给外部的依然是安全接口。4.3 把unsafe锁进“小隔间”Rust重写FFmpeg绕不开unsafe。音视频处理里有几种场景几乎必然用到unsafe对齐内存分配、在解码数据中构造切片、直接操作硬件加速API返回的指针、SIMD读取。我的原则很简单让unsafe集中于边界层。帧缓冲区分配器不安全的代码全部收敛在alloc模块里外部拿到的都是安全抽象的[u8]或者ArcFrame。SIMD函数内循环内部是不安全的但函数签名是安全的调用者不需要感知。架构评审时全项目搜索“unsafe”应该只出现在少量文件里而且每个文件都能讲清楚为什么必须不安全。这个约束应该在项目的CONTRIBUTING文档里明确写死。5. 绕开“完全重写”的激进路线Rust项目真正能落地的路径5.1 纯重写、C绑定、混合架构三个路线怎么选我列出几种路线路线A纯Rust重写全部组件。最大优点是安全收益彻底、无FFI开销最大缺点是工程量巨大十年都不一定能追上FFmpeg的完整覆盖路线B只在Rust里给FFmpeg C API做安全壳。开发效率高能立刻用上全部能力但这不叫“重写”内存安全问题还在C那边路线C混合架构。容器、协议、滤镜用Rust重写编解码器先用FFI对接C实现后续再逐个替换为Rust原生实现我个人最看好路线C。它承认了编解码器是最难的部分却没放弃长期目标。每一步替换都有明确边界格式层安全、协议层牢固之后再向编解码层进军。5.2 我期待ffmpreg先做成什么样如果我是项目维护者第一年的路线图会定成建立管道框架和命令行工具骨架1-2个月支持MP4、MKV、FLV的remux3-6个月接入H.264/H.265解码器先通过FFI对接成熟库验证管道正确性6-12个月开始实现一个简单解码器比如从音频MP3或者视频的基线profile开始积累Rust原生编解码经验12个月之后这个节奏的好处是每个阶段都有可演示的成果社区能看到项目在持续产出。最怕的是项目一开始就宣称“我们要重写H.264解码器”然后半年过去什么都跑不起来所有人的热情都被消耗掉。5.3 判断一个Rust重写项目是否“成了”的三个信号我自己判断这类项目不看star数只看三个信号。第一兼容性。真正的视频样本千奇百怪能打开几十种真实文件而不是自造的test case才算过了第一关。第二性能。用criterion或者在真实转码任务上做基准和FFmpeg同类功能比差距如果在1.2倍以内已经是非常好的成绩。第三内存安全证据。跑cargo fuzz开了sanitizer的测试矩阵要干净。这不是宣传口号是硬指标。这三个信号比README里的雄心重要得多。对ffmpreg这种项目我特别希望看到的是它愿意公开自己的失败样本和兼容性测试矩阵。很多开源项目挂着“支持MP4”的招牌一遇到带B-frames、带多音轨、带章节元数据的文件就现出原形。只有测试矩阵足够透明社区才敢真正信任它。6. 我在模拟项目X里踩过的多媒体坑可能也是这个项目会踩的6.1 AVPacket和AVFrame的所有权比想象的更麻烦我在模拟项目X里给一个视频处理Demo写过Rust包装层第一版就出了崩溃。问题出在AVPacket的引用计数字段FFmpeg的AVPacket里有个buf指针av_packet_ref会增加引用计数av_packet_unref会减少。我在Rust侧直接把它包装成结构体却没处理Drop结果同一个包被释放了两次。后来改成用内部ArcPacketBuf来包住C侧引用让Rust的析构逻辑管住av_packet_unref才解决。ffmpreg如果早期接C解码器这种所有权问题一定会成为第一个热门issue。6.2 解码器的“饥饿”问题与状态机映射另一个坑是解码器的驱动方式。很多视频解码器不是输入一个包就能立刻输出一帧它会缓存若干包甚至要等到下一个关键帧才能输出。如果上层代码假设“一个包对应一帧”结果就是首帧延迟、音频卡死。C的解码器天然用状态机处理这种饥饿。Rust这边如果按迭代器思路包一层就要把内部状态真正建起来一个“读取输入、喂给解码器、输出可用帧”的循环。这个状态机没设计好后面加并行解码、加GPU解码时会全部返工。6.3 时间基与浮点漂移音画不同步的根源我先踩过的一个特别隐蔽的坑用浮点数表示PTS在长时间转码时音画逐渐不同步。MP4的时间戳按90000频点MKV按1ms如果中途统一转成浮点秒再转回来积累的误差会让字幕和音频对不上。解决方案就是用有理数做时间基换算。所有内部时间都用分子和分母保存只在最后输出时才做一次除法。这也是为什么我在前面的架构里坚持Rational类型不是代码洁癖是实战教训。写到这里我对ffmpreg这个项目又多了几分期待。一个能跑通第一版remux的项目已经能帮助很多人等它真的开始啃编解码器我会第一个拉出自己收藏的奇怪视频样本来做测试。如果你也想参与我的建议是别一上来就对着“完全重写”四个字热血沸腾先挑一个几千行能搞定的组件——比如某个容器的demuxer——认真做做扎实。大型项目的根基从来都是这些小部件一个个垒起来的。