Retour au blog
13 août 2026Sergei Solod11 min de lecture

Comment j’ai réduit mes vidéos H.264 de plusieurs fois sans perte de qualité évidente

J’ai conservé H.264, mais j’ai reconstruit le profil d’encodage autour de CRF 28, x264 veryslow, d’une limite de résolution de classe 720p, d’une fréquence d’images utile et d’exigences de décodage volontairement conservatrices. La principale leçon était économique : l’encodage se paie une fois, la bande passante à chaque lecture.

H.264FFmpegCompression vidéoPerformance webx264

Le résultat ressemblait d’abord à ce qu’on attendrait d’un codec plus récent : mes fichiers vidéo sont devenus plusieurs fois plus petits, alors que la lecture normale restait visuellement correcte et que je ne voyais pas de perte de qualité évidente aux tailles d’affichage habituelles.

Pourtant, je n’étais passé ni à AV1, ni à HEVC, ni à VP9. J’utilisais toujours H.264 dans un conteneur MP4.

Ce qui avait changé, c’était tout ce qui entourait le codec. J’ai reconstruit la politique d’encodage pour un workload précis : de courts clips illustrés et animés, une audience très mobile, la bande passante comme coût récurrent majeur et pratiquement aucune contrainte sur la durée d’un encodage offline effectué une seule fois.

Mon baseline est donc devenu volontairement conservateur côté lecture et volontairement coûteux côté encodage : H.264 Main Profile @ Level 3.1, avc1, yuv420p 8 bits, CRF 28, x264 veryslow, plafond de résolution de classe 720p, fréquence d’images utile généralement limitée à 30 fps, références et B-frames bornées, et faststart pour une livraison MP4 progressive.

L’optimisation la plus importante n’était pas une option FFmpeg

Le changement décisif concernait ma manière de penser le coût.

L’encodage arrive une fois. Le transfert du fichier arrive à chaque lecture.

Pour de la vidéo temps réel, dépenser beaucoup plus de CPU afin d’économiser un peu de bitrate peut être un mauvais compromis. Mes fichiers sont encodés offline puis servis encore et encore. Dans ce modèle, gagner dix minutes à l’encodage peut ne presque rien valoir si l’encode plus rapide grossit chaque requête future.

C’est pour cela que -preset veryslow est rationnel dans mon cas. Je suis prêt à dépenser du CPU une fois si x264 peut s’en servir pour trouver une représentation plus efficace. Le navigateur ne répète jamais la recherche coûteuse de l’encodeur ; il ne fait que décoder le bitstream final.

La règle est devenue simple : dépenser du calcul à l’étape unique et économiser agressivement les octets à l’étape répétée.

Pourquoi je suis resté sur H.264 au lieu de courir après le codec le plus récent

Je ne prétends pas que H.264 offre la meilleure compression disponible. Ce n’est pas le cas. Les codecs plus récents sont très intéressants quand l’architecture de distribution peut conserver plusieurs renditions et choisir la meilleure pour chaque client.

Ma contrainte était différente : une URL, un fichier, un codec et le moins de surprises de lecture possible pour une audience très mobile.

Pour ce besoin, H.264 dans MP4 reste un baseline très sûr. Apple recommande actuellement aux développeurs web d’utiliser des fichiers MP4 encodés en H.264 pour la vidéo statique dans Safari. La documentation Android actuelle liste H.264 dans MP4 et exige un décodeur Main Profile depuis Android 6.0 ; ses recommandations de lecture incluent également 1280×720 à 30 fps comme configuration H.264 HD. Voir les formats multimédias pris en charge par Android.

Cela ne signifie pas que les appareils modernes sont limités à Main Profile ou Level 3.1. Apple, par exemple, préfère généralement High Profile à Main ou Baseline pour HLS. J’ai choisi Main@3.1 parce que je voulais une enveloppe de décodage volontairement modeste pour un seul MP4 statique, pas parce qu’Apple l’impose.

J’ai arrêté d’encoder des pixels qui n’avaient pas besoin d’exister

La résolution était l’un des leviers les plus importants. Mon plafond est devenu environ 1280×720 en paysage, 720×1280 en portrait et près de 960×960 pour du contenu carré ou d’orientation mixte.

La règle la plus importante est : ne jamais agrandir une source uniquement pour atteindre ce plafond.

Si une source fait 900×600, la passer à 1280×720 ne récupère aucun détail. Elle crée seulement davantage d’échantillons à décrire par l’encodeur. Une source 1920×1080 peut être réduite vers la classe 720p ; une source 900×600 peut rester proche de 900×600. Le plafond est un maximum, pas une cible.

Cela paraît simple, mais supprimer des pixels inutiles peut économiser davantage que beaucoup de réglages obscurs de l’encodeur.

J’ai arrêté de payer pour des images que la source n’avait pas réellement

La fréquence d’images est un autre multiplicateur. Si une animation contient environ 16 états visuels réellement utiles par seconde, la stocker à 30 ou 60 fps n’améliore pas automatiquement le mouvement. On peut surtout ajouter des échantillons temporels répétés ou synthétisés qui doivent malgré tout être codés.

Ma politique consiste à conserver la cadence utile de la source et à rester normalement à 30 fps ou moins. Pour ce type de contenu, 12, 15, 16, 18, 20, 24, 25 ou 30 fps peuvent tous être raisonnables lorsqu’ils décrivent réellement la source.

Je préfère également un CFR propre dans la sortie générée. Le VFR n’est pas intrinsèquement cassé ; le CFR simplifie simplement les timestamps, le nombre d’images, les contrôles de durée, le seeking et la validation ultérieure dans mon pipeline.

Le principe général compte plus qu’un chiffre précis : ne pas payer de bande passante pour une information temporelle absente de la source.

CRF 28 est un choix de workload, pas un nombre magique

Je ne voulais pas forcer tous les clips vers le même bitrate cible. Une illustration presque statique et une scène avec un mouvement complexe n’ont pas besoin du même nombre de bits pour rester acceptables.

J’utilise donc le mode CRF de x264 et, pour ce workload illustré orienté bande passante, je me suis arrêté autour de -crf 28. FFmpeg documente CRF dans libx264 comme un contrôle de débit à qualité constante ; voir la documentation des codecs FFmpeg.

CRF 28 est volontairement agressif. Je ne copierais pas cette valeur aveuglément sur du grain cinéma, une vidéo caméra bruitée, du texte minuscule dans une capture d’écran ou un workload où la fidélité compte davantage que la bande passante.

Je n’ai pas non plus de score perceptuel universel prouvant que CRF 28 est transparent. Ce que je peux dire de mon contenu est plus limité : les fichiers sont devenus nettement plus petits tout en continuant à paraître normaux en lecture courante. C’est une observation pratique, pas une affirmation de transparence visuelle.

Veryslow coûte cher à l’encodeur, pas automatiquement au décodeur

Mon preset est -preset veryslow. Un preset plus lent donne à x264 davantage de possibilités pour chercher des décisions de prédiction et de codage efficaces. Le prix est le temps et le CPU d’encodage.

La distinction essentielle est que l’effort d’encodage et la complexité de décodage ne sont pas la même chose.

Je peux laisser x264 travailler très longtemps tout en limitant séparément le flux final. Mon contrat de sortie conservateur est :

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

FFmpeg expose séparément CRF, presets, tuning, restrictions de profil, reference frames et B-frames. C’est exactement ainsi que je les traite : laisser l’encodeur chercher de manière coûteuse, tout en gardant le côté lecture banal.

GOP et VBV sont des garde-fous, pas le contrôle principal de qualité

Pour ces courts clips progressifs, j’utilise un GOP maximal d’environ cinq secondes : près de -g 150 à 30 fps, -g 120 à 24 fps ou -g 80 à 16 fps.

C’est un choix de workload, pas une règle universelle. Le streaming adaptatif a d’autres contraintes ; Apple recommande par exemple des IDR toutes les deux secondes pour HLS. Je ne transpose pas automatiquement cette règle HLS à de courts fichiers MP4 statiques et progressifs.

J’utilise aussi environ :

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

Ces valeurs sont des plafonds contre les pics de bitrate inhabituels. Elles ne signifient pas « tout encoder à 4 Mbps ». CRF continue de gérer l’allocation normale des bits, ce qui permet aux clips faciles de devenir très petits.

J’ai aussi rendu le conteneur MP4 ennuyeux

J’utilise explicitement avc1. La documentation HLS actuelle d’Apple recommande des sample formats comme avc1 plutôt que avc3. Ce n’est pas ce qui a réduit mes fichiers, mais cela correspond à l’objectif d’un H.264-in-MP4 conventionnel.

J’utilise également -movflags +faststart. La documentation des formats FFmpeg indique que faststart déplace l’index MP4 moov au début du fichier. Les exigences Android pour le streaming HTTP indiquent également que, dans MPEG-4, moov doit précéder mdat après ftyp.

ftyp
moov
mdat

Faststart n’améliore pas la compression. Il rend la lecture HTTP progressive moins problématique.

Pour du SDR classique, j’utilise yuv420p 8 bits et je signale BT.709 en limited/video range. Si un clip n’a pas besoin d’audio, je ne fabrique pas de piste audio. Pour le contenu illustré, j’utilise aussi -tune animation ; je considère ce choix comme spécifique au type de contenu, pas comme une partie du contrat universel de compatibilité.

Le profil FFmpeg central

Pour une source illustrée à 30 fps, le cœur de la commande ressemble approximativement à ceci :

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

Le scaling et la fréquence d’images ne sont volontairement pas codés en dur ici. Une source 900×600 ne doit pas être agrandie simplement parce que le plafond est 1280×720, et une animation naturellement lente ne doit pas être forcée à 30 fps uniquement parce que l’exemple utilise -g 150.

La commande implémente la politique ; elle n’est pas la politique elle-même.

Pourquoi les fichiers sont devenus plusieurs fois plus petits

Il n’y avait pas de flag miracle.

La réduction est venue de l’addition de plusieurs décisions supprimant chacune une forme de gaspillage : pixels inutiles, frames inutiles, raisonnement en bitrate fixe, réglages d’encodeur trop peu coûteux, keyframes trop fréquentes et streams dont je n’avais pas besoin.

C’est pourquoi « ce fichier est en H.264 » dit étonnamment peu sur sa taille. Deux encodes H.264 de la même source peuvent être très différents parce que le nom du codec ne décrit ni la résolution, ni le FPS, ni le rate control, ni le preset, ni le GOP, ni le profil, ni la préparation de la source.

Dans mon cas, ces décisions autour du codec ont compté davantage que le remplacement du codec lui-même.

Ce que ce résultat ne prouve pas

Je n’ai pas isolé chaque réglage dans une expérience contrôlée, donc je ne peux pas attribuer honnêtement un pourcentage exact de l’économie à veryslow, CRF 28, la réduction de résolution ou la réduction du FPS séparément.

Je ne peux pas non plus affirmer que toute sortie CRF 28 est perceptuellement transparente. « Pas de perte de qualité évidente » est mon observation pour ce workload illustré aux tailles d’affichage normales, pas une garantie scientifique pour n’importe quelle vidéo.

Enfin, je ne soutiens pas qu’un unique fichier H.264 soit la bonne architecture pour tous les sites. Plusieurs renditions, le streaming adaptatif, le HDR, la 4K et la négociation de codec changent les compromis.

Le résultat est plus étroit et plus utile : pour une bibliothèque mobile-heavy de courts clips illustrés et animés où la bande passante prime, où le temps d’encodage coûte peu et où une lecture prévisible compte, ce profil a rendu mes fichiers plusieurs fois plus petits tout en gardant un rendu normal en lecture courante.

La règle que j’utilise maintenant

Avant, je voyais surtout l’optimisation vidéo comme un problème de réglages d’encodeur. Maintenant, je la vois comme un problème de coût sur toute la durée de vie du fichier.

L’encodeur peut s’exécuter une seule fois. Les octets peuvent traverser le réseau des milliers ou des millions de fois.

Cela change ce que signifie « cher ».

Je suis prêt à dépenser du CPU une fois. Je le suis beaucoup moins à envoyer à chaque future requête des pixels créés par upscale, des frames qui n’ajoutent aucun mouvement utile ou un bitrate dont le contenu n’a pas besoin.

Le codec est resté banal : H.264 dans MP4. L’optimisation s’est faite autour.

Pour mon workload, la leçon est plus utile que n’importe quel flag FFmpeg : optimiser le coût que l’on paie à répétition, pas celui que l’on paie une seule fois.