Non ho costruito questo processo per sperimentare con i codec. L’ho costruito perché le immagini animate erano diventate un modo costoso di distribuire qualcosa che, in pratica, si comportava già come un breve video senza audio.
Il mio carico di lavoro è composto soprattutto da WebP animato, GIF e APNG brevi: di solito pochi secondi, spesso poche decine di fotogrammi visibili e molta ridondanza temporale. La maggior parte delle visualizzazioni arriva da dispositivi mobili, gli stessi file possono essere richiesti molte volte e il costo di codifica pagato una volta conta molto meno dei byte trasferiti a ogni riproduzione.
La parte difficile non è chiamare FFmpeg. Un’immagine animata non è necessariamente una sequenza ordinata di immagini complete a frequenza regolare. Può contenere rettangoli parziali, regole di composizione e smaltimento, canale alfa, ritardi irregolari, fotogrammi a durata zero, orientamenti diversi e metadati temporali che uno strumento generico può riassumere male.
Per questo tratto la conversione come un insieme di invarianti, non come un singolo comando:
WebP animato / GIF / APNG
↓
ricostruire gli stati completi realmente visualizzati
↓
recuperare e normalizzare la temporizzazione originale
↓
analizzare tutte le sorgenti della sequenza finale
↓
scegliere un solo CFR per l’MP4 finale
↓
calcolare l’area comune minima senza ingrandire
↓
codificare segmenti H.264 compatibili
↓
verificare il contratto del flusso
↓
concatenare tramite copia del flusso
↓
normalizzare e verificare la linea temporale dei pacchetti
↓
verificare la distribuzione HTTP
↓
pubblicare in modo atomico
Il codec conta, ma conta ancora di più preservare esattamente ciò che l’animazione mostrava.
Un risultato misurato in produzione: 217 WebP animati sono diventati un MP4 da 78,49 MB
L’ingresso non era un singolo video da 1,49 GB. Erano 217 file WebP animati separati con 10.633 fotogrammi visibili. Insieme, le animazioni sorgente occupavano circa 1,49 GB.
INGRESSO
217 file WebP animati
1,49 GB totali
10.633 fotogrammi visibili
USCITA
1 MP4 H.264
78,49 MB
0,98 Mbps
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow
Il file H.264 risultante pesava 78,49 MB con un bitrate medio di circa 0,98 Mbps. Rispetto ai byte complessivi delle animazioni sorgente, significa circa 19 volte meno, cioè circa 94,7% di dati in meno.
È un risultato reale dell’intero processo, non un test A/B pulito tra “vecchio H.264” e “nuovo H.264”. La rappresentazione è cambiata da centinaia di immagini animate a un unico video compresso temporalmente; quindi non attribuisco l’intero 19× a CRF 28, veryslow o a una singola opzione del codificatore.
Per prima cosa ricostruisco le immagini che lo spettatore vede davvero
La scorciatoia più pericolosa è assumere che ogni fotogramma memorizzato sia un’immagine completa che sostituisce la precedente.
Un fotogramma di un WebP animato può descrivere un rettangolo posizionato con regole di composizione e smaltimento. APNG ha offset, dimensioni, durata e operazioni di composizione e smaltimento. GIF può mantenere l’area di composizione precedente, cancellare una regione o ripristinare uno stato precedente.
Un fotogramma memorizzato può quindi essere solo una piccola porzione il cui significato dipende dal area di composizione costruito dai fotogrammi precedenti. Codificare queste porzioni come immagini complete produce un’animazione sbagliata, non una copia più piccola di quella corretta.
Il mio confine di estrazione è lo stato completo del area di composizione visualizzato: i pixel già composti che un visualizzatore corretto mostrerebbe dopo lo smaltimento del fotogramma precedente e la composizione di quello corrente.
Questa è la prima garanzia di correttezza dell’intero processo. Una volta che una porzione ricostruita male è stata appiattita in H.264, nessun CRF, preimpostazione o opzione del contenitore può ripararla.
La durata dei fotogrammi è un dato della sorgente, non un FPS da indovinare
I formati di animazione memorizzano il tempo in modi diversi. WebP animato usa durate per fotogramma in unità da 1 ms. GIF memorizza ritardi in centesimi di secondo. APNG usa un numeratore e un denominatore per ogni ritardo; se il denominatore è zero, la specifica PNG indica di trattarlo come 100.
Questi ritardi costituiscono la linea temporale. Un valore FPS riportato da uno strumento generico è solo un riassunto e può essere fuorviante.
Un WebP reale nella mia processo era 1264×720 con 49 fotogrammi visibili. I ritardi alternavano 62 e 63 ms, per una durata totale di 3,063 secondi. In pratica è un ritmo di 16 fps, perché un fotogramma a 16 fps dura 62,5 ms.
Uno strumento generico riportava però 25 fps per quella sorgente. Fidarmi di quel numero avrebbe modificato la temporizzazione originale o creato ripetizioni inutili.
Serve anche una regola esplicita per durate non valide o ambigue. WebP lascia all’implementazione l’interpretazione di una durata pari a zero e spesso di valori molto piccoli. GIF può contenere ritardi nulli. APNG consente un numeratore zero, cioè il fotogramma successivo va mostrato il prima possibile, pur permettendo al visualizzatore di imporre un minimo pratico.
La mia politica conserva la precisione al millisecondo, applica un piccolo minimo di 10 ms alle durate nulle o chiaramente troppo piccole e usa 100 ms solo come valore di riserva quando non esiste davvero un’informazione temporale utile. Sono scelte di produzione, non standard universali.
Scelgo un solo CFR per l’intero MP4 finale invece di imporre 30 fps
Dopo aver ricostruito gli stati visibili e le loro durate, porto la linea temporale della sorgente su una linea temporale video. Non codifico tutto alla cieca a 30 fps.
Per tutte le sorgenti destinate allo stesso MP4 finale valuto questo piccolo insieme:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
Il selettore sceglie il CFR più basso che rappresenta adeguatamente l’intera sequenza finale. MP4 finali diversi possono avere frequenze diverse, ma tutti i segmenti codificati separatamente dentro lo stesso MP4 usano esattamente lo stesso CFR.
L’esempio di 3,063 secondi mostra bene il risparmio: a 16 fps servono circa 49 fotogrammi in uscita; a 30 fps circa 92. Se un’altra sorgente nello stesso MP4 richiede davvero 30 fps, l’intera raccolta usa 30. Non mescolo frequenze diverse nello stesso flusso finale.
Per la traccia video uso una scala temporale di 90.000 Hz, perché ogni CFR ammesso produce una durata intera:
10 fps → 9000 unità
12 fps → 7500 unità
15 fps → 6000 unità
16 fps → 5625 unità
18 fps → 5000 unità
20 fps → 4500 unità
24 fps → 3750 unità
25 fps → 3600 unità
30 fps → 3000 unità
È la scala temporale della traccia video a definire questa griglia esatta. Imposto anche la scala temporale generale del contenitore MP4 a 90.000 per coerenza, ma si tratta di un orologio distinto. Il validatore controlla i pacchetti video sulla griglia intera esatta invece di fidarsi di durate decimali arrotondate.
I limiti di risoluzione sono tetti, non area di composizione obbligatori
La mia area di distribuzione arriva circa a 1280×720 in orizzontale, 720×1280 in verticale e a un massimo di 960 pixel sia in larghezza sia in altezza per materiale con orientamenti misti.
La regola non negoziabile è non ingrandire mai. Una sorgente 900×600 non acquista dettaglio diventando 1280×720; crea solo pixel interpolati che l’codificatore deve descrivere.
La seconda regola è meno ovvia: 960×960 è un limite massimo, non un’area di composizione quadrata obbligatoria.
Prima calcolo per ogni sorgente le dimensioni attive consentendo soltanto la riduzione. Poi costruisco il più piccolo area di composizione comune con dimensioni pari che possa contenere tutti i rettangoli già ridotti.
Per esempio, se la sequenza richiede un’immagine orizzontale 960×540 e una verticale 500×900, l’area comune può essere 960×900, non 960×960. Tutti i segmenti mantengono le stesse dimensioni codificate, quindi la concatenazione tramite copia resta possibile, ma evito di codificare area nera inutile.
Preservo il rapporto d’aspetto e riempio lo spazio libero invece di deformare l’immagine. Nella mia processo lo sfondo è nero. Poiché H.264/yuv420p ordinario non conserva il canale alfa originale, la trasparenza viene composta intenzionalmente su questo sfondo.
Perché H.264 MP4 si adatta bene a questo problema di distribuzione
GIF, APNG e WebP animato non sono formati primitivi. Possono anch’essi evitare di ridisegnare le aree immutate, quindi dire che “il video è sempre più piccolo” sarebbe falso.
H.264 è però progettato per la predizione temporale tra immagini. Brevi cicli illustrati con sfondi statici e piccole regioni in movimento sono un caso favorevole per immagini di riferimento, predizione inter-fotogramma e fotogrammi P e B.
La documentazione Safari attuale di Apple raccomanda MP4 codificato in H.264 per video statico e afferma che i GIF animati possono richiedere fino a dodici volte più banda e circa il doppio dell’energia rispetto a un codec video moderno. Quel 12× è un esempio di Apple, non la mia misurazione.
Il mio 19× misurato è quindi un’osservazione di produzione utile, ma continuo a misurare invece di assumere che ogni WebP animato già piccolo debba perdere contro MP4.
Ogni animazione viene codificata come segmento sotto un unico contratto di flusso
Quando parte l’codificatore, la processo conosce già i fotogrammi visualizzati, il CFR comune, il area di composizione finale e la durata prevista.
La parte centrale del mio comando per segmento è circa questa:
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
Il GOP massimo è di circa cinque secondi e deriva dal CFR scelto: 80 fotogrammi a 16 fps, 120 a 24 fps e 150 a 30 fps.
La protezione -t "$EXPECTED_DURATION" non è decorativa. Nella mia lista di immagini ripeto l’ultima come sentinella affinché venga rispettata la durata dell’ultimo fotogramma reale. Senza un limite esplicito, la sentinella può diventare un fotogramma finale aggiuntivo.
L’ho riprodotto sul caso di 49 fotogrammi e 3,063 secondi. Senza -t ottenevo 50 fotogrammi a 16 fps e 94 a 30 fps. Con -t 3.063 ottenevo i 49 previsti a 16 fps e 92 a 30 fps.
La concatenazione tramite copia è sicura solo dopo controlli rigorosi di compatibilità
Il demultiplexer di concatenazione di FFmpeg si aspetta gli stessi flussi, compresi codec e base temporale, e usa la durata di ogni file per posizionare il successivo. Una durata errata può quindi creare difetti temporali.
Non uso la concatenazione per rendere compatibili file incompatibili. Ogni segmento deve già rispettare il contratto prima di essere accettato:
CFR della raccolta = identico
base temporale del flusso = identica
scala temporale della traccia MP4 = identica
dimensioni dell’area comune / SAR = identiche
profilo / livello / formato pixel = identici
segnalazione colore = identica
avcC / dati aggiuntivi AVC = identici byte per byte
Uso stitchable=1 di x264 perché i segmenti sono codificati indipendentemente, ma non considero questa opzione una prova dell’uguaglianza della configurazione AVC. Prima della concatenazione confronto comunque i byte reali della configurazione.
Quando il contratto è soddisfatto, l’unione finale non richiede una seconda compressione:
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy \
-an -sn -dn \
-movflags +faststart \
final.mp4
-c:v copy evita di decodificare e ricomprimere i segmenti H.264 già pronti.
La convalida riguarda sia il file MP4 sia il modo in cui viene distribuito
Non pubblico un file soltanto perché FFmpeg termina con codice 0.
Il validatore ha rilevato veri errori temporali deterministici: a 16 fps ho visto 5580 unità dove il contratto richiedeva 5625; più tardi un risultato a 24 fps conteneva 3751 invece di 3750 esatti. L’indagine dettagliata sullo scarto di una singola unità è un altro argomento; qui basta ricordare che ripetere la stessa operazione non corregge un errore temporale deterministico.
Per il file multimediale verifico numero di flussi, profilo e livello H.264, formato pixel, dimensioni pianificate esatte, SAR, segnalazione colore, scala temporale della traccia video a 90 kHz, durate dei pacchetti, conteggi di fotogrammi e pacchetti, durata totale, relazioni PTS/DTS, configurazione AVC identica tra segmenti, moov prima di mdat e decodifica completa con gestione rigorosa degli errori.
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 -
Ma un MP4 locale corretto può ancora essere servito male sulla rete. Verifico quindi anche il percorso HTTP: Content-Type previsto, Content-Length corretto, supporto alle richieste per intervalli di byte, una risposta valida 206 Partial Content e un Content-Range corretto.
Quando cambio il contratto del codificatore eseguo inoltre una piccola prova su dispositivi e browser reali, invece di supporre che ffprobe dimostri la compatibilità del dispositivo. Verifico avvio, ricerca, ciclo, passaggio in secondo piano e ripresa, oltre alla riproduzione per intervalli, su un iPhone attuale con Safari, un dispositivo Android economico e i principali browser per computer.
Solo dopo che file e distribuzione rispettano il contratto sostituisco atomicamente la risorsa di produzione.
Dove questa processo perde informazioni intenzionalmente
È una trasformazione per la distribuzione, non un master archivistico. L’alfa viene appiattito. La temporizzazione irregolare viene quantizzata su un solo CFR per l’MP4 finale. Le sorgenti grandi possono essere ridotte. Un WebP animato già con perdita subisce un’altra generazione con perdita. L’audio è volutamente assente.
Non userei esattamente questa processo quando la trasparenza deve restare componibile su sfondi arbitrari, quando la temporizzazione irregolare esatta è parte del significato, quando sto creando una sorgente archivistica o quando l’applicazione possiede già una struttura video adattiva multicodec che risolve la distribuzione in altro modo.
Confronto inoltre i piccoli WebP animati già molto ottimizzati invece di presumere che MP4 debba necessariamente vincere.
La sequenza di produzione che uso oggi
- Rilevare il formato animato e leggere i metadati reali di controllo dei fotogrammi.
- Ricostruire gli stati completi visualizzati rispettando le regole di composizione e smaltimento.
- Recuperare e normalizzare la durata di ogni fotogramma.
- Costruire la linea temporale autorevole della sorgente con precisione al millisecondo.
- Analizzare tutte le sorgenti destinate allo stesso MP4 finale.
- Scegliere un unico CFR comune tra 10/12/15/16/18/20/24/25/30.
- Proiettare gli stati visualizzati su quella linea temporale CFR.
- Calcolare dimensioni attive consentendo solo la riduzione; mai ingrandire.
- Costruire il più piccolo area di composizione comune con dimensioni pari necessario.
- Riempire senza deformare e comporre intenzionalmente l’alfa.
- Codificare ogni sorgente in H.264 Main@3.1 / yuv420p / avc1 sotto lo stesso contratto.
- Limitare ogni segmento alla sua durata prevista.
- Rifiutare ogni segmento la cui configurazione AVC reale o temporizzazione violi il contratto.
- Concatenare i segmenti accettati con
-c:v copy. - Normalizzare e convalidare la linea temporale finale dei pacchetti.
- Decodificare completamente il risultato.
- Verificare intestazioni HTTP, intervalli di byte e risposte parziali.
- Dopo modifiche al profilo di codifica, eseguire una prova su dispositivi e browser.
- Pubblicare atomicamente solo dopo il superamento di tutti i controlli.
La fonte di verità è la linea temporale, non l’estensione del file
Un WebP animato, GIF o APNG è una sequenza temporale di stati completi visualizzati, non semplicemente una cartella di immagini con una certa estensione.
H.264 può sfruttare molto bene la ridondanza temporale, ma non può riparare una composizione errata, una temporizzazione inventata o metadati di segmenti incompatibili. Gran parte dell’ingegneria che ha reso affidabile questa processo avviene prima e dopo x264.
La regola che mi è rimasta è questa: cambiare la rappresentazione solo dopo aver definito con precisione ciò che deve restare invariato.
Documentazione primaria
- Specifica del contenitore WebP di Google — rettangoli, durata, composizione e smaltimento dei fotogrammi.
- Specifica PNG del W3C, terza edizione — temporizzazione APNG, offset, composizione e smaltimento.
- Specifica GIF89a — ritardi e comportamento di smaltimento.
- Documentazione FFmpeg sui formati — requisiti di concat e comportamento MP4.
- Documentazione FFmpeg sui filtri del flusso binario —
setts. - Documentazione ffprobe — ispezione di flussi e pacchetti.
- Apple: distribuzione di contenuti video per Safari — H.264 MP4 per video statico e sostituzione dei GIF animati.
- Formati multimediali supportati da Android — supporto H.264 e requisiti di trasmissione HTTP.