Ik heb dit proces niet gebouwd om met codecs te experimenteren. Ik bouwde het omdat geanimeerde afbeeldingen een dure manier waren geworden om iets te leveren dat zich in de praktijk al gedroeg als korte video zonder geluid.
Mijn materiaal bestaat vooral uit korte geanimeerde WebP-, GIF- en APNG-bestanden: meestal een paar seconden, vaak slechts enkele tientallen zichtbare beelden en veel temporele redundantie. Het meeste verkeer komt van mobiele apparaten, dezelfde bestanden worden herhaaldelijk opgevraagd en de eenmalige rekentijd voor codering telt veel minder dan de bytes die daarna bij elke weergave worden verstuurd.
De moeilijke stap is niet FFmpeg starten. Een geanimeerde afbeelding hoeft geen nette reeks volledige beelden met één regelmatige beeldsnelheid te zijn. Ze kan deelrechthoeken, meng- en verwijderregels, alfa, onregelmatige vertragingen, beelden met duur nul, verschillende oriëntaties en tijdmetagegevens bevatten die een algemeen hulpprogramma misleidend kan samenvatten.
Daarom behandel ik de omzetting als een verzameling invarianten, niet als één commando:
geanimeerde WebP / GIF / APNG
↓
volledige zichtbare beeldtoestanden reconstrueren
↓
brontijdsturing herstellen en normaliseren
↓
alle bronnen in de uiteindelijke reeks analyseren
↓
één CFR kiezen voor de uiteindelijke MP4
↓
kleinste gemeenschappelijke beeldvlak berekenen zonder op te schalen
↓
compatibele H.264-segmenten coderen
↓
gegevensstroomcontract controleren
↓
samenvoegen via gegevensstroomkopie
↓
pakkettijdlijn normaliseren en controleren
↓
HTTP-levering controleren
↓
atomair publiceren
De codec is belangrijk, maar nog belangrijker is dat precies behouden blijft wat de animatie werkelijk liet zien.
Een gemeten productieresultaat: 217 geanimeerde WebP-bestanden werden één MP4 van 78,49 MB
De invoer was geen enkele video van 1,49 GB. Het waren 217 afzonderlijke geanimeerde WebP-bestanden met 10.633 zichtbare beelden. Samen namen de bronanimaties ongeveer 1,49 GB in.
INVOER
217 geanimeerde WebP-bestanden
1,49 GB totaal
10.633 zichtbare beelden
UITVOER
1 H.264 MP4
78,49 MB
0,98 Mbit/s
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow
Het resulterende H.264-bestand was 78,49 MB groot bij ongeveer 0,98 Mbit/s. Vergeleken met de gezamenlijke bronbestanden is dat ongeveer 19 keer kleiner, oftewel circa 94,7% minder gegevens.
Dat is een echt resultaat van het volledige proces, geen zuivere A/B-test van “oud H.264 tegenover nieuw H.264”. De representatie veranderde van honderden geanimeerde afbeeldingsbestanden naar één temporeel gecomprimeerde video. Daarom schrijf ik de volledige factor 19 niet toe aan CRF 28, veryslow of één codeerderoptie.
Eerst reconstrueer ik de beelden die de kijker werkelijk ziet
De gevaarlijkste vereenvoudiging is aannemen dat elk opgeslagen animatiebeeld een volledige afbeelding is die de vorige volledig vervangt.
Een beeld uit een geanimeerde WebP kan een gepositioneerde rechthoek plus meng- en verwijdergedrag beschrijven. APNG heeft verschuivingen, afmetingen, duur en meng- en verwijderbewerkingen. GIF kan het vorige beeldvlak laten staan, een gebied wissen of een eerdere toestand herstellen.
Een opgeslagen beeld kan dus slechts een kleine patch zijn waarvan de betekenis afhangt van het beeldvlak dat door eerdere beelden is opgebouwd. Zulke patches als volledige afbeeldingen coderen levert niet dezelfde animatie in kleiner formaat op, maar een verkeerde animatie.
Mijn extractiegrens is de volledig weergegeven beeldvlaktoestand: de samengestelde pixels die een correcte weergaveprogramma zou tonen na toepassing van de verwijderregel van het vorige beeld en de mengregel van het huidige beeld.
Dit is de eerste correctheidsgarantie van het hele proces. Zodra een verkeerd gereconstrueerde patch in H.264 is vastgelegd, kan geen CRF, voorinstelling of containeroptie hem later repareren.
De duur van een beeld is brondata, geen FPS-waarde om te raden
Animatieformaten slaan tijd verschillend op. Geanimeerde WebP gebruikt een duur per beeld in eenheden van 1 ms. GIF bewaart vertragingen in honderdsten van een seconde. APNG gebruikt voor elke vertraging een teller en noemer; als de noemer nul is, schrijft de PNG-specificatie voor dat die als 100 wordt behandeld.
Die vertragingen vormen de tijdlijn. Een FPS-waarde die een algemeen hulpmiddel toont is slechts een samenvatting en kan misleidend zijn.
Een echte WebP in mijn proces was 1264×720 met 49 zichtbare beelden. De vertragingen wisselden tussen 62 en 63 ms en de totale duur was 3,063 seconden. Dat komt praktisch neer op 16 fps, omdat één beeld bij 16 fps 62,5 ms duurt.
Een algemeen hulpprogramma rapporteerde voor dezelfde bron 25 fps. Als ik dat getal had gevolgd, had ik de oorspronkelijke tijdsturing veranderd of onnodige herhalingsbeelden gemaakt.
Ik heb ook een expliciete regel nodig voor ongeldige of dubbelzinnige vertragingen. WebP laat de interpretatie van duur nul en vaak zeer kleine duurwaarden aan de implementatie over. GIF kan nulvertraging bevatten. APNG staat een teller van nul toe, wat betekent dat het volgende beeld zo snel mogelijk moet worden weergegeven, terwijl een weergaveprogramma nog steeds een praktisch minimum mag toepassen.
Mijn normalisatie bewaart millisecondenprecisie, gebruikt een klein minimum van 10 ms voor nul of duidelijk onbruikbaar korte duren en valt alleen terug op 100 ms wanneer bruikbare tijdsturing echt ontbreekt. Dat zijn productieregels, geen universele standaarden.
Ik kies één CFR voor de hele uiteindelijke MP4 in plaats van standaard 30 fps
Nadat de zichtbare toestanden en hun duren zijn hersteld, projecteer ik de brontijdlijn op een videotijdlijn. Ik codeer niet alles blind op 30 fps.
Voor alle bronnen die in dezelfde uiteindelijke MP4 terechtkomen beoordeel ik deze kleine verzameling:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
De selectie kiest de laagste CFR die de hele uiteindelijke reeks voldoende goed weergeeft. Verschillende MP4-bestanden mogen verschillende snelheden gebruiken, maar alle afzonderlijk gecodeerde segmenten binnen één MP4 gebruiken exact dezelfde gekozen CFR.
Het voorbeeld van 3,063 seconden maakt de besparing duidelijk: bij 16 fps zijn ongeveer 49 uitvoerbeelden nodig, bij 30 fps ongeveer 92. Als een andere bron in dezelfde MP4 echt 30 fps nodig heeft, gebruikt de hele verzameling 30. Ik meng geen verschillende beeldsnelheden in één uiteindelijke gegevensstroom.
Voor de videotrack gebruik ik een tijdschaal van 90.000 Hz, omdat elke toegestane CFR een gehele beeldduur oplevert:
10 fps → 9000 tikken
12 fps → 7500 tikken
15 fps → 6000 tikken
16 fps → 5625 tikken
18 fps → 5000 tikken
20 fps → 4500 tikken
24 fps → 3750 tikken
25 fps → 3600 tikken
30 fps → 3000 tikken
De tijdschaal van de videotrack bepaalt dit exacte raster. Voor consistentie stel ik de algemene MP4-tijdschaal ook op 90.000 in, maar dat is een afzonderlijke containerklok. De validator controleert videopakketten tegen het exacte gehele raster en vertrouwt niet op afgeronde decimale duren.
Resolutielimieten zijn plafonds, geen verplichte beeldvlaksen
Mijn leveringskader is ongeveer maximaal 1280×720 voor liggend materiaal, 720×1280 voor staand materiaal en maximaal 960 pixels breed én hoog voor gemengde oriëntaties.
De ononderhandelbare regel is nooit opschalen. Een bron van 900×600 krijgt geen extra detail door hem 1280×720 te maken; er ontstaan alleen geïnterpoleerde pixels die de codeerder moet beschrijven.
De tweede regel is minder vanzelfsprekend: 960×960 is een maximale begrenzing, geen verplicht vierkant beeldvlak.
Eerst bereken ik voor elke bron actieve afmetingen waarbij alleen verkleinen is toegestaan. Daarna krijgt de uiteindelijke reeks het kleinste gemeenschappelijke beeldvlak met even afmetingen dat alle reeds verkleinde actieve rechthoeken kan bevatten.
Als de reeks bijvoorbeeld een liggend beeld van 960×540 en een staand beeld van 500×900 nodig heeft, kan het gemeenschappelijke beeldvlak 960×900 zijn in plaats van 960×960. Alle segmenten houden dezelfde gecodeerde afmetingen, zodat samenvoegen via gegevensstroomkopie mogelijk blijft zonder nutteloze zwarte ruimte te coderen.
De beeldverhouding blijft behouden en ongebruikte ruimte wordt opgevuld in plaats van het beeld uit te rekken. In mijn proces is de achtergrond zwart. Omdat normaal H.264/yuv420p het oorspronkelijke alfakanaal niet behoudt, wordt transparantie bewust tegen die achtergrond samengesteld.
Waarom H.264 MP4 goed past bij dit leveringsprobleem
GIF, APNG en geanimeerde WebP zijn geen primitieve formaten. Ook zij kunnen vermijden dat onveranderde gebieden volledig opnieuw worden getekend, dus “video is altijd kleiner” zou onjuist zijn.
H.264 is echter ontworpen voor temporele voorspelling tussen beelden. Korte geïllustreerde lussen met een statische achtergrond en kleine bewegende gebieden zijn gunstig voor referentiebeelden, voorspelling tussen beelden en P- en B-beelden.
De huidige Safari-documentatie van Apple beveelt H.264-gecodeerde MP4 aan voor statische video en vermeldt dat geanimeerde GIF’s tot twaalf keer zoveel bandbreedte en ongeveer tweemaal zoveel energie kunnen kosten als een moderne videocodec. Die factor 12 is een voorbeeld van Apple, niet mijn meting.
Mijn gemeten factor 19 is dus een nuttige productieobservatie, maar ik meet nog steeds per geval in plaats van aan te nemen dat elk reeds klein geanimeerde WebP-bestand van MP4 verliest.
Elke animatie wordt als segment onder één gegevensstroomcontract gecodeerd
Wanneer de codeerder start, kent het proces de zichtbare beelden, de gemeenschappelijke CFR, het uiteindelijke beeldvlak en de verwachte duur al.
Het centrale deel van mijn segmentcommando ziet er ongeveer zo uit:
ffmpeg -framerate "$COLLECTION_FPS" -i frame-%06d.png \
-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 "$GOP_FRAMES" \
-maxrate:v 4M \
-bufsize:v 8M \
-x264-params "stitchable=1:open-gop=0:b-pyramid=normal:nal-hrd=none" \
-color_range tv \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-fps_mode passthrough \
-map_metadata -1 \
-map_chapters -1 \
-an -sn -dn \
-video_track_timescale 90000 \
-movie_timescale 90000 \
-t "$EXPECTED_DURATION" \
segment.mp4
De maximale GOP is ongeveer vijf seconden en wordt uit de gekozen CFR afgeleid: 80 beelden bij 16 fps, 120 bij 24 fps en 150 bij 30 fps.
De begrenzing -t "$EXPECTED_DURATION" is niet decoratief. In mijn beeldenlijst herhaal ik het laatste beeld als afsluitmarkering zodat de duur van het vorige echte beeld wordt toegepast. Zonder expliciete tijdslimiet kan die markering als extra eindbeeld worden opgenomen.
Ik heb dit gereproduceerd met het geval van 49 beelden en 3,063 seconden. Zonder -t kreeg ik 50 beelden bij 16 fps en 94 bij 30 fps. Met -t 3.063 werden het de bedoelde 49 bij 16 fps en 92 bij 30 fps.
Samenvoegen via gegevensstroomkopie is alleen veilig na strenge compatibiliteitscontrole
De concat-demultiplexer van FFmpeg verwacht dezelfde gegevensstromen, waaronder codec en tijdbasis, en gebruikt de duur van elk bestand om het volgende te positioneren. Onjuiste duurmetagegevens kunnen dus tijdlijnfouten veroorzaken.
Ik gebruik samenvoegen niet om incompatibele bestanden compatibel te maken. Een segment moet vóór acceptatie al aan dit contract voldoen:
CFR van de verzameling = identiek
tijdbasis van de gegevensstroom = identiek
MP4-tijdschaal van de videospoor = identiek
afmetingen van het beeldvlak / SAR = identiek
profiel / niveau / pixelformaat = identiek
kleursignalering = identiek
avcC / AVC-extra gegevens = byte voor byte identiek
Ik gebruik stitchable=1 van x264 omdat de segmenten onafhankelijk worden gecodeerd, maar ik behandel die optie niet als bewijs dat de AVC-configuratie gelijk is. Voor het samenvoegen vergelijk ik nog steeds de werkelijke configuratiebytes.
Wanneer het contract klopt, hoeft de uiteindelijke verbinding geen tweede compressieronde te krijgen:
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy \
-an -sn -dn \
-movflags +faststart \
final.mp4
-c:v copy voorkomt dat de reeds gecodeerde H.264-segmenten opnieuw worden gedecodeerd en gecomprimeerd.
Validatie omvat zowel het MP4-bestand als de manier waarop het wordt geleverd
Ik publiceer een bestand niet alleen omdat FFmpeg met code 0 eindigt.
De validator heeft echte deterministische tijdsturingfouten gevonden: bij 16 fps zag ik 5580 tikken waar het contract 5625 vereiste; later bevatte een 24-fps-resultaat 3751 in plaats van exact 3750. De diepere analyse van dat verschil van één tik is een apart onderwerp; hier is de les dat dezelfde bewerking opnieuw uitvoeren een deterministische tijdsturingfout niet oplost.
Voor het mediabestand controleer ik het verwachte aantal gegevensstromen, H.264-profiel en -niveau, pixelformaat, exact geplande afmetingen, SAR, kleursignalering, de 90-kHz-tijdschaal van het videospoor, pakketduren, aantallen beelden en pakketten, totale duur, PTS/DTS-relaties, identieke AVC-configuratie tussen segmenten, moov vóór mdat en volledige decodering met strenge foutafhandeling.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json \
final.mp4
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
Maar een lokaal correct MP4-bestand kan via het netwerk nog steeds verkeerd worden geleverd. Daarom controleer ik ook het HTTP-pad: de verwachte Content-Type, correcte Content-Length, ondersteuning voor bytebereiken, een geldige 206 Partial Content-reactie en een correcte Content-Range.
Wanneer ik het codeerdercontract wijzig, voer ik bovendien een kleine proef uit op echte apparaten en browsers in plaats van te veronderstellen dat ffprobe hardwarecompatibiliteit bewijst. Ik test starten, zoeken, herhalen, achtergrond/hervatten en bereikweergave op een recente iPhone met Safari, een bescheiden Android-apparaat en gangbare desktopbrowsers.
Pas wanneer zowel bestand als levering aan het contract voldoen, vervang ik het productieobject atomair.
Waar dit proces bewust informatie verliest
Dit is een transformatie voor levering, geen archiefmaster. Alfa wordt tegen een achtergrond samengesteld. Onregelmatige brontijdsturing wordt naar één CFR voor de uiteindelijke MP4 gekwantiseerd. Grote bronnen mogen worden verkleind. Een geanimeerde WebP dat al met verlies is gecomprimeerd krijgt nog een verliesgevende generatie. Audio ontbreekt bewust.
Ik zou precies dit proces niet gebruiken wanneer transparantie boven willekeurige achtergronden behouden moet blijven, wanneer de exacte onregelmatige tijdsturing per beeld inhoudelijk betekenisvol is, wanneer ik een archiefbron maak of wanneer de toepassing al een adaptieve videostapel met meerdere codecs heeft die het leveringsprobleem anders oplost.
Zeer kleine, al sterk geoptimaliseerde geanimeerde WebP-bestanden meet ik eveneens eerst in plaats van aan te nemen dat MP4 noodzakelijk wint.
De productiereeks die ik nu gebruik
- Het animatieformaat herkennen en de echte metagegevens voor beeldbesturing lezen.
- Volledige zichtbare beeldvlaktoestanden reconstrueren volgens de meng- en verwijderregels.
- De duur van elk beeld herstellen en normaliseren.
- De gezaghebbende brontijdlijn met millisecondenprecisie opbouwen.
- Alle bronnen analyseren die in dezelfde uiteindelijke MP4 komen.
- Één gemeenschappelijke CFR kiezen uit 10/12/15/16/18/20/24/25/30.
- De zichtbare toestanden op die CFR-tijdlijn projecteren.
- Actieve afmetingen berekenen met alleen verkleinen; nooit opschalen.
- Het kleinste noodzakelijke gemeenschappelijke beeldvlak met even afmetingen bouwen.
- Opvullen zonder uitrekken en alfa bewust samenstellen.
- Elke bron coderen als H.264 Main@3.1 / yuv420p / avc1 onder hetzelfde gegevensstroomcontract.
- Elk segment begrenzen tot de verwachte duur.
- Elk segment afwijzen waarvan de werkelijke AVC-configuratie of tijdsturing het contract schendt.
- Geaccepteerde segmenten samenvoegen met
-c:v copy. - De uiteindelijke pakket-tijdlijn normaliseren en valideren.
- Het resultaat volledig decoderen.
- HTTP-koppen, bytebereiken en gedeeltelijke antwoorden controleren.
- Na wijzigingen aan het codeerderprofiel een apparaat- en browserproef uitvoeren.
- Pas na alle geslaagde controles atomair publiceren.
De tijdlijn is de bron van waarheid, niet de bestandsextensie
Een geanimeerde WebP, GIF of APNG is een getimede reeks volledig zichtbare beeldtoestanden, niet simpelweg een map afbeeldingen met een bepaalde extensie.
H.264 kan temporele redundantie zeer efficiënt benutten, maar kan verkeerde compositie, verzonnen tijdsturing of incompatibele segmentmetagegevens niet herstellen. Een groot deel van de techniek die dit proces betrouwbaar maakt, vindt vóór en na x264 plaats.
De regel die voor mij overblijft: verander de representatie pas nadat exact is vastgelegd wat onveranderd moet blijven.
Primaire documentatie
- WebP-containerspecificatie van Google — beeldrechthoeken, duur, menging en verwijdering.
- PNG-specificatie van W3C, derde editie — APNG-tijdsturing, verschuivingen, menging en verwijdering.
- GIF89a-specificatie — vertragingen en verwijdergedrag.
- FFmpeg-documentatie over formaten — concat-vereisten en MP4-gedrag.
- FFmpeg-documentatie over bitstreamfilters —
setts. - ffprobe-documentatie — inspectie van gegevensstromen en pakketten.
- Apple: videocontent leveren voor Safari — H.264 MP4 voor statische video en vervanging van geanimeerde GIF’s.
- Ondersteunde mediaformaten in Android — H.264-ondersteuning en vereisten voor HTTP-streaming.