Terug naar de blog
13 augustus 2026Sergei Solod12 min leestijd

Hoe ik een productievideo met H.264 van ~280 MB naar ~50 MB bracht

Een concrete video uit de productieomgeving kromp na de vernieuwde H.264-configuratie van ongeveer 280 MB naar ongeveer 50 MB. De nieuwe aanpak combineert CRF 28, x264 veryslow, een resolutieplafond in de 720p-klasse, zinvolle beeldsnelheden en bescheiden eisen aan de decoder. In een eerdere fase was hetzelfde voorbeeld al van ongeveer 350 naar 238 MB teruggebracht; een controle van de oude bibliotheek liet bovendien zien dat H.264-bestanden van meerdere megabit per seconde heel normaal waren.

H.264FFmpegx264VideocompressieInternetprestatiesMedia-optimalisatie

Het getal dat deze optimalisatie voor mij echt concreet maakte was simpel: één echte productievideo ging na het nieuwe H.264-beleid van ongeveer 280 MB naar 50 MB. Dat is ongeveer 5,6× kleiner en bespaart circa 230 MB, oftewel ongeveer 82% van de oorspronkelijke bestandsgrootte.

Ik bereikte dat niet door de levering naar AV1, HEVC of VP9 te verplaatsen. De productie-uitvoer bleef H.264 in MP4. Wat veranderde was het beleid rond de codec: minder overbodige pixels, minder nutteloze temporele monsters, een veel minder behoudend kwaliteitsdoel, meer codeerwerk tijdens de eenmalige stap en bewust begrensde eisen aan de decodeerder.

De toepassing was specifiek: korte geïllustreerde en geanimeerde video’s, ongeveer 80% mobiel verkeer, bandbreedte als terugkerende kostenpost en vooraf coderen waarbij CPU-tijd goedkoop is vergeleken met jarenlang te grote bestanden blijven versturen.

Samengevat zag oud versus nieuw er ongeveer zo uit:

oude configuratie
H.264 Main @ Level 4.0
CRF 19
voorinstelling slow
tot 1920x1080 / 1080x1920
30 fps
refs = 3
B-beelden = 3
GOP ≈ 2 seconden
VBV ≈ 10M / 20M

nieuwe configuratie
H.264 Main @ Level 3.1
CRF 28
voorinstelling veryslow
720p-klasse als plafond, niet vergroten
zinvolle CFR, normaal <= 30 fps
refs = 4
B-beelden = 5
GOP ≈ 5 seconden
VBV ≈ 4M / 8M
yuv420p / avc1 / faststart

Het gemeten resultaat: van ~280 MB naar ~50 MB

Ik heb meerdere echte productiemetingen, maar ze zijn niet allemaal dezelfde proef. Die categorieën uit elkaar houden is belangrijker dan het grootste percentage uitkiezen.

MetingVoorNaReductie
Dezelfde concrete video~280 MB~50 MB~5,6× kleiner / ~82% minder
Eerdere fase van hetzelfde bestand~350 MB~238 MB~1,47× kleiner / 32% minder

De eerste rij is het schoonste bewijs voor de titel: dezelfde concrete video vóór en na het nieuwere beleid, ongeveer 280 MB tegenover ongeveer 50 MB. Voor precies dat paar heb ik in de teruggevonden oude logboeken geen bewaarde duur/bitsnelheid-regel, dus die verzin ik niet. De bestandsgrootte alleen laat al een factor van ongeveer 5,6 zien.

De ~350→238 MB-case was een eerdere optimalisatiefase van hetzelfde voorbeeld in die periode. De uitvoer was ongeveer 1264×720, 30 fps, ~500 seconden, zonder geluid en rond 3,8 Mbps. De rekensom klopt: 3,8 Mbps gedurende ongeveer 500 seconden is ongeveer 238 MB. Dat was al 32% minder dan ~350 MB, maar nog steeds veel te groot voor mijn bandbreedtedoel.

De oude bibliotheek liet ook zien dat grote H.264-bestanden geen toevallige uitschieter waren. Eén controle bevatte 238 productievideo’s met samen 6,37 GB: 117 H.264 en 121 AV1. 101 bestanden waren minstens 20 MB en 34 minstens 50 MB. Enkele grote H.264-bestanden:

GrootteDuurGemiddelde bitsnelheid
121,0 MB4:253,83 Mbps
101,2 MB5:192,659 Mbps
92,78 MB4:442,735 Mbps
90,10 MB3:553,206 Mbps
89,74 MB4:412,676 Mbps

Dit zijn verschillende bronnen, dus de tabel geeft context en is geen A/B-test. Wel blijkt eruit dat het oude voorbeeld van 3,8 Mbps geen toevallige uitschieter was: meerdere grote H.264-bestanden uit de oude bibliotheek zaten werkelijk rond 2,6–3,8 Mbps.

De belangrijkste optimalisatie was geen FFmpeg-optie

De grootste verandering zat in hoe ik over kosten nadacht.

Coderen gebeurt één keer. Het bestand versturen gebeurt telkens wanneer iemand het opvraagt.

Bij realtimevideo kan veel extra CPU gebruiken om een beetje bitsnelheid te besparen een slechte afweging zijn. Mijn bestanden worden vooraf gecodeerd en daarna steeds opnieuw geleverd. In dat model kan tien minuten codeertijd besparen economisch nauwelijks iets betekenen als de snellere codering elke toekomstige overdracht groter maakt.

Daarom is -preset veryslow voor mij logisch. Ik wil best één keer CPU uitgeven als x264 die tijd kan gebruiken om een efficiëntere representatie te vinden. De browser herhaalt het zoekwerk van de codeerder niet; hij decodeert alleen de uiteindelijke bitstroom.

De regel werd simpel: besteed rekenwerk 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 beschikbare compressie biedt. Dat doet het niet. Nieuwere codecs kunnen aantrekkelijk zijn wanneer een distributiesysteem meerdere versies kan bewaren en per apparaat de beste kan kiezen.

Mijn beperking was anders: één URL, één bestand, één codec en zo weinig mogelijk gedoe met afspelen bij een overwegend mobiel publiek.

Voor dat doel blijft H.264 in MP4 een zeer veilige basis. Apple adviseert webontwikkelaars momenteel 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-profiel-decodeerder; de aanbevelingen voor H.264-weergave noemen voor HD ook 1280×720 bij 30 fps, met de opmerking dat HD niet op elk apparaat beschikbaar is. Zie de ondersteunde mediaformaten van Android.

Dat betekent niet dat moderne apparaten beperkt zijn tot Main-profiel of niveau 3.1. Apple geeft in zijn HLS-richtlijnen bijvoorbeeld doorgaans de voorkeur aan High-profiel boven Main of Baseline-profiel. Ik koos Main@3.1 omdat ik voor één statisch MP4-bestand bewust bescheiden decodeerdereisen wilde, niet omdat Apple dit profiel verplicht.

Ik stopte met het coderen van pixels die niet nodig waren

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

De belangrijkere regel is: schaal nooit omhoog alleen om het plafond te bereiken.

Als een bron 900×600 is, levert vergroten naar 1280×720 geen detail terug. Het maakt alleen meer monsters die de codeerder moet beschrijven. Een bron van 1920×1080 kan naar de 720p-klasse worden verkleind, terwijl een bron van 900×600 ongeveer 900×600 kan blijven. Het plafond is een maximum, geen doel.

Dat klinkt eenvoudig, maar onnodige pixels verwijderen kan meer opleveren dan veel obscure fijnafstellingen van de codeerder.

Dat plafond is geen willekeurig rond getal. Een beeld van 1920×1080 bevat 2.073.600 pixels; 1280×720 bevat er 921.600. De stap van 1080p naar 720p verwijdert dus ongeveer 55,6% van de ruimtelijke monsters voordat de codeerder überhaupt compressiebeslissingen neemt.

Ik heb 540p ook als algemene standaard overwogen. Maar 960×540 bevat slechts 518.400 pixels: 43,75% minder dan 1280×720, zodat 56,25% van de 720p-monsters overblijft. Bij geïllustreerd materiaal beschrijven die monsters dunne lijnen, ogen, haar, vingers, gezichten en scherpe contouren. Als ik nog minder bytes nodig heb, test ik liever eerst een iets hogere CRF dan zonder meting nog eens 43,75% van de ruimtelijke informatie weg te gooien. Kwantisering kan bij een volgende codering worden aangepast; detail dat door verkleinen is verwijderd, is al verdwenen.

Daarom is de 720p-klasse mijn voorzichtige algemene plafond, niet de stelling dat 540p slecht is. Voor een specifiek bestand kan een gemeten 540p-versie best winnen. Ik maak die onomkeerbare ruimtelijke stap alleen niet zonder gegevens tot algemene regel.

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

Beeldsnelheid is nog een vermenigvuldigingsfactor. Als een animatie ongeveer 16 werkelijk nuttige visuele toestanden per seconde bevat, maakt opslaan als 30 of 60 fps de beweging niet automatisch beter. Vaak ontstaan vooral herhaalde of gesynthetiseerde tijdmonsters die alsnog moeten worden gecodeerd.

Mijn beleid 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 in de gegenereerde uitvoer ook de voorkeur aan een nette constante beeldsnelheid. VFR is niet inherent fout; CFR maakt tijdstempels, frametellingen, duurcontroles, zoeken en latere validatie in mijn verwerkingsketen gewoon eenvoudiger.

Het algemene principe is nuttiger dan één specifieke FPS-waarde: betaal geen bandbreedte voor tijdsinformatie die de bron niet bevat.

CRF 28 is een keuze voor mijn materiaal, geen magisch getal

Ik wilde niet elke video naar dezelfde doelbitsnelheid dwingen. Een bijna statische illustratie en een scène met complexe beweging hebben niet hetzelfde aantal bits nodig om er acceptabel uit te zien.

Daarom gebruik ik de CRF-modus van x264 en kwam ik voor dit geïllustreerde, bandbreedtegerichte materiaal uit rond -crf 28. FFmpeg documenteert CRF in libx264 als kwaliteitsgestuurde bitsnelheidsregeling; zie de FFmpeg-codecdocumentatie.

CRF 28 is bewust agressief. Ik zou die waarde niet klakkeloos kopiëren voor filmkorrel, rumoerige camerabeelden, piepkleine tekst op het scherm of een toepassing waarin beeldgetrouwheid belangrijker is dan bandbreedte.

Ik heb ook geen universele perceptuele maatstaf die bewijst dat CRF 28 transparant is. Wat ik over mijn eigen materiaal kan zeggen is beperkter: de bestanden werden veel kleiner en zagen er bij normaal afspelen voor mij nog steeds goed uit. Dat is een praktische waarneming, geen claim dat CRF 28 visueel verliesvrij is.

Veryslow is duur voor de codeerder, niet automatisch voor de decodeerder

Mijn voorinstelling is -preset veryslow. Een langzamere voorinstelling geeft x264 meer gelegenheid om efficiënte voorspellings- en codeerbeslissingen te zoeken. De prijs is CPU en tijd aan de kant van de codeerder.

Het belangrijke onderscheid is dat inspanning van de codeerder en de complexiteit van de decodeerder niet hetzelfde zijn.

Ik kan x264 heel hard laten werken en tegelijk de uiteindelijke bitstroom apart begrenzen. Mijn conservatieve uitvoervereisten zijn:

H.264, profiel Main
Level 3.1
8-bits yuv420p
avc1
refs = 4
B-beelden = 5
B-pyramid = normal
open GOP = uitgeschakeld

FFmpeg stelt CRF, voorinstellingen, fijnafstelling, profielbeperkingen, referentiebeelden en B-beelden afzonderlijk beschikbaar. Zo bekijk ik ze ook: de codeerder mag grondig zoeken, maar de afspeelkant moet gewoon blijven.

GOP en VBV zijn vangrails, niet de belangrijkste kwaliteitsregeling

Voor deze korte progressieve video’s 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 keuze voor mijn toepassing, geen universele regel. Adaptieve videolevering heeft andere beperkingen; Apples HLS-richtlijnen adviseren bijvoorbeeld om de twee seconden een IDR. Ik neem die HLS-regel niet zomaar over voor korte statische progressieve MP4-bestanden.

Ik gebruik daarnaast ongeveer:

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

Die waarden vormen een plafond voor ongebruikelijke pieken in de bitsnelheid. Ze betekenen niet “codeer alles op 4 Mbps”. CRF blijft verantwoordelijk voor de normale bitverdeling, zodat eenvoudige video’s nog steeds heel klein kunnen worden.

Ook de MP4-container hield ik bewust eenvoudig

Ik gebruik expliciet avc1. Apples huidige HLS-documentatie beveelt bemonsteringsformaten zoals avc1 aan in plaats van avc3. Dat is niet de reden dat mijn bestanden kleiner werden, maar het past bij het doel om conventionele H.264 in MP4-uitvoer te maken.

Ik gebruik ook -movflags +faststart. De FFmpeg-formaatdocumentatie zegt dat faststart de MP4-index moov naar het begin van het bestand verplaatst. De HTTP-overdrachtsvereisten van Android zeggen eveneens dat bij MPEG-4 moov vóór mdat en na ftyp moet staan.

ftyp
moov
mdat

Faststart verbetert de compressie niet. Het maakt progressief afspelen via HTTP alleen minder omslachtig.

Voor gewone SDR-uitvoer gebruik ik 8-bit yuv420p en signaleer ik BT.709 met beperkt videobereik. Als een video geen geluid heeft, maak ik geen kunstmatig geluidsspoor. Voor dit geïllustreerde materiaal gebruik ik ook -tune animation; dat zie ik als een inhoudsspecifieke keuze, niet als onderdeel van de universele compatibiliteitseisen.

Het kernprofiel voor FFmpeg

Voor een geïllustreerde bron van 30 fps ziet het centrale deel van de opdracht 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

De schaal- en beeldsnelheidstappen zijn hier bewust niet vastgezet. Een bron van 900×600 moet niet worden vergroot alleen omdat het plafond 1280×720 is, en een animatie met van nature een lage beeldsnelheid moet niet naar 30 fps worden gedwongen alleen omdat het voorbeeld -g 150 gebruikt.

De opdracht is een implementatie van het beleid, niet het beleid zelf.

Waarom de bestanden meerdere keren kleiner werden

Er was geen wonderoptie.

De verkleining kwam door meerdere beslissingen te combineren die elk een ander soort verspilling wegnamen: onnodige pixels, onnodige beelden, een te behoudend kwaliteitsdoel, instellingen van de codeerder die snelheid boven efficiëntie zetten, te frequente sleutelbeelden en gegevensstromen die ik niet nodig had.

Daarom zegt “dit bestand is H.264” verrassend weinig over de bestandsgrootte. Twee H.264-coderingen van dezelfde bron kunnen sterk verschillen omdat de codecnaam niets zegt over resolutie, beeldsnelheid, bitsnelheidsregeling, voorinstelling, GOP-structuur, profiel of voorbereiding van de bron.

In mijn geval waren die beslissingen rond de codec belangrijker dan een codecwissel.

Wat het resultaat niet bewijst

Ik heb niet elke instelling in een gecontroleerde proef geïsoleerd, dus ik kan niet eerlijk een precies percentage van de besparing afzonderlijk toeschrijven aan veryslow, CRF 28, resolutieverlaging of beeldsnelheidverlaging.

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

En ik beweer niet dat één H.264-bestand voor elke website de juiste architectuur is. Meerdere versies, adaptieve videodistributie, HDR, 4K en codecselectie veranderen de afwegingen.

Wat ik daadwerkelijk kan zeggen is beperkter: voor een bandbreedtegerichte bibliotheek met korte geïllustreerde en geanimeerde video’s, een overwegend mobiel publiek, goedkope codeertijd en een sterke behoefte aan voorspelbare weergave maakte dit profiel mijn bestanden meerdere keren kleiner terwijl ze bij normaal afspelen nog steeds goed uitzagen.

Hoe dit resultaat mijn optimalisatiestrategie veranderde

Vroeger zag ik video-optimalisatie vooral als een probleem van instellingen van de codeerder. Nu zie ik het als een probleem van kosten over de hele levensduur.

De codeerder 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 stuur veel minder graag bij elk toekomstig verzoek pixels die door opschalen zijn ontstaan, beelden zonder extra nuttige beweging of bitsnelheid die de inhoud niet nodig heeft.

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

Voor deze toepassing is de les duidelijker dan welke afzonderlijke FFmpeg-optie ook: optimaliseer de kosten die je steeds opnieuw betaalt, niet de kosten die je één keer betaalt.