Terug naar de blog
13 augustus 2026Sergei Solod9 min leestijd

Hoe ik mijn H.264-video’s meerdere keren kleiner maakte zonder duidelijk kwaliteitsverlies

Ik bleef H.264 gebruiken, maar bouwde het encodingprofiel opnieuw op rond CRF 28, x264 veryslow, een resolutieplafond van ongeveer 720p, nuttige framerates en conservatieve decodervereisten. De belangrijkste les was economisch: encoding betaal je één keer, bandbreedte bij elke weergave.

H.264FFmpegVideocompressieWebperformancex264

Het resultaat leek eerst op een verbetering waarvoor je normaal een nieuwere codec nodig hebt: mijn videobestanden werden meerdere keren kleiner, terwijl normale weergave er nog steeds goed uitzag en ik op gebruikelijke kijkgroottes geen duidelijk kwaliteitsverlies zag.

Maar ik was niet overgestapt op AV1, HEVC of VP9. Ik gebruikte nog steeds H.264 in MP4.

Wat veranderde was alles rond de codec. Ik bouwde de encodingpolicy opnieuw op voor één specifieke workload: korte geïllustreerde en geanimeerde clips, een sterk mobiel publiek, bandbreedte als belangrijkste terugkerende kostenpost en vrijwel geen zorg over hoe lang een eenmalige offline encode duurt.

De baseline werd bewust conservatief voor playback en bewust duur voor de encoder: H.264 Main Profile @ Level 3.1, avc1, 8-bit yuv420p, CRF 28, x264 veryslow, een resolutieplafond rond de 720p-klasse, alleen nuttige framerates normaal tot 30 fps, begrensde references en B-frames en faststart voor progressieve MP4-delivery.

De belangrijkste optimalisatie was geen FFmpeg-flag

De grootste verandering was hoe ik naar kosten keek.

Encoding gebeurt één keer. Het bestand versturen gebeurt bij iedere weergave.

Voor realtime video kan veel meer CPU uitgeven om wat bitrate te besparen een slechte ruil zijn. Mijn bestanden worden offline gecodeerd en daarna steeds opnieuw geleverd. In dat model kan tien minuten encodingtijd besparen economisch bijna niets waard zijn als de snellere encode iedere toekomstige request groter maakt.

Daarom is -preset veryslow voor mij logisch. Ik wil best één keer CPU uitgeven als x264 daarmee een efficiëntere representatie kan vinden. De browser herhaalt de zoektocht van de encoder niet; hij decodeert alleen de uiteindelijke bitstream.

De regel werd: besteed rekenkracht aan de stap die één keer gebeurt en wees zuinig met bytes in de stap die steeds terugkomt.

Waarom ik bij H.264 bleef in plaats van de nieuwste codec na te jagen

Ik beweer niet dat H.264 de efficiëntste compressie biedt. Dat doet het niet. Nieuwere codecs zijn aantrekkelijk wanneer een deliverysysteem meerdere renditions kan bewaren en per client de beste kan kiezen.

Mijn beperking was anders: één URL, één bestand, één codec en zo weinig mogelijk playbackproblemen voor een mobile-heavy publiek.

Voor die taak blijft H.264 in MP4 een zeer veilige baseline. Apple adviseert webontwikkelaars momenteel om H.264-gecodeerde MP4-bestanden voor statische video in Safari te gebruiken. De huidige Android-documentatie vermeldt H.264 in MP4 en vereist vanaf Android 6.0 een Main Profile-decoder; de playbackaanbevelingen noemen ook 1280×720 bij 30 fps als HD H.264-configuratie. Zie Androids ondersteunde mediaformaten.

Dat betekent niet dat moderne apparaten beperkt zijn tot Main Profile of Level 3.1. Apple geeft voor HLS bijvoorbeeld doorgaans de voorkeur aan High Profile boven Main of Baseline. Ik koos Main@3.1 omdat ik voor één statische MP4 bewust een bescheiden decoder-envelope wilde, niet omdat Apple het verplicht.

Ik stopte met pixels encoden die niet hoefden te bestaan

Resolutie was een van de grootste hefbomen. Mijn plafond werd ongeveer 1280×720 voor landscape, 720×1280 voor portrait en circa 960×960 voor vierkant of gemengd georiënteerd materiaal.

De belangrijkste regel is: nooit upscalen alleen om dat plafond te bereiken.

Als een bron 900×600 is, brengt 1280×720 geen detail terug. Het creëert alleen meer samples die de encoder moet beschrijven. Een bron van 1920×1080 kan naar de 720p-klasse worden verkleind, terwijl 900×600 ongeveer 900×600 kan blijven. Het plafond is een maximum, geen doel.

Dat klinkt simpel, maar onnodige pixels verwijderen kan meer opleveren dan veel obscure encodertweaks.

Ik stopte met betalen voor frames die de bron eigenlijk niet had

Framerate is nog een vermenigvuldiger. Als een animatie ongeveer 16 echt nuttige visuele toestanden per seconde bevat, maakt opslag op 30 of 60 fps de beweging niet automatisch beter. Het kan vooral herhaalde of gesynthetiseerde tijdsamples toevoegen die alsnog gecodeerd moeten worden.

Mijn policy is de nuttige cadans van de bron te behouden en normaal op of onder 30 fps te blijven. Voor dit soort materiaal kunnen 12, 15, 16, 18, 20, 24, 25 of 30 fps allemaal logisch zijn als ze de bron werkelijk beschrijven.

Ik geef ook de voorkeur aan een schone CFR-output. VFR is niet inherent kapot; CFR maakt timestamps, frameaantallen, durationchecks, seeking en latere validatie in mijn pipeline gewoon eenvoudiger.

Het algemene principe is nuttiger dan één fps-getal: betaal geen bandbreedte voor temporele informatie die de bron niet bevat.

CRF 28 is een workloadkeuze, geen magisch getal

Ik wilde niet iedere clip naar dezelfde target bitrate dwingen. Een bijna statische illustratie en een scène met complexe beweging hebben niet hetzelfde aantal bits nodig om acceptabel te ogen.

Daarom gebruik ik x264's CRF-modus en kwam ik voor deze bandwidth-first geïllustreerde workload rond -crf 28 uit. FFmpeg documenteert CRF in libx264 als constant-quality rate control; zie de FFmpeg-codecdocumentatie.

CRF 28 is bewust agressief. Ik zou die waarde niet blind kopiëren naar film grain, rumoerige camerabeelden, kleine tekst in screen recordings of een workload waarin fidelity belangrijker is dan bandbreedte.

Ik heb ook geen universele perceptuele score die bewijst dat CRF 28 transparant is. Wat ik voor mijn eigen materiaal kan zeggen is smaller: de bestanden werden veel kleiner en bleven er bij gewone weergave normaal uitzien. Dat is een praktische observatie, geen claim van visuele losslessness.

Veryslow is duur voor de encoder, niet automatisch voor de decoder

Mijn preset is -preset veryslow. Een langzamere preset geeft x264 meer kans om efficiënte prediction- en codingbeslissingen te vinden. De prijs is CPU en encodingtijd.

Het belangrijke onderscheid is dat encoderinspanning en decodercomplexiteit niet hetzelfde zijn.

Ik kan x264 hard laten werken en de uiteindelijke stream toch apart begrenzen. Mijn conservatieve outputcontract is:

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

FFmpeg stelt CRF, presets, tuning, profielrestricties, reference frames en B-frames afzonderlijk beschikbaar. Zo behandel ik ze ook: de encoder mag duur zoeken, de playbackkant moet gewoon blijven.

GOP en VBV zijn vangrails, niet de hoofdregeling voor kwaliteit

Voor deze korte progressieve clips gebruik ik een maximale GOP van ongeveer vijf seconden: circa -g 150 bij 30 fps, -g 120 bij 24 fps of -g 80 bij 16 fps.

Dat is een workloadkeuze, geen universele regel. Adaptive streaming heeft andere beperkingen; Apple adviseert voor HLS bijvoorbeeld IDR's om de twee seconden. Die HLS-regel kopieer ik niet automatisch naar korte statische progressieve MP4-bestanden.

Daarnaast gebruik ik ongeveer:

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

Die waarden zijn plafonds tegen uitzonderlijke bitratepieken. Ze betekenen niet “encode alles op 4 Mbps”. CRF blijft de normale bitallocatie sturen, zodat eenvoudige clips nog steeds zeer klein kunnen worden.

Ik maakte ook de MP4-container saai

Ik gebruik expliciet avc1. Apples huidige HLS-documentatie beveelt sample formats zoals avc1 aan boven avc3. Dat maakte mijn bestanden niet kleiner, maar past bij het doel van conventionele H.264-in-MP4-output.

Ik gebruik ook -movflags +faststart. Volgens de FFmpeg-formatdocumentatie verplaatst faststart de MP4-moov-index naar het begin van het bestand. Androids HTTP-streamingeisen zeggen eveneens dat bij MPEG-4 moov na ftyp maar vóór mdat moet staan.

ftyp
moov
mdat

Faststart verbetert de compressie niet. Het maakt progressieve HTTP-weergave eenvoudiger.

Voor normale SDR-output gebruik ik 8-bit yuv420p en signaleer ik BT.709 met limited/video range. Als een clip geen audio nodig heeft, maak ik geen audiotrack. Voor geïllustreerd materiaal gebruik ik ook -tune animation; dat zie ik als contentspecifieke keuze, niet als onderdeel van het universele compatibiliteitscontract.

Het centrale FFmpeg-profiel

Voor een geïllustreerde bron op 30 fps ziet de kern van het commando er ongeveer zo uit:

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 en framerate zijn hier bewust niet hardcoded. Een bron van 900×600 moet niet worden vergroot alleen omdat het plafond 1280×720 is, en een animatie met van nature lage framerate moet niet naar 30 fps worden gedwongen alleen omdat het voorbeeld -g 150 gebruikt.

Het commando implementeert het beleid; het is niet het beleid zelf.

Waarom de bestanden meerdere keren kleiner werden

Er was geen wonderflag.

De reductie kwam door meerdere beslissingen te stapelen die elk een ander soort verspilling verwijderden: onnodige pixels, onnodige frames, fixed-bitrate-denken, goedkope encoderinstellingen, te frequente keyframes en streams die ik niet nodig had.

Daarom zegt “dit bestand is H.264” verrassend weinig over de grootte. Twee H.264-encodes van dezelfde bron kunnen sterk verschillen omdat de codecnaam niets zegt over resolutie, FPS, rate control, preset, GOP-structuur, profile of bronvoorbewerking.

In mijn geval waren die beslissingen rond de codec belangrijker dan het vervangen van de codec zelf.

Wat dit resultaat niet bewijst

Ik heb niet elke instelling geïsoleerd in een gecontroleerd experiment, dus ik kan niet eerlijk een exact percentage van de besparing afzonderlijk toeschrijven aan veryslow, CRF 28, resolutieverlaging of framerateverlaging.

Ik kan ook niet beweren dat elke CRF 28-output perceptueel transparant is. “Geen duidelijk kwaliteitsverlies” is mijn observatie voor deze geïllustreerde workload bij normale kijkgroottes, geen wetenschappelijke garantie voor willekeurige video.

En ik beweer niet dat één H.264-bestand voor elke site de juiste architectuur is. Multiple renditions, adaptive streaming, HDR, 4K en codec negotiation veranderen de trade-offs.

Het resultaat is smaller en nuttiger: voor een bandwidth-first, mobile-heavy bibliotheek met korte geïllustreerde en geanimeerde clips, waar encodingtijd goedkoop is en voorspelbare playback belangrijk, maakte dit profiel mijn bestanden meerdere keren kleiner terwijl normale weergave er nog steeds goed uitzag.

De regel die ik nu gebruik

Vroeger zag ik video-optimalisatie vooral als een probleem van encoderinstellingen. Nu zie ik het als een probleem van kosten over de levensduur van het bestand.

De encoder draait misschien één keer. De bytes kunnen duizenden of miljoenen keren over het netwerk gaan.

Dat verandert wat “duur” betekent.

Ik geef graag één keer CPU uit. Ik ben veel minder bereid om bij iedere toekomstige request ge-upscalede pixels, frames zonder nuttige extra beweging of bitrate die de content niet nodig heeft te versturen.

De codec bleef saai: H.264 in MP4. De optimalisatie gebeurde eromheen.

Voor mijn workload is de les nuttiger dan welke FFmpeg-flag dan ook: optimaliseer de kosten die je steeds opnieuw betaalt, niet de kosten die je één keer betaalt.