返回博客
2026年8月13日Sergei Solod9 分钟阅读

我如何在几乎看不出画质下降的情况下,把 H.264 视频缩小到原来的几分之一

Codec 仍然是 H.264。我真正改的是整套编码策略:CRF 28、x264 veryslow、720p 级别的分辨率上限,以及只保留真正有用的帧率。文件缩小了数倍,普通播放时画面仍然自然。最重要的认识是:编码成本只付一次,而带宽成本会在每次播放时重复支付。

H.264FFmpeg视频压缩Web 性能x264

结果看起来像是换了更新的 codec 才会有的提升:我的视频文件缩小到了原来的几分之一,但普通播放仍然很自然,在常见观看尺寸下我也看不出明显的画质下降。

但我并没有换成 AV1、HEVC 或 VP9。我仍然使用MP4 容器里的 H.264

真正改变的是 codec 周围的所有决策。我针对一个非常具体的 workload 重做了编码策略:短小的插画和动画片段,mobile-heavy 的用户群,bandwidth 是最重要的重复成本,而一次性的 offline encode 花多长时间几乎不重要。

最终的 baseline 在 playback 侧刻意保守,在 encoding 侧则刻意昂贵:H.264 Main Profile @ Level 3.1、avc1、8-bit yuv420p、CRF 28、x264 veryslow、720p 级别的分辨率上限、通常不高于 30 fps 的有效帧率、受限的 reference frame 与 B-frame,以及用于 progressive MP4 delivery 的 faststart

最重要的优化并不是某个 FFmpeg 参数

最大的变化,是我重新理解了成本。

Encoding 只发生一次。Delivery 则会在每次有人请求这个文件时发生。

对于 real-time video,为了省一点 bitrate 而多耗很多 CPU 可能并不划算。我的文件是 offline encode,然后被反复分发。在这种模型里,如果更快的 encode 会让未来每个 request 都更大,那么节省十分钟编码时间在经济上可能几乎没有意义。

这就是为什么 -preset veryslow 对我合理。如果 x264 能用更多 CPU 找到更高效的表示,我愿意只支付一次这笔成本。Browser 不会重复 encoder 的搜索过程,它只负责 decode 最终的 bitstream。

规则变得很简单:在只执行一次的步骤上花计算量,在会重复发生的步骤上节省 byte

为什么我没有追逐最新 codec,而是继续用 H.264

我并不认为 H.264 是现有压缩效率最高的 codec。它不是。如果 delivery system 能保存多个 rendition,并针对不同 client 选择最合适的版本,那么更新的 codec 非常有吸引力。

我的限制不同:一个 URL、一个文件、一个 codec,并且对 mobile-heavy 用户尽可能减少实际播放问题。

对这个目标来说,MP4 中的 H.264 依然是非常安全的 baseline。Apple 目前明确建议 Web 开发者在 Safari 中提供 static video 时使用采用 H.264 编码的 MP4 文件。Android 当前文档也列出了 MP4 中的 H.264,并要求 Android 6.0 及以上具备 Main Profile decoder;它的 H.264 HD playback 推荐中还包含 1280×720、30 fps。详见 Android supported media formats

这并不表示现代设备只能处理 Main Profile 或 Level 3.1。例如 Apple 的 HLS 指南通常更倾向 High Profile,而不是 Main 或 Baseline。我选择 Main@3.1,是因为希望一个单独的 static MP4 拥有刻意保守的 decoder envelope,而不是因为 Apple 强制要求它。

不需要存在的 pixel,就不要 encode

Resolution 是最有效的杠杆之一。我的上限大致是:landscape 1280×720,portrait 720×1280,square 或 mixed-orientation 内容大约 960×960

更重要的规则是:不要为了碰到上限而 upscale

如果 source 是 900×600,把它放大到 1280×720 并不会恢复任何细节,只会制造更多需要 encoder 描述的 sample。1920×1080 的 source 可以缩到 720p 级别,而 900×600 的 source 可以继续保持在 900×600 左右。Ceiling 是最大值,不是目标值。

这听起来很简单,但去掉不必要的 pixel,往往比许多隐晦的 encoder tweak 更重要。

source 没有的 frame,就不要为它付 bandwidth

Frame rate 是另一个乘数。如果一段 animation 每秒实际上只有大约 16 个有意义的视觉状态,把它保存成 30 或 60 fps 并不会自动得到更好的运动效果。很多时候只是多出重复或合成的 temporal sample,而这些依然需要被表示。

我的策略是保留 source 真正有用的 cadence,并且通常保持在 30 fps 或以下。对于这类素材,只要确实符合 source,12、15、16、18、20、24、25、30 fps 都可能是合理的。

在生成的 output 中,我也更喜欢干净的 CFR。VFR 本身并没有问题;只是对我的 pipeline 来说,CFR 让 timestamps、frame counts、duration checks、seeking 以及后续 validation 更容易处理。

比某个具体 FPS 数字更重要的是原则:不要为 source 中不存在的时间信息支付 bandwidth

CRF 28 是 workload 选择,不是魔法数字

我不想把每个 clip 都强行推向同一个 target bitrate。几乎静止的 illustration 和运动复杂的 scene,并不需要相同数量的 bit 才能看起来正常。

因此我使用 x264 的 CRF mode,并针对这个 bandwidth-first 的插画类 workload,最终选择了大约 -crf 28。FFmpeg 将 libx264 中的 CRF 描述为 constant-quality rate control,详见 FFmpeg codec documentation

CRF 28 是刻意偏激进的设置。我不会把它盲目复制到 film grain 很重、camera noise 很多、画面包含极小文字,或者 fidelity 比 bandwidth 更重要的 workload。

我也没有一个 universal perceptual score 能证明 CRF 28 一定 transparent。我能从自己的 workload 得出的结论更窄:文件明显变小了,而普通 playback 在我看来仍然正常。这是实践观察,不是宣称 CRF 28 visually lossless。

veryslow 对 encoder 很贵,不代表对 decoder 也自动很贵

我使用的 preset 是 -preset veryslow。更慢的 preset 会给 x264 更多机会搜索更高效的 prediction 和 coding decision,代价是 encoding CPU 与时间。

关键区别在于:encoder effort 和 decoder complexity 不是一回事

我可以让 x264 做非常重的搜索,同时单独限制最终 stream。我的保守 output contract 是:

H.264 Main Profile
Level 3.1
8-bit yuv420p
avc1
refs = 4
B-frames = 5
B-pyramid = normal
open GOP = disabled

FFmpeg 分别提供 CRF、preset、tune、profile restriction、reference frame 与 B-frame 控制。我也是这样理解它们的:让 encoder 认真搜索,但保持 playback 侧普通、可预测。

GOP 与 VBV 是护栏,不是主要的质量控制

对于这些短 progressive clip,我使用大约五秒的 maximum GOP:30 fps 时约 -g 150,24 fps 时 -g 120,16 fps 时 -g 80

这是 workload 选择,不是 universal rule。Adaptive streaming 有不同约束。例如 Apple HLS authoring guidance 推荐每两秒一个 IDR。我不会把这条 HLS 规则直接照搬到短小的 static progressive MP4。

我还大致使用:

-maxrate:v 4M
-bufsize:v 8M

这些值是防止异常 bitrate spike 的 ceiling,不是“所有内容都编码到 4 Mbps”。正常的 rate allocation 仍由 CRF 决定,因此简单 clip 依然可以变得非常小。

MP4 container 也刻意保持普通

我明确使用 avc1。Apple 当前的 HLS 文档更推荐 avc1 这类 sample format,而不是 avc3。这不会让文件更小,但符合我生成常规 H.264-in-MP4 的目标。

我还使用 -movflags +faststart。FFmpeg 的format documentation说明,faststart 会把 MP4 的 moov index 移到文件开头。Android 的 HTTP streaming requirement 同样要求 MPEG-4 中 moov 位于 ftyp 之后、mdat 之前。

ftyp
moov
mdat

faststart 不会改善 compression,它只是让 progressive HTTP playback 更顺畅。

普通 SDR output 我使用 8-bit yuv420p,并 signal BT.709 与 limited/video range。如果 clip 不需要 audio,我不会制造 audio track。针对这类 illustration content,我还使用 -tune animation;它是 content-specific 的选择,不是 universal compatibility contract 的一部分。

核心 FFmpeg profile

对于 30 fps 的 illustration source,command 的核心大致如下:

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

Scaling 与 frame-rate stage 在这里是故意没有 hard-code 的。900×600 source 不应该因为 ceiling 是 1280×720 就被放大;自然 frame rate 较低的 animation,也不应该因为示例中用了 -g 150 就被强制变成 30 fps。

Command 是 policy 的实现,不是 policy 本身。

为什么文件能缩小几倍

并不存在一个 magic flag。

体积下降来自多个决定的叠加,每个决定去掉一种不同的浪费:不必要的 pixel、不必要的 frame、fixed-bitrate 思维、过于便宜的 encoder setting、过于频繁的 keyframe,以及根本不需要的 stream。

所以只说“这个文件是 H.264”,其实几乎无法判断它有多大。同一个 source 的两个 H.264 encode 可能差异巨大,因为 codec 名称并没有说明 resolution、frame rate、rate control、preset、GOP structure、profile 或 source preparation。

对我来说,改变 codec 周围这些决策,比更换 codec 本身更重要。

这个结果不能证明什么

我没有在 controlled experiment 中逐项 isolate 每个 setting,因此无法诚实地给出一个精确比例,说明 savings 中有多少分别来自 veryslow、CRF 28、resolution reduction 或 frame-rate reduction。

我也不能声称所有 CRF 28 output 都 perceptually transparent。“没有明显画质下降”只是我在正常观看尺寸下对这类 illustration workload 的观察,不是对任意视频的科学保证。

我更不是在说一个 H.264 文件适合所有网站。Multiple renditions、adaptive streaming、HDR、4K、codec negotiation 都会改变 trade-off。

我的结论更窄,也因此更实用:对于一个 bandwidth-first、mobile-heavy 的短插画/动画 clip library,如果 encoding time 很便宜,而 predictable playback 很重要,那么这套 profile 能让我的文件缩小数倍,同时在普通播放下仍然看起来正常。

我现在遵循的规则

以前我主要把 video optimization 看成 encoder setting 问题。现在我把它看成 lifetime cost 问题。

Encoder 也许只运行一次。Byte 可能通过 network 成千上万、甚至数百万次。

这会改变“昂贵”的含义。

我愿意多花一次 CPU。相比之下,我更不愿意在每个未来 request 中持续发送 upscale 生成的 pixel、没有增加有效 motion 的 frame,或者 content 根本不需要的 bitrate。

Codec 仍然很普通:MP4 中的 H.264。优化发生在它周围。

对于这个 workload,结论比任何单独的 FFmpeg flag 都更清楚:优先优化你会反复支付的成本,而不是只支付一次的成本