この最適化を本当に実感させた数字は単純でした。追跡していた実際の実運用動画1本が、新しいH.264方針の適用後、約280 MBから50 MBまで小さくなりました。約5.6分の1、つまり約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%削減 |
1行目が見出しを最も直接的に裏付ける結果です。同じ具体的な動画が、新しい方針の前後で約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を大幅に増やすのは割に合わないことがあります。私のファイルはオフラインで一度エンコードし、その後何度も配信します。この形なら、速いエンコードのせいで将来のすべてのリクエストが大きくなるのであれば、エンコード時間を10分短縮しても経済的な価値はほとんどありません。
だから私には-preset veryslowが合っています。x264がより効率のよい表現を探せるなら、そのためのCPUコストは一度だけ払えばいい。ブラウザはエンコーダーの探索をやり直しません。完成したビットストリームをデコードするだけです。
考え方は単純になりました。一度しか実行しない工程には計算量を使い、何度も繰り返す工程ではバイトを節約する。
新しいコーデックを追わず、H.264を残した理由
H.264が現在もっとも圧縮効率の高いコーデックだと言うつもりはありません。そうではありません。配信側で複数のエンコード版を持ち、端末ごとに最適なものを選べるなら、新しいコーデックは非常に魅力的です。
私の条件は別でした。URLは1つ、ファイルも1つ、コーデックも1つ。そしてモバイル中心のユーザーで、実際の再生トラブルをできるだけ減らしたい。
この目的なら、MP4内のH.264は今でも非常に安全な基準です。Appleは現在、Safariで静的な動画ファイルを配信する場合にH.264でエンコードしたMP4ファイルを使うようウェブ開発者に案内しています。Androidの現行ドキュメントでもMP4内のH.264が掲載されており、Android 6.0以降ではMainプロファイルデコーダーが必須です。H.264の再生向け推奨値にはHDとして1280×720・30 fpsも掲載されていますが、HDはすべての端末で利用できるわけではないとも明記されています。詳しくはAndroidの対応メディア形式を参照してください。
だからといって、現代の端末がMainプロファイルやレベル3.1までしか扱えないわけではありません。たとえばAppleのHLSガイドでは、一般にMainやBaselineプロファイルよりHighプロファイルが推奨されています。私が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%しか残りません。イラスト系の素材では、そのサンプルが細い線、目、髪、指、顔、輪郭の鋭さを支えています。さらに容量を削る必要があるなら、空間情報を一律に43.75%追加で捨てる前に、まずCRFを少し上げた場合を測ります。量子化は次のエンコードで調整できるが、縮小で消した細部は戻りません。
だから720p級は私の慎重な共通上限であり、540pが悪いという意味ではありません。個別の素材では、測定した540p版が勝つこともあります。データなしで、その不可逆な空間削減を全体の規則にはしません。
元素材にないフレームへ帯域を使わない
フレームレートも乗算的に効きます。アニメーションが実質的に毎秒16個ほどの有効な視覚状態しか持たないなら、それを30 fpsや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をおよそ5秒にしています。30 fpsなら約-g 150、24 fpsなら-g 120、16 fpsなら-g 80です。
これは私の用途向けの選択で、一般則ではありません。アダプティブストリーミングには別の制約があります。たとえばAppleのHLSオーサリングガイドは2秒ごとのIDRを推奨しています。私はそのHLSルールを短い静的プログレッシブMP4へそのまま持ち込みません。
さらに、おおよそ次の値を使っています。
-maxrate:v 4M
-bufsize:v 8M
これらは異常なビットレートスパイクに対する上限です。「すべてを4 Mbpsでエンコードする」という意味ではありません。通常のビット配分はCRFが担当するので、簡単なクリップは十分小さくできます。
MP4コンテナも、できるだけ普通にした
avc1を明示的に使っています。Appleの現行HLSドキュメントは、avc3よりavc1のようなサンプル形式を推奨しています。これがファイルを小さくした理由ではありませんが、一般的な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
スケーリングとフレームレートの処理は、ここでは意図的に固定していません。上限が1280×720だからといって900×600の素材を拡大する必要はありませんし、例で-g 150を使っているからといって、本来フレームレートの低いアニメーションを30 fpsへ強制する必要もありません。
コマンドは方針を実装したものです。方針そのものではありません。
なぜファイルは数分の一まで小さくなったのか
魔法のオプションが1つあったわけではありません。
小さくなったのは、別々の無駄を消す複数の判断を重ねたからです。不要な画素、不要なフレーム、保守的すぎる品質目標、効率より速度を優先したエンコーダー設定、必要以上に多いキーフレーム、不要なストリームをそれぞれ削りました。
だから「このファイルはH.264だ」という情報だけでは、サイズはほとんど分かりません。同じ素材をH.264でエンコードしても、解像度、フレームレート、レート制御、プリセット、GOP構造、プロファイル、素材の前処理が違えば、ファイルサイズは大きく変わります。
私の場合は、コーデックを変えるより、その周辺の判断を変えるほうが重要でした。
この結果からは言えないこと
各設定を個別の対照実験で切り分けていないため、veryslow、CRF 28、解像度削減、フレームレート削減がそれぞれ何パーセント効いたのかを正確に言うことはできません。
すべてのCRF 28出力が知覚上透明だとも言えません。「明らかな画質低下を感じなかった」というのは、通常の表示サイズでこのイラスト系素材を見た私の観察であり、あらゆる動画に対する科学的な保証ではありません。
また、単一のH.264ファイルがすべてのサイトに正しい設計だと言っているわけでもありません。複数のエンコード版、アダプティブストリーミング、HDR、4K、コーデックネゴシエーションを使えば、前提は変わります。
実際に言えることはもっと限定的です。帯域を優先し、モバイル利用が多く、エンコード時間は安く、予測しやすい再生互換性を重視する短いイラスト・アニメーションのライブラリでは、この設定でファイルを数分の一にしながら、通常再生では自然に見える状態を保てました。
この結果で最適化の考え方がどう変わったか
以前は動画最適化を主にエンコーダー設定の問題として考えていました。今はファイルの生涯コストの問題として考えています。
エンコーダーが動くのは一度かもしれません。バイトはネットワークを何千回、何百万回と通るかもしれません。
そう考えると、「高い」の意味が変わります。
CPUを一度多く使うのは構いません。それより、拡大で作ったピクセル、意味のある動きを増やさないフレーム、コンテンツが必要としていないビットレートを、将来のすべてのリクエストで送り続けるほうが嫌です。
コーデックは普通のままです。MP4内のH.264。最適化したのはその周辺でした。
この用途では、どのFFmpegオプションよりもこの原則のほうが重要です。一度しか払わないコストではなく、繰り返し払うコストを最適化する。