ブログに戻る
2026年8月13日Sergei Solod12 分で読めます

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は、再生側では意図的に保守的、エンコード側では意図的に重い構成です。H.264 Main Profile @ Level 3.1、avc1、8-bit yuv420p、CRF 28、x264 veryslow、720pクラスの解像度上限、通常は30 fps以下の実用的なフレームレート、reference frameとB-frameの上限、そしてprogressive MP4配信用のfaststartです。

一番効いた最適化は、FFmpegのflagではなかった

もっとも大きく変わったのは、コストの見方でした。

Encodingは一度です。Deliveryは、そのファイルが要求されるたびに発生します。

Real-time videoなら、少しbitrateを下げるために大量のCPUを使うのは割に合わないことがあります。私のファイルはofflineでencodeし、その後何度も配信します。このモデルでは、encodeを10分短縮できても、その代わりに今後のすべてのrequestが少しずつ大きくなるなら、経済的にはほとんど意味がありません。

だから私には-preset veryslowが合っています。x264がより効率のよい表現を探すためにCPUを使えるなら、そのコストは一度払えばいい。Browserはencoderの探索処理を繰り返しません。完成したbitstreamをdecodeするだけです。

考え方は単純になりました。一度しか行わない処理には計算資源を使い、何度も繰り返す処理ではbyteを節約する

新しいcodecを追わず、H.264を残した理由

H.264が最も圧縮効率の高いcodecだと言うつもりはありません。そうではありません。複数のrenditionを用意し、clientごとに適したものを選べるdelivery systemなら、新しいcodecには大きなメリットがあります。

私の制約は別でした。URLは一つ、ファイルも一つ、codecも一つ。そしてmobile-heavyなユーザー環境で、実運用上の再生トラブルをできるだけ減らしたい。

この条件では、MP4内のH.264は今でもかなり安全なbaselineです。Appleは現在、Safariでstatic videoを配信する場合、Web開発者にH.264でエンコードしたMP4ファイルを使うよう案内しています。Androidの現行ドキュメントでもMP4内のH.264が記載され、Android 6.0以降ではMain Profile decoderが必須です。またH.264のHD構成例として1280×720、30 fpsが挙げられています。詳しくはAndroidのSupported media formatsを参照できます。

だからといって、現代の端末がMain ProfileやLevel 3.1までしか扱えないわけではありません。たとえばAppleのHLSガイドでは、一般にMainやBaselineよりHigh Profileが推奨されています。私がMain@3.1を選んだのは、単一のstatic MP4に対して意図的に控えめなdecoder envelopeを設定したかったからで、AppleがMain@3.1を必須としているからではありません。

存在する必要のないpixelをencodeしない

Resolutionは大きなレバーの一つでした。上限はlandscapeでおよそ1280×720、portraitで720×1280、正方形やmixed-orientationの素材ではおよそ960×960にしました。

それ以上に重要なのは、上限に合わせるためだけのupscaleはしないことです。

sourceが900×600なら、1280×720に拡大してもディテールは戻りません。encoderが表現しなければならないsampleが増えるだけです。1920×1080のsourceなら720pクラスまで縮小できますが、900×600なら900×600前後のままで構いません。Ceilingは最大値であって、目標値ではありません。

単純な話ですが、不要なpixelを減らすほうが、細かなencoder tweakをいくつも試すより効くことがあります。

sourceにないframeのためにbandwidthを使わない

Frame rateも大きな乗数です。1秒あたり実質16個ほどの有効な画面状態しかないanimationを30 fpsや60 fpsで保存しても、動きが自動的に滑らかになるわけではありません。多くの場合、繰り返しや補間によるtemporal sampleが増え、それも結局は表現する必要があります。

私の方針は、sourceの有効なcadenceを維持し、通常は30 fps以下に抑えることです。この種の素材なら、12、15、16、18、20、24、25、30 fpsのどれも、sourceを正しく表しているなら妥当です。

生成後のoutputでは、扱いやすいCFRも好んで使っています。VFRそのものが壊れているわけではありません。私のpipelineでは、CFRのほうがtimestamps、frame count、duration check、seeking、その後のvalidationを扱いやすいだけです。

特定のFPS値より重要なのは原則です。sourceに存在しない時間方向の情報にbandwidthを払わない

CRF 28はworkload向けの選択であって、魔法の数字ではない

すべてのclipを同じtarget bitrateに押し込むつもりはありませんでした。ほぼ静止画に近いイラストと、複雑な動きがあるsceneでは、自然に見せるために必要なbit数が違います。

そこでx264のCRF modeを使い、このbandwidth-firstなイラスト系workloadでは-crf 28前後に落ち着きました。FFmpegはlibx264のCRFをconstant-quality rate controlとして説明しています。詳しくはFFmpeg codec documentationを参照できます。

CRF 28は意図的に攻めた設定です。Film grainが多い映像、noiseの多いcamera footage、小さな画面文字、あるいはbandwidthよりfidelityを優先するworkloadに、そのままコピーするつもりはありません。

また、CRF 28なら常に知覚的にtransparentだと証明するuniversalなperceptual scoreを持っているわけでもありません。私のworkloadについて言えるのはもっと限定的です。ファイルは大幅に小さくなり、通常再生では私には自然に見えました。これは実運用上の観察であり、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制限、reference frame、B-frameをそれぞれ独立して指定できます。私も同じように考えています。encoderにはしっかり探索させる。ただしplayback側は普通のままにする。

GOPとVBVは品質の本体ではなく、ガードレール

この短いprogressive clipでは、maximum GOPをおよそ5秒にしています。30 fpsなら-g 150、24 fpsなら-g 120、16 fpsなら-g 80程度です。

これはworkloadに合わせた選択で、普遍的なルールではありません。Adaptive streamingには別の制約があります。たとえばAppleのHLS authoring guidanceでは2秒ごとのIDRが推奨されています。私はそのHLSルールを、短いstatic progressive MP4にそのまま持ち込んではいません。

さらに、おおよそ次の値も使っています。

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

これは異常なbitrate spikeを抑えるためのceilingです。「すべてを4 Mbpsでencodeする」という意味ではありません。通常のrate allocationはCRFに任せるので、圧縮しやすいclipは非常に小さくなれます。

MP4 containerも、あえて普通にした

avc1を明示的に使っています。Appleの現行HLSドキュメントでは、avc3よりavc1のようなsample formatが推奨されています。これはファイルサイズを小さくする設定ではありませんが、一般的なH.264-in-MP4を作るという目的には合っています。

-movflags +faststartも使っています。FFmpegのformat documentationによると、faststartはMP4のmoov indexをファイル先頭へ移動します。AndroidのHTTP streaming要件でも、MPEG-4ではftypの後、mdatより前にmoovを置く必要があります。

ftyp
moov
mdat

faststartは圧縮率を改善しません。Progressive HTTP playbackを扱いやすくするためのものです。

通常のSDR outputには8-bit yuv420pを使い、BT.709とlimited/video rangeをsignalしています。音声が不要なclipにはaudio trackを作りません。イラスト系contentでは-tune animationも使っていますが、これはcontent-specificな選択であり、普遍的なcompatibility contractの一部とは考えていません。

実際のFFmpeg profile

30 fpsのイラスト系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の処理は、ここでは意図的にhard-codeしていません。900×600のsourceを、ceilingが1280×720だからという理由だけで拡大するべきではありません。また、もともと低frame-rateのanimationを、例が-g 150だからという理由だけで30 fpsにするべきでもありません。

Commandはpolicyの実装であって、policyそのものではありません。

なぜファイルは数分の一まで小さくなったのか

魔法のflagが一つあったわけではありません。

効果は、異なる種類の無駄を消す複数の判断を積み重ねた結果です。不要なpixel、不要なframe、fixed-bitrate前提の考え方、軽いencoder設定、必要以上に頻繁なkeyframe、不要なstream。それぞれ別の無駄を減らしました。

だから「このファイルはH.264です」という情報だけでは、サイズについて驚くほど何も分かりません。同じsourceのH.264 encodeでも、resolution、frame rate、rate control、preset、GOP structure、profile、source preparationが違えばサイズは大きく変わります。

私の場合、codecを変えるより、その周辺の判断を変えるほうが重要でした。

この結果から言えないこと

各settingをcontrolled experimentで一つずつ分離したわけではないため、節約できた容量の何%がveryslow、CRF 28、resolution reduction、frame-rate reductionのどれによるものかを正確に割り当てることはできません。

CRF 28のすべてのoutputが知覚的にtransparentだとも言えません。「明らかな画質低下を感じなかった」というのは、このイラスト系workloadを通常の表示サイズで見た私の観察であり、任意の動画に対する科学的な保証ではありません。

また、単一のH.264ファイルがすべてのサイトに正しいarchitectureだと主張しているわけでもありません。Multiple renditions、adaptive streaming、HDR、4K、codec negotiationが必要ならtrade-offは変わります。

ここで言えることは、もっと狭く、そのぶん実用的です。短いイラスト・アニメーションclipを大量に扱い、bandwidthを重視し、mobile-heavyで、encoding timeは安く、predictable playbackを優先する私のlibraryでは、このprofileによってファイルは数分の一まで小さくなり、通常再生では自然に見える状態を保てました。

今はこう考えている

以前はvideo optimizationを主にencoder settingの問題として考えていました。今はlifetime costの問題として考えています。

Encoderが動くのは一度かもしれません。Byteはnetworkを何千回、何百万回と通るかもしれません。

そう考えると、「高いコスト」の意味が変わります。

CPUを一度多く使うことには抵抗がありません。それよりも、upscaleで作っただけのpixel、実際のmotionを増やさないframe、contentが必要としていないbitrateを、その後のすべてのrequestで送り続けるほうを避けたい。

Codecは地味なままです。MP4内のH.264。最適化したのはその周辺でした。

このworkloadで得た教訓は、個別のFFmpeg flagより明確です。一度しか払わないコストではなく、何度も払い続けるコストを最適化する