Un jour, mon pipeline multimédia a cessé de publier certains fichiers à cause d'une erreur qui semblait presque dérisoire :
Durée de paquet CFR incorrecte : 5580 ticks, 5625 attendus
FFmpeg n'avait pas planté et le H.264 avait bien été produit. C'est mon validateur qui refusait le fichier après encodage : la vidéo devait avoir une cadence constante, mais un paquet ne tombait pas sur la grille temporelle prévue.
J'ai relancé le traitement : encore 5580. Une deuxième fois : toujours 5580. Un autre fichier source a ensuite produit la même valeur. Le problème était donc déterministe, pas un incident réseau passager.
Plus tard, j'ai rencontré un autre écart. À 24 images/s sur une piste à 90 000 unités par seconde, chaque durée normale devait valoir exactement 3750. Le validateur a trouvé 3751.
Ces deux cas ne racontent pas la même histoire. 5580 au lieu de 5625 représente 45 ticks, soit exactement 0,5 ms. 3751 au lieu de 3750 ne diffère que d'un tick, environ 11,1 microsecondes. Les résumer par « FFmpeg arrondit » aurait été trop facile.
Ils m'ont surtout obligé à distinguer ce que je mélangeais auparavant sous le mot FPS : cadence, base de temps, PTS/DTS et durée des paquets.
Le CFR n'est pas simplement une métadonnée « 16 fps »
Dans mon pipeline, voir 16/1 dans ffprobe ne suffit pas. Le CFR est un contrat : les instants de présentation suivent une grille régulière et la durée normale d'un échantillon correspond à un pas de cette grille.
1 / 16 = 0.0625 s = 62.5 ms
90000 / 16 = 5625 ticks
5625 découle donc directement du couple 16 images/s et 90 000 ticks/s.
Cette exigence n'est valable que parce que j'ai choisi des cadences représentables exactement. Si une cadence exige un motif de plusieurs durées entières, le validateur doit vérifier ce motif plutôt que réclamer une constante impossible.
Cadence, base de temps et échelle de temps MP4 sont trois notions distinctes
- Cadence : rythme d'affichage ; à 16 images/s, une image toutes les 62,5 ms.
- Base de temps : durée d'un tick entier, par exemple
1/90000s. - échelle de temps MP4 : nombre d'unités par seconde ; 90 000 signifie la même résolution temporelle.
- PTS : moment de présentation.
- DTS : moment où le paquet doit être décodé.
- Durée de paquet : durée de l'échantillon dans la base de temps du flux.
Avec des images B, PTS et DTS peuvent légitimement être différents. Les forcer à être égaux n'est donc pas une correction générale ; la documentation de setts le signale explicitement.
Pourquoi 90 000 était une bonne grille pour mon cas
| Cadence | Ticks par image |
|---|---|
| 10 | 9000 |
| 12 | 7500 |
| 15 | 6000 |
| 16 | 5625 |
| 18 | 5000 |
| 20 | 4500 |
| 24 | 3750 |
| 25 | 3600 |
| 30 | 3000 |
Et :
1 ms = 90 ticks
C'était pratique pour des animations dont les délais d'origine étaient exprimés en millisecondes. FFmpeg permet de fixer video_track_timescale dans le muxer MP4. La grille ne garantit pas la correction ; elle rend les écarts mesurables.
5580 indiquait déjà où chercher
5580 / 90000 = 0.062 s = 62 ms
J'avais précisément un fichier réel de 49 images affichées en 3,063 s, avec des délais alternant entre 62 et 63 ms :
49 / 3.063 ≈ 15.997 fps
62 + 63 = 125 ms
2 × 62.5 ms = 125 ms
Sur la grille milliseconde de la source, 62/63 ms est une approximation naturelle de 16 images/s. Après le choix explicite du CFR 16, la sortie doit cependant utiliser 62,5 ms, donc 5625 ticks, pour chaque image normale.
5580 indiquait fortement qu'une durée source de 62 ms avait survécu jusqu'à une étape où le temps aurait déjà dû être quantifié.
Je n'en tire pas une causalité artificielle : le journal prouve la valeur et l'arithmétique, pas la ligne exacte de code responsable.
L'horloge d'entrée à 1000 Hz n'était pas l'erreur
J'ai reproduit ce point avec FFmpeg 7.1.5 et ffconcat. Avec :
duration 0.010
option framerate 1000
Les horodatages des paquets conservaient bien les positions prévues :
0 ms
10 ms
20 ms
30 ms
j'obtenais 0, 10, 20 et 30 ms. En utilisant 30 dès cette étape, le temps était déjà ramené à des pas d'environ 33,3 ms.
1000 Hz conservait donc correctement les délais d'origine à la milliseconde. Cela ne signifiait pas que la sortie devait avoir 1000 images/s.
délais source précis
→ timeline de référence
→ choix du CFR
→ quantification explicite sur la grille CFR
→ conservation de cette grille jusqu'au MP4
La haute précision en entrée n’était pas le problème. Le problème était de ne pas rendre explicite la transition entre le temps source conservé et sa quantification sur la grille CFR.
Le CFR est une quantification contrôlée du temps
62, 63, 62, 63 ms
↓
62.5, 62.5, 62.5, 62.5 ms
Le filtre fps de FFmpeg est adapté à cette conversion : il construit la cadence demandée à partir des PTS, en répétant ou supprimant des images selon la règle d'arrondi.
Après cette frontière, je préfère conserver la grille au lieu de demander à une autre couche de refaire indépendamment la conversion.
Pourquoi trois relances n'ont rien changé
FAIL 5580, attendu 5625
retry 1/3
FAIL 5580, attendu 5625
retry 2/3
FAIL 5580, attendu 5625
Une relance est utile si la cause peut disparaître : réseau, stockage momentanément indisponible, ressource temporaire. Elle ne corrige pas une violation déterministe produite par le même algorithme.
- Erreur transitoire : relance possible.
- Entrée invalide : traitement alternatif ou rejet.
- Invariant déterministe brisé : arrêter et diagnostiquer.
Puis j'ai trouvé 3751 au lieu de 3750
90000 / 24 = 3750 ticks
Un fichier concaténé réel contenait pourtant 3751. Un tick vaut environ :
1 / 90000 s ≈ 11.111 µs
Impossible à percevoir à l'œil, mais intéressant car 24 images/s est exactement représentable sur cette grille. Le tick supplémentaire signalait la perte d'un invariant, pas une limite mathématique.
La copie du flux ne signifie pas que les horodatages restent intacts
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy final.mp4
-c:v copy évite un deuxième encodage H.264. Le conteneur doit malgré tout reconstruire une séquence temporelle de paquets.
La documentation de concat précise que la durée de chaque fichier sert à ajuster les timestamps du suivant. FFmpeg effectue aussi des changements d'échelle entre bases de temps rationnelles avec des règles d'arrondi.
Cela ne prouve pas que concat était l'unique origine de mon 3751. Cela prouve que l'identité du bitstream compressé et l'identité de la grille temporelle sont deux propriétés différentes.
PTS et DTS ne se réparent pas avec une formule unique
PTS = N * durée
DTS = N * durée
Cette formule peut être fausse en présence d'images B, car l'ordre de décodage diffère de l'ordre de présentation.
Une normalisation correcte doit partir d’une chronologie connue, restaurer la grille de présentation et les durées, tout en préservant une relation PTS/DTS légale.
C'est pourquoi je ne donne pas une expression setts universelle et hors contexte.
Pourquoi setts était le bon niveau d'intervention
setts peut modifier PTS, DTS, la durée et la base de temps au niveau des paquets sans réencoder la vidéo.
timeline connue
+ CFR connu
+ grille 90000
→ positions et durées attendues
→ normalisation des paquets
→ nouvelle validation
La correction vient du modèle temporel, pas d'une règle « si 3751, soustraire 1 ».
Pourquoi je n'ai pas ajouté une tolérance de ±1 tick
Une tolérance est nécessaire si la cadence n'est pas exactement représentable. Ici, j'avais choisi des valeurs qui le sont :
16 fps → 5625
24 fps → 3750
30 fps → 3000
Autoriser ±1 aurait simplement transformé une violation inexpliquée en état accepté. Le tick n'est pas dangereux visuellement ; perdre silencieusement le contrat l'est davantage pour le pipeline.
- \n
- si la grille impose une alternance de durées entières, valider le bon motif ; \n
- si la durée doit être un entier exact, exiger exactement cet entier ; \n
- ne jamais utiliser ±1 comme moyen universel de faire passer un validateur en échec. \n
Comment je valide le CFR paquet par paquet
avg_frame_rate et les autres résumés du flux sont utiles, mais ils ne suffisent pas pour ce contrat ; je dois examiner les paquets réels.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json final.mp4
Je vérifie la base de temps effective, puis pts, dts et duration.
expected = 90000 / 16 // 5625
for each normal video packet:
assert packet.duration == 5625
assert decode order is legal
assert PTS/DTS relationship is legal
assert frame count and duration match the plan
Des edit lists, du trimming ou d'autres cas intentionnels doivent être modélisés explicitement. Ce n'est pas une loi universelle du MP4, mais un contrat strict pour un générateur contrôlé.
Valider les métadonnées et décoder entièrement ne répondent pas à la même question
ffprobe/paquets → structure et temps
full decode → intégrité du flux compressé
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
Je ne publie qu'après les deux contrôles.
Un code de sortie 0 de l’encodeur ne signifie plus, à lui seul, que le fichier est terminé.
Ce que les deux erreurs ont réellement démontré
5580 : valeur répétée de façon déterministe, exactement équivalente à 62 ms à 90 kHz, correspondant aux délais 62/63 ms d’une source réelle, alors que CFR 16 exige 62,5 ms. L’expérience à 1000 Hz confirmait la conservation des millisecondes. Cela étaye fortement l’hypothèse d’une chronologie source ayant subsisté trop loin, sans permettre d’identifier à elle seule la fonction responsable.
3751 : paquet réel avec un tick de trop, dans un processus utilisant concat par copie de flux puis une normalisation avec setts. La documentation confirme les mécanismes de réajustement et de réécriture des horodatages, sans permettre d’affirmer que concat ajoute toujours un tick.
Mon processus aujourd'hui
- Reconstruire la chronologie réelle.
- Préserver les délais de la source avec une horloge d’entrée suffisamment précise.
- Choisir le CFR séparément.
- Quantifier explicitement sur cette grille.
- Utiliser une échelle de temps qui représente exactement les cadences autorisées quand c'est possible.
- Éviter une deuxième conversion indépendante de cadence.
- Valider les paquets avant concat.
- Vérifier la compatibilité complète avant la copie de flux.
- Revalider après concat.
- Normaliser à partir de la chronologie connue si nécessaire.
- Revalider.
- Décoder entièrement.
- Publier atomiquement seulement après succès.
La règle que j'ai gardée : le CFR est un contrat temporel entier
time base = 1/90000
durée normale = 5625 ticks
cadence = 62.5 ms
grille des paquets conforme
PTS/DTS légaux avec le réordonnancement H.264
5580 a révélé l'ancienne grille en millisecondes. 3751 m'a appris qu'un seul tick, même invisible, peut montrer qu'un invariant exact a cessé d'être respecté.
Je ne valide plus l'étiquette « CFR ». Je valide le temps dont cette étiquette doit découler.