Il risultato sembrava il tipo di miglioramento che normalmente richiede un codec più recente: i miei file video sono diventati diverse volte più piccoli, mentre la riproduzione normale continuava ad apparire corretta e, alle dimensioni di visualizzazione abituali, non vedevo una perdita di qualità evidente.
Eppure non ero passato ad AV1, HEVC o VP9. Stavo ancora usando H.264 dentro MP4.
Era cambiato tutto ciò che circondava il codec. Ho ricostruito la policy di encoding per un workload preciso: clip brevi illustrati e animati, pubblico soprattutto mobile, banda come principale costo ricorrente e quasi nessun vincolo sul tempo richiesto da un encode offline eseguito una sola volta.
Il baseline finale è volutamente conservativo in playback e volutamente costoso in encoding: H.264 Main Profile @ Level 3.1, avc1, yuv420p 8-bit, CRF 28, x264 veryslow, tetto di risoluzione di classe 720p, solo frame rate utili normalmente fino a 30 fps, reference e B-frame limitati e faststart per la distribuzione progressiva MP4.
L’ottimizzazione più importante non era un flag di FFmpeg
Il cambiamento decisivo è stato il modo in cui pensavo al costo.
L’encoding avviene una volta. Il trasferimento del file avviene a ogni visualizzazione.
Nel video real-time, spendere molta più CPU per risparmiare un po’ di bitrate può essere un pessimo compromesso. I miei file vengono invece codificati offline e poi serviti ripetutamente. In questo modello, risparmiare dieci minuti durante l’encode può avere pochissimo valore se l’encode più veloce rende più pesante ogni richiesta futura.
Per questo -preset veryslow ha senso per me. Sono disposto a spendere CPU una volta se x264 può usarla per trovare una rappresentazione più efficiente. Il browser non ripete la ricerca dell’encoder; decodifica soltanto il bitstream finale.
La regola è diventata: spendere calcolo nel passaggio che avviene una volta ed essere avari di byte nel passaggio che si ripete.
Perché sono rimasto su H.264 invece di inseguire il codec più nuovo
Non sto dicendo che H.264 sia il codec con la migliore efficienza di compressione disponibile. Non lo è. I codec più recenti sono molto interessanti quando il sistema di delivery può mantenere più rendition e scegliere quella migliore per ogni client.
Il mio vincolo era diverso: un URL, un file, un codec e il minor numero possibile di sorprese in playback per un pubblico prevalentemente mobile.
Per questo lavoro, H.264 in MP4 rimane un baseline molto sicuro. Apple oggi indica agli sviluppatori web di usare file MP4 codificati H.264 per i video statici in Safari. La documentazione Android attuale include H.264 in MP4 e richiede un decoder Main Profile da Android 6.0 in poi; tra le raccomandazioni di playback include anche 1280×720 a 30 fps come configurazione H.264 HD. Vedi i formati multimediali supportati da Android.
Questo non significa che i dispositivi moderni siano limitati a Main Profile o Level 3.1. Apple, per esempio, in HLS tende a preferire High Profile rispetto a Main o Baseline. Io ho scelto Main@3.1 perché volevo un envelope di decodifica volutamente modesto per un singolo MP4 statico, non perché Apple lo imponga.
Ho smesso di codificare pixel che non servivano
La risoluzione è stata una delle leve più forti. Il mio tetto è diventato circa 1280×720 in landscape, 720×1280 in portrait e circa 960×960 per materiale quadrato o con orientamento misto.
La regola più importante è: non fare upscale solo per raggiungere quel tetto.
Se una sorgente è 900×600, portarla a 1280×720 non recupera dettaglio. Crea soltanto più campioni da descrivere per l’encoder. Una sorgente 1920×1080 può essere ridotta alla classe 720p, mentre una 900×600 può rimanere intorno a 900×600. Il tetto è un massimo, non un target.
Sembra banale, ma eliminare pixel inutili può contare più di molti tweak oscuri dell’encoder.
Ho smesso di pagare per frame che la sorgente non aveva davvero
Il frame rate è un altro moltiplicatore. Se un’animazione contiene circa 16 stati visivi realmente utili al secondo, memorizzarla a 30 o 60 fps non crea automaticamente un movimento migliore. Può soprattutto aggiungere campioni temporali ripetuti o sintetizzati che devono comunque essere codificati.
La mia policy è preservare la cadenza utile della sorgente e normalmente restare a 30 fps o meno. Per questo tipo di materiale, 12, 15, 16, 18, 20, 24, 25 o 30 fps possono essere tutti valori sensati quando descrivono davvero la sorgente.
Preferisco anche un CFR pulito nell’output generato. Il VFR non è intrinsecamente sbagliato; il CFR rende semplicemente più facili timestamp, conteggio frame, controlli di durata, seeking e validazione successiva nel mio pipeline.
Il principio generale è più utile di un singolo valore: non pagare banda per informazione temporale che la sorgente non contiene.
CRF 28 è una scelta per il workload, non un numero magico
Non volevo forzare ogni clip verso lo stesso bitrate target. Un’illustrazione quasi statica e una scena con movimento complesso non richiedono lo stesso numero di bit per apparire accettabili.
Uso quindi la modalità CRF di x264 e, per questo workload illustrato bandwidth-first, mi sono fermato intorno a -crf 28. FFmpeg documenta CRF in libx264 come rate control a qualità costante; vedi la documentazione codec di FFmpeg.
CRF 28 è volutamente aggressivo. Non lo copierei alla cieca su grana cinematografica, video rumoroso da camera, testo minuscolo in uno screen recording o un workload in cui la fedeltà conta più della banda.
Non ho nemmeno uno score percettivo universale che dimostri che CRF 28 sia trasparente. Posso affermare qualcosa di più ristretto: sul mio materiale i file sono diventati molto più piccoli e continuavano a sembrare normali durante la riproduzione ordinaria. È un’osservazione pratica, non una dichiarazione di lossless visivo.
Veryslow è costoso per l’encoder, non automaticamente per il decoder
Il mio preset è -preset veryslow. Un preset più lento offre a x264 più opportunità per cercare decisioni di prediction e coding efficienti. Il prezzo è CPU e tempo dell’encoder.
La distinzione fondamentale è che lo sforzo dell’encoder e la complessità del decoder non sono la stessa cosa.
Posso lasciare x264 lavorare molto e, allo stesso tempo, limitare separatamente lo stream finale. Il mio contratto di output conservativo è:
H.264 Main Profile
Level 3.1
8-bit yuv420p
avc1
refs = 4
B-frames = 5
B-pyramid = normal
open GOP = disabled
FFmpeg espone CRF, preset, tuning, restrizioni di profilo, reference frame e B-frame separatamente. È esattamente così che li considero: lasciare all’encoder una ricerca costosa mantenendo normale il lato playback.
GOP e VBV sono guardrail, non il controllo principale della qualità
Per questi clip progressivi brevi uso un GOP massimo di circa cinque secondi: circa -g 150 a 30 fps, -g 120 a 24 fps o -g 80 a 16 fps.
È una scelta per il mio workload, non una regola universale. L’adaptive streaming ha vincoli diversi; Apple, per esempio, raccomanda IDR ogni due secondi per HLS. Non trasferisco automaticamente quella regola HLS ai miei brevi MP4 statici progressivi.
Uso inoltre circa:
-maxrate:v 4M
-bufsize:v 8M
Questi valori sono tetti contro picchi di bitrate insoliti. Non significano “codificare tutto a 4 Mbps”. CRF continua a controllare l’allocazione normale dei bit, quindi i clip facili restano liberi di diventare molto piccoli.
Ho reso noioso anche il contenitore MP4
Uso esplicitamente avc1. La documentazione HLS attuale di Apple raccomanda sample format come avc1 invece di avc3. Non è questo a ridurre i file, ma è coerente con l’obiettivo di produrre un normale H.264-in-MP4.
Uso anche -movflags +faststart. La documentazione dei formati FFmpeg spiega che faststart sposta l’indice MP4 moov all’inizio del file. I requisiti Android per HTTP streaming indicano inoltre che, per MPEG-4, moov deve precedere mdat dopo ftyp.
ftyp
moov
mdat
Faststart non migliora la compressione. Rende meno problematica la riproduzione progressiva via HTTP.
Per SDR normale uso yuv420p 8-bit e segnalo BT.709 con limited/video range. Se un clip non richiede audio, non creo una traccia audio. Per i contenuti illustrati uso anche -tune animation; lo considero una scelta specifica del contenuto, non parte del contratto universale di compatibilità.
Il profilo FFmpeg centrale
Per una sorgente illustrata a 30 fps, il nucleo del comando è circa questo:
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 e frame rate non sono volutamente hardcoded. Una sorgente 900×600 non deve essere ingrandita solo perché il tetto è 1280×720, e un’animazione naturalmente a basso frame rate non deve essere forzata a 30 fps solo perché l’esempio usa -g 150.
Il comando implementa la policy; non è la policy stessa.
Perché i file sono diventati diverse volte più piccoli
Non c’è stato un flag miracoloso.
La riduzione è arrivata accumulando decisioni che eliminavano sprechi diversi: pixel inutili, frame inutili, ragionamento a bitrate fisso, impostazioni encoder troppo economiche, keyframe troppo frequenti e stream di cui non avevo bisogno.
Per questo dire “questo file è H.264” spiega sorprendentemente poco della sua dimensione. Due encode H.264 della stessa sorgente possono essere molto diversi perché il nome del codec non descrive risoluzione, FPS, rate control, preset, struttura GOP, profilo o preparazione della sorgente.
Nel mio caso, cambiare queste decisioni intorno al codec ha contato più del cambiare codec.
Cosa non dimostra questo risultato
Non ho isolato ogni impostazione in un esperimento controllato, quindi non posso attribuire onestamente una percentuale esatta del risparmio a veryslow, CRF 28, riduzione della risoluzione o riduzione del frame rate singolarmente.
Non posso nemmeno sostenere che ogni output CRF 28 sia percettivamente trasparente. “Nessuna perdita di qualità evidente” è la mia osservazione per questo workload illustrato a normali dimensioni di visualizzazione, non una garanzia scientifica per qualsiasi video.
E non sto dicendo che un unico file H.264 sia l’architettura giusta per ogni sito. Multiple rendition, adaptive streaming, HDR, 4K e codec negotiation cambiano i compromessi.
Il risultato è più ristretto e più utile: per una libreria bandwidth-first e mobile-heavy di brevi clip illustrati e animati, dove il tempo di encoding costa poco e il playback prevedibile conta, questo profilo ha reso i miei file diverse volte più piccoli mantenendo un aspetto normale nella riproduzione ordinaria.
La regola che uso adesso
Prima pensavo all’ottimizzazione video soprattutto come a un problema di impostazioni dell’encoder. Ora la considero un problema di costo lungo tutta la vita del file.
L’encoder può eseguire una sola volta. I byte possono attraversare la rete migliaia o milioni di volte.
Questo cambia il significato di “costoso”.
Sono felice di spendere CPU una volta. Sono molto meno disposto a inviare a ogni futura richiesta pixel creati dall’upscale, frame che non aggiungono movimento utile o bitrate di cui il contenuto non ha bisogno.
Il codec è rimasto noioso: H.264 in MP4. L’ottimizzazione è avvenuta intorno.
Per il mio workload, la lezione è più utile di qualsiasi singolo flag FFmpeg: ottimizzare il costo che paghi ripetutamente, non quello che paghi una sola volta.