真正让这次优化变得具体的数字很简单:我跟踪的一个实际运行中的真实视频,在采用新的 H.264 策略后,从大约 280 MB 降到了 50 MB。也就是大约缩小 5.6 倍,节省约 230 MB,相当于原始文件大小的约 82%。
这个结果并不是靠迁移到 AV1、HEVC 或 VP9 得到的。实际运行输出 仍然是 MP4 中的 H.264。变化的是 编解码器 周围的整个策略:去掉不必要的 像素,减少没有意义的 时间采样,采用明显更激进的 质量目标,在一次性的 编码 阶段投入更多 CPU,同时把 解码约束 有意限制在较保守的范围内。
这是一个非常具体的 使用场景:短篇插画和动画内容,约 80% 的 流量 来自 移动端,带宽 是持续发生的成本,而 离线编码 的时间成本相比反复发送过大的文件要便宜得多。
旧策略和新策略大致如下:
旧配置
H.264 Main @ Level 4.0
CRF 19
预设 slow
最高 1920x1080 / 1080x1920
30 fps
refs = 3
B 帧 = 3
GOP ≈ 2 秒
VBV ≈ 10M / 20M
新配置
H.264 Main @ Level 3.1
CRF 28
预设 veryslow
720p 级为上限,不放大
有用的 CFR,通常 <= 30 fps
refs = 4
B 帧 = 5
GOP ≈ 5 秒
VBV ≈ 4M / 8M
yuv420p / avc1 / faststart
实测结果:约 280 MB 降到约 50 MB
我有几组实际运行环境中的真实实测,但它们并不是同一种 实验。把这些类别分开,比单纯挑一个看起来最大的百分比更重要。
| 测量项 | 修改前 | 修改后 | 缩减 |
|---|---|---|---|
| 同一个具体视频 | ~280 MB | ~50 MB | 约缩小 5.6× / 减少约 82% |
| 同一文件的早期阶段 | ~350 MB | ~238 MB | 约缩小 1.47× / 减少 32% |
第一行是标题最直接、最干净的证据。它就是同一条具体视频在新策略前后的结果:大约 280 MB 变成大约 50 MB。我没有在恢复出来的旧日志里找到这对结果对应的精确 码率/时长 行,所以不会编造。单凭 大小 的变化已经足够:这一个具体文件确实缩小了约 5.6 倍。
~350→238 MB 是同一个文件在更早阶段的一次优化。当时的 输出 大约是 1264×720、30 fps、约 500 秒、无 音频、约 3.8 Mbps。数学上也与观察到的文件大小吻合:3.8 Mbps 持续约 500 秒,大约就是 238 MB。相比 ~350 MB 已经节省了 32%,但对我的 带宽 目标来说依旧太大。
旧 媒体库 的 检查 也说明,过大的 H.264 并不是某一个奇怪的 异常值。一次 检查 中共有 238 个实际运行视频,总计 6.37 GB,其中 117 条 H.264、121 条 AV1。101 个文件至少 20 MB,34 个至少 50 MB。一些较大的 H.264 如下:
| 大小 | 时长 | 平均码率 |
|---|---|---|
| 121.0 MB | 4:25 | 3.83 Mbps |
| 101.2 MB | 5:19 | 2.659 Mbps |
| 92.78 MB | 4:44 | 2.735 Mbps |
| 90.10 MB | 3:55 | 3.206 Mbps |
| 89.74 MB | 4:41 | 2.676 Mbps |
这些文件对应的是不同内容,因此这张表只能提供背景,不能算严格的前后对照测试。但它至少说明旧方案中 3.8 Mbps 并不是偶然的异常值:旧媒体库里确实有多个较大的 H.264 文件处在约 2.6–3.8 Mbps 的范围。
最重要的优化并不是某个 FFmpeg 参数
最大的变化其实是我看待成本的方式。
编码只发生一次。文件每被请求一次,就要传输一次。
对于实时视频,为了省一点码率而多花很多 CPU 可能并不划算。我的文件是在离线环境中编码,然后被反复提供给用户。在这种模式下,如果更快的编码会让今后的每次请求都多传一些数据,那么省下十分钟编码时间在经济上几乎没有意义。
所以 -preset veryslow 对我很合理。只要 x264 能用额外 CPU 找到更高效的表示方式,我愿意一次性付出这笔成本。浏览器不会重复编码器做过的搜索;它只负责解码最终的比特流。
于是规则变得很简单:把计算成本花在只做一次的步骤上,把字节省在会重复发生的步骤上。
为什么我没有追逐最新编解码器,而是继续用 H.264
我并不是说 H.264 是目前压缩效率最高的编解码器。它不是。如果分发系统可以保留多个版本,并针对不同客户端选择最合适的版本,那么更新的编解码器会很有吸引力。
我的限制条件不同:一个 URL、一个文件、一个编解码器,并且要让以移动端为主的用户尽可能少遇到播放问题。
对于这个目标,MP4 中的 H.264 仍然是很稳妥的基线。Apple 目前建议 网页开发者在 Safari 中为静态视频使用 H.264 编码的 MP4 文件。Android 当前文档列出了 MP4 中的 H.264,并从 Android 6.0 起要求支持 Main 配置档 解码;它的 H.264 播放建议还列出了 1280×720、30 fps 的 HD 配置,同时注明并非所有设备都支持 HD。参见 Android 支持的媒体格式。
这不代表现代设备只能处理 Main 配置档 或 级别 3.1。例如,Apple 的 HLS 指南通常更偏向 High 配置档,而不是 Main 或 Baseline 配置档。我选择 Main@3.1,是因为单个静态 MP4 文件需要一个刻意保守的解码复杂度范围,并不是因为 Apple 强制要求这样做。
我不再编码那些没有必要存在的像素
分辨率是影响最大的因素之一。我的上限大致是:横向 1280×720、纵向 720×1280,方形或混合方向素材约 960×960。
更重要的规则是:绝不要为了碰到上限而把素材放大。
如果源素材是 900×600,把它变成 1280×720 并不会恢复任何细节,只会制造更多需要编码器描述的采样点。1920×1080 的源素材可以缩到 720p 级别,而 900×600 的素材完全可以继续保持在大约 900×600。上限是最大值,不是目标值。
这听起来很简单,但去掉不必要的像素,往往比许多冷门的编码器微调更有价值。
这个上限并不是为了选一个好看的整数。1920×1080 一帧包含 2,073,600 个像素,而 1280×720 包含 921,600 个。也就是说,从 1080p 降到 720p,在编码器开始做压缩决策之前就已经去掉了约 55.6% 的空间采样。
我也考虑过把 540p 作为统一默认值。但 960×540 只有 518,400 个像素,比 1280×720 少 43.75%,也就是只保留 720p 的 56.25% 采样。对于插画类内容,这些采样承载的是细线、眼睛、头发、手指、脸部以及锐利轮廓。如果还需要更小的文件,我宁愿先测试稍高一点的 CRF,也不会在没有测量的情况下直接再丢掉 43.75% 的空间信息。量化强度可以在下一次编码时调整,而缩小分辨率时被删掉的细节已经无法恢复。
因此,720p 级是我谨慎采用的统一上限,并不是说 540p 不好。对某个具体文件来说,经过测量的 540p 版本完全可能更合适。我只是不在没有数据的情况下,把这种不可逆的空间裁减变成整个媒体库的规则。
我不再为源素材里并不存在的帧付带宽成本
帧率也是一个乘数。如果一段动画每秒实际上只有大约 16 个有意义的视觉状态,把它存成 30 或 60 fps 并不会自动让运动变得更好。很多时候只是增加了重复或合成出来的时间采样,而这些数据仍然需要被编码。
我的策略是保留源素材真正有用的节奏,并且通常保持在 30 fps 或以下。对于这类素材,只要确实符合源素材,12、15、16、18、20、24、25 或 30 fps 都可能是合理的选择。
生成文件时我也更喜欢干净的恒定帧率。VFR 本身没有问题;只是 CFR 能让我的流程更容易处理时间戳、帧数、时长检查、跳转以及后续验证。
比某个具体 FPS 更重要的是这个原则:不要为源素材里根本没有的时间信息付带宽成本。
CRF 28 是针对我的使用场景做的选择,不是魔法数字
我不想强迫所有片段都靠近同一个目标码率。几乎静止的插画和运动复杂的场景,并不需要相同数量的比特才能看起来可接受。
因此我使用 x264 的 CRF 模式。对于这种带宽优先的插画类素材,我最终选在大约 -crf 28。FFmpeg 将 libx264 中的 CRF 记录为恒定质量的码率控制方式;参见 FFmpeg 编解码器文档。
CRF 28 是刻意偏激进的选择。我不会把它直接套用到有胶片颗粒的内容、噪点很多的相机视频、屏幕上的极小文字,或者任何比带宽更重视保真度的场景。
我也没有一个通用的感知指标可以证明 CRF 28 是“透明”的。我能针对自己的素材说得更有限:文件明显小了很多,而正常播放时在我看来仍然自然。这是实际观察,不是说 CRF 28 在视觉上无损。
veryslow 增加的是编码器成本,不会自动增加解码器成本
我的预设是 -preset veryslow。更慢的预设让 x264 有更多机会搜索更高效的预测和编码决策,代价是编码端需要更多 CPU 和时间。
关键区别是:编码器付出的计算量和解码器的复杂度不是一回事。
我可以让 x264 花很多时间搜索,同时单独限制最终比特流。我的保守输出约束是:
H.264 Main 配置档
Level 3.1
8 位 yuv420p
avc1
refs = 4
B 帧 = 5
B-pyramid = normal
开放 GOP = 关闭
FFmpeg 把 CRF、预设、调优、配置档 限制、参考帧和 B 帧分开控制。我也按同样的方式看待它们:让编码器尽可能认真地搜索,但让播放端保持普通和可预测。
GOP 和 VBV 是护栏,不是主要的画质控制手段
对于这些短篇渐进式播放片段,我使用大约五秒的最大 GOP:30 fps 时约 -g 150,24 fps 时 -g 120,16 fps 时 -g 80。
这是针对我的使用场景做的选择,不是普遍规则。自适应流媒体有不同约束;例如 Apple 的 HLS 制作指南建议每两秒出现一个 IDR。我不会把这条 HLS 规则原样套到短篇静态渐进式 MP4 文件上。
我还大致使用下面这些值:
-maxrate:v 4M
-bufsize:v 8M
这些值只是用来限制异常码率尖峰的上限,并不意味着“所有内容都按 4 Mbps 编码”。正常的比特分配仍然由 CRF 决定,因此容易压缩的片段依旧可以非常小。
我也把 MP4 容器做得尽量普通
我会明确使用 avc1。Apple 当前的 HLS 文档建议使用 avc1 这类 样本格式,而不是 avc3。这并不是文件变小的原因,但符合生成传统 MP4 中的 H.264 文件的目标。
我也使用 -movflags +faststart。FFmpeg 的格式文档说明,faststart 会把 MP4 的 moov 索引移动到文件开头。Android 的 HTTP 流媒体要求同样指出,对于 MPEG-4,moov 必须在 ftyp 之后、mdat 之前。
ftyp
moov
mdat
Faststart 不会改善压缩率。它只是让渐进式 HTTP 播放更顺畅。
对于普通 SDR 输出,我使用 8 位 yuv420p,并标记 BT.709 和 有限视频范围。如果片段没有音频,我不会凭空制造音轨。对于这类插画内容,我还使用 -tune animation;我把它视为针对内容类型的选择,而不是通用兼容性约束的一部分。
核心 FFmpeg 配置
对于 30 fps 的插画类源素材,命令的核心部分大致如下:
ffmpeg -i input \
-c:v libx264 \
-preset veryslow \
-tune animation \
-crf 28 \
-profile:v main \
-level:v 3.1 \
-pix_fmt yuv420p \
-tag:v avc1 \
-refs 4 \
-bf 5 \
-g 150 \
-maxrate:v 4M \
-bufsize:v 8M \
-x264-params "open-gop=0:b-pyramid=normal:nal-hrd=none" \
-color_range tv \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-an \
-movflags +faststart \
output.mp4
缩放和帧率阶段在这里故意没有写死。900×600 的源素材不应该仅仅因为上限是 1280×720 就被放大;天然低帧率的动画也不应该仅仅因为示例使用了 -g 150 就被强制转换成 30 fps。
这条命令只是策略的一种实现,并不等于策略本身。
为什么文件能缩小数倍
没有什么神奇参数。
文件缩小是多个决定叠加后的结果,每个决定都去掉了一类浪费:不必要的像素、不必要的帧、过于保守的质量目标、偏向编码速度而不是效率的设置、过于频繁的关键帧,以及我根本不需要的数据流。
所以,“这个文件是 H.264”其实几乎说明不了它有多大。同一个源素材的两次 H.264 编码可能相差很大,因为编解码器名称并不会告诉你分辨率、帧率、码率控制、预设、GOP 结构、配置档 或源素材如何预处理。
在我的场景里,改变这些围绕编解码器的决策,比更换编解码器本身更重要。
这个结果不能证明什么
我没有在受控实验中逐项隔离每个设置,因此不能诚实地说节省的空间分别有多少百分比来自 veryslow、CRF 28、降低分辨率或降低帧率。
我也不能说所有 CRF 28 输出在感知上都“透明”。“没有明显画质下降”只是我对这类插画素材在常见观看尺寸下的观察,不是对任意视频的科学保证。
我同样不是说一个 H.264 文件就是所有网站都该采用的架构。多版本、自适应流媒体、HDR、4K 和编解码器协商都会改变取舍。
我真正能主张的范围更窄:对于一个带宽优先、移动端用户为主的短篇插画和动画素材库,在编码时间便宜而稳定播放很重要的情况下,这个配置让我的文件缩小了数倍,同时正常播放时仍然看起来自然。
这个结果如何改变了我的优化思路
过去我主要把视频优化看成编码器设置问题。现在我更把它看成整个文件生命周期的成本问题。
编码器也许只运行一次,但这些字节可能会通过网络传输成千上万甚至数百万次。
这会改变“昂贵”这个词的含义。
我很愿意一次性多花 CPU,但我不愿意在今后的每次请求里发送因放大而产生的像素、没有增加有效运动信息的帧,或内容根本不需要的码率。
编解码器还是那个普通选择:MP4 中的 H.264。真正的优化发生在它周围。
对于这种使用场景,最重要的经验比任何单独的 FFmpeg 参数都更清楚:优化你会反复支付的成本,而不是只支付一次的成本。