Eines Tages verweigerte meine Medienpipeline die Veröffentlichung mehrerer Videos wegen einer Fehlermeldung, die zunächst beinahe lächerlich wirkte:
Ungültige CFR-Paketdauer: 5580 Ticks, erwartet wurden 5625
FFmpeg war nicht abgestürzt. Die H.264-Datei war erzeugt worden. Mein Validator lehnte sie erst danach ab, weil ein Video mit konstanter Bildrate einen Paketwert enthielt, der nicht auf dem von mir festgelegten Zeitraster lag.
Ich startete denselben Vorgang erneut: wieder 5580. Noch einmal: wieder 5580. Später traf ein anderer Ausgangsdatensatz denselben Fehler. Das war ein wichtiges Signal: kein zufälliger Netzfehler, sondern ein deterministischer Vertragsbruch.
Danach tauchte ein zweiter Fall auf. Bei 24 Bildern/s und einer Videospur mit 90 000 Zeiteinheiten pro Sekunde erwartete ich exakt 3750 Ticks. Ein Paket hatte 3751.
Diese Fälle darf man nicht mit „FFmpeg rundet eben“ zusammenfassen. Von 5580 zu 5625 fehlen 45 Ticks, genau 0,5 ms. Von 3751 zu 3750 ist es ein einziger Tick, ungefähr 11,1 Mikrosekunden.
Die Fehler zwangen mich, Begriffe sauber zu trennen, die ich vorher zu oft unter „FPS“ zusammengefasst hatte: Bildrate, Zeitbasis, PTS/DTS und Paketdauer.
CFR ist mehr als die Metadatenangabe „16 fps“
Für meinen Generator reicht es nicht, wenn ffprobe 16/1 meldet. CFR bedeutet hier ein regelmäßiges Präsentationsraster, auf dem jeder normale Sample-Schritt liegt.
1 / 16 = 0.0625 s = 62.5 ms
90000 / 16 = 5625 Ticks
5625 ist deshalb keine willkürliche Prüfkonstante. Der Wert folgt aus 16 Bildern/s und 90 000 Ticks/s.
Diese Strenge ist nur sinnvoll, weil meine erlaubten Bildraten die Zeitskala ganzzahlig teilen. Bei einer nicht exakt darstellbaren Rate müsste ein Validator das korrekte Muster unterschiedlicher Ganzzahldauern prüfen.
Bildrate, Zeitbasis und MP4-Zeitskala sind verschiedene Größen
- Bildrate: Präsentationsrhythmus, bei 16 fps alle 62,5 ms.
- Zeitbasis: Dauer eines Timestamp-Ticks, etwa
1/90000s. - MP4-Zeitskala: Ticks pro Sekunde; 90 000 entspricht ebenfalls
1/90000s pro Tick. - PTS: Zeitpunkt der Darstellung.
- DTS: Zeitpunkt, zu dem das codierte Paket dekodiert werden muss.
- Paketdauer: Dauer des Samples in der Zeitbasis des Streams.
Bei B-Frames dürfen PTS und DTS voneinander abweichen. Ein pauschales PTS = DTS wäre daher keine saubere Reparatur; auch die FFmpeg-Dokumentation zu setts warnt in diesem Zusammenhang vor B-Frames.
Warum ich 90 000 als Zeitskala gewählt habe
| Bildrate | Ticks pro Bild |
|---|---|
| 10 | 9000 |
| 12 | 7500 |
| 15 | 6000 |
| 16 | 5625 |
| 18 | 5000 |
| 20 | 4500 |
| 24 | 3750 |
| 25 | 3600 |
| 30 | 3000 |
Alle Werte sind ganzzahlig. Zusätzlich gilt:
1 ms = 90 Ticks
Das passte gut zu meinen animierten Quellen mit Millisekunden-Verzögerungen. FFmpeg kann die video_track_timescale der MP4-Spur explizit setzen. Die Skala repariert das Timing nicht; sie macht einen präzisen Vertrag möglich.
5580 war selbst ein Diagnosehinweis
5580 / 90000 = 0.062 s = 62 ms
Ich hatte tatsächlich eine Quelle mit 49 angezeigten Bildern in 3,063 s und wechselnden Verzögerungen von 62 und 63 ms:
49 / 3.063 ≈ 15.997 fps
62 + 63 = 125 ms
2 × 62.5 ms = 125 ms
Auf einem Millisekundenraster ist 62/63 ms eine plausible Näherung an 16 fps. Nach der bewussten Umwandlung zu CFR 16 muss die Ausgabe jedoch bei 62,5 ms beziehungsweise 5625 Ticks liegen.
Darum deutete 5580 stark darauf hin, dass eine originale 62-ms-Dauer in einen Bereich gelangt war, der bereits das CFR-Raster enthalten sollte.
Mehr behaupte ich nicht: Der Logeintrag beweist Zahl und Zusammenhang, aber nicht die exakte Codezeile, die den Wert durchgelassen hat.
Die 1000-Hz-Eingangsuhr war nicht das Problem
Mit FFmpeg 7.1.5 und ffconcat testete ich:
duration 0.010
option framerate 1000
Daraus entstanden Zeitpunkte bei:
0 ms
10 ms
20 ms
30 ms
Mit 30 als frühem Eingaberaster wurde dagegen bereits auf ungefähr 33,3-ms-Schritte quantisiert.
1000 Hz bewahrte also korrekt die Millisekundeninformation der Quelle. Es bedeutete keineswegs 1000 fps in der Ausgabedatei.
präzise Quellverzögerungen
→ autoritative Quellzeitachse
→ Ziel-CFR wählen
→ explizit auf CFR-Raster quantisieren
→ dieses Raster bis zur fertigen MP4 bewahren
Die hohe Präzision am Eingang war nicht der Fehler. Der Fehler war, den Übergang vom bewahrten Quell-Timing zum quantisierten CFR-Raster nicht ausdrücklich zu machen.
CFR ist kontrollierte Zeitquantisierung
62, 63, 62, 63 ms
↓
62.5, 62.5, 62.5, 62.5 ms
Der FFmpeg-Filter fps ist eine geeignete Stelle dafür: Er erzeugt die gewünschte Bildrate anhand der Eingabe-PTS und der gewählten Rundungsregel, indem Bilder ausgelassen oder wiederholt werden.
Nach dieser Stelle sollte eine weitere Schicht nicht erneut unabhängig entscheiden, wie das Zeitraster zu erzeugen ist.
Warum drei Wiederholungen nichts änderten
FAIL: 5580, erwartet 5625
Retry 1/3
FAIL: 5580, erwartet 5625
Retry 2/3
FAIL: 5580, erwartet 5625
Wiederholungen helfen bei vorübergehenden Ursachen. Sie beheben keinen deterministischen Vertragsbruch, der mit denselben Eingaben immer denselben ganzzahligen Wert erzeugt.
- Temporärer Fehler: Ein erneuter Versuch kann sinnvoll sein.
- Ungültige Eingabe: anders behandeln oder ablehnen.
- Deterministischer Invariantenbruch: stoppen und untersuchen.
Dann kam 3751 statt 3750
90000 / 24 = 3750 Ticks
Ein echter zusammengefügter Output enthielt trotzdem 3751. Ein Tick entspricht:
1 / 90000 s ≈ 11.111 µs
Optisch ist das bedeutungslos. Für den Vertrag ist es interessant, weil 24 fps in dieser Skala exakt darstellbar sind. 3751 ist keine notwendige Näherung.
Stream Copy bedeutet nicht, dass Zeitstempel unverändert bleiben
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy final.mp4
-c:v copy verhindert eine erneute verlustbehaftete H.264-Codierung. Der Container muss die Paketzeitstempel trotzdem zu einer neuen Sequenz zusammensetzen.
Der concat-Demuxer verwendet laut Dokumentation die Dauer jeder Datei, um die Zeitstempel der folgenden Datei anzupassen. FFmpeg skaliert Zeitstempel außerdem zwischen rationalen Zeitbasen mit definierten Rundungsregeln.
Das beweist nicht, dass concat allein meinen einzelnen 3751-Tick verursacht hat. Es beweist aber, dass „Bitstream kopiert“ und „Timestamp-Raster identisch“ nicht dasselbe sind.
PTS und DTS lassen sich nicht mit einer hübschen Formel reparieren
PTS = N * Dauer
DTS = N * Dauer
Bei H.264 mit B-Frames kann das falsch sein, weil Dekodier- und Darstellungsreihenfolge verschieden sind.
Die Normalisierung muss daher aus einer bekannten Segmentzeitachse abgeleitet werden: Präsentationsraster und Paketdauern wiederherstellen, ohne eine zulässige Reorder-Struktur zu zerstören.
Deshalb gibt es von mir keine kontextfreie „magische“ setts-Formel.
Warum setts die richtige Ebene war
setts kann Paket-PTS, DTS, Dauer und Ausgangszeitbasis ändern, ohne das Video neu zu codieren.
bekannte Segmentzeitachse
+ bekannter CFR
+ 90000er Raster
→ gültige Paketpositionen kennen
→ Zeitfelder normalisieren
→ erneut validieren
Das ist ein modellbasiertes Verfahren und kein Sonderfall „3751 minus 1“.
Warum ich keine Toleranz von ±1 Tick eingebaut habe
Bei nicht exakt darstellbaren Raten sind Rundungsmuster unvermeidlich. Mein erlaubter Satz war absichtlich anders:
16 fps → 5625
24 fps → 3750
30 fps → 3000
Wenn der Sollwert exakt darstellbar ist, würde ±1 nur einen unbekannten Fehler legalisieren. Ein Tick ist für das Auge unwichtig; der Verlust eines bewusst entworfenen Invarianten ist es nicht.
- \n
- wenn das Raster zwangsläufig zwischen ganzzahligen Dauern wechselt, das korrekte Muster prüfen; \n
- wenn die Dauer genau eine ganze Zahl sein muss, genau diesen Wert verlangen; \n
- ±1 nie pauschal verwenden, nur damit ein fehlgeschlagener Validator grün wird. \n
So prüfe ich CFR auf Paketebene
avg_frame_rate und ähnliche Stream-Zusammenfassungen sind hilfreich, reichen für diesen Vertrag aber nicht aus; ich prüfe die tatsächlichen Pakete.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json final.mp4
Ich prüfe die tatsächliche Zeitbasis sowie pts, dts und duration jedes relevanten Pakets.
expected = 90000 / 16 // 5625
for each normal video packet:
assert packet.duration == 5625
assert legal decode order
assert legal PTS/DTS relationship
assert frame count and duration match the plan
Bewusste Sonderfälle wie Edit Lists oder Trimming müssen ausdrücklich modelliert werden. Diese Regel ist mein Generatorvertrag, kein universelles Gesetz für jede MP4-Datei.
Metadatenprüfung und vollständiges Dekodieren prüfen Verschiedenes
Saubere Timestamps beweisen nicht, dass der gesamte H.264-Bitstream dekodierbar ist. Umgekehrt kann eine Datei vollständig dekodieren und trotzdem mein Zeitraster verletzen.
ffprobe/Pakete → Struktur und Timing
vollständiger Decode → Integrität des komprimierten Streams
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
Ein Exit-Code 0 des Encoders bedeutet für mich allein nicht mehr, dass die Datei fertig ist.
Was die beiden Fehler tatsächlich belegen
5580: Der Wert wiederholte sich deterministisch; 5580 bei 90 kHz sind exakt 62 ms; reale Quelldaten enthielten 62/63 ms; CFR 16 verlangt 62,5 ms; und der 1000-Hz-Versuch bewahrte Millisekunden korrekt. Das stützt stark die Erklärung, dass Quellzeit zu weit in die CFR-Stufe gelangte, beweist aber nicht die exakte Codezeile.
3751: Ein echter Output enthielt den einen zusätzlichen Tick; der Prozess nutzte Stream-Copy-Concat und später setts zur Normalisierung. FFmpeg dokumentiert sowohl Timestamp-Anpassungen bei concat als auch paketweises Umschreiben durch setts. Das ist mit Rescaling/Rundung vereinbar, aber keine Aussage „concat addiert immer einen Tick“.
Mein heutiger Ablauf
- Quellzeitachse aus echten Dauern rekonstruieren.
- Millisekunden-Timing präzise bewahren.
- Ziel-CFR separat wählen.
- Explizit auf das CFR-Raster quantisieren.
- Eine passende ganzzahlige Track-Zeitskala verwenden.
- Spätere Schichten die Bildrate nicht erneut konvertieren lassen.
- Segmente vor concat auf Zeitbasis, PTS/DTS und Dauer prüfen.
- Vor Stream Copy vollständige Kompatibilität prüfen.
- Nach concat das Paketraster erneut prüfen.
- Falls nötig aus der bekannten Zeitachse normalisieren.
- Noch einmal validieren.
- Alles vollständig dekodieren.
- Erst danach atomar veröffentlichen.
Die Regel, die geblieben ist: CFR ist ein ganzzahliger Zeitvertrag
time base = 1/90000
normale Bilddauer = 5625 Ticks
Präsentationsrhythmus = 62.5 ms
Paketzeitachse folgt diesem Raster
PTS/DTS bleiben für H.264-Reordering zulässig
5580 verriet das alte Millisekundenraster. 3751 war gerade deshalb wertvoll, weil ein einzelner praktisch unsichtbarer Tick zeigte, dass ein exakt definierter Invariant nicht mehr erhalten blieb.
Ich validiere nicht mehr das Etikett „CFR“. Ich validiere die Zeit, aus der dieses Etikett folgen muss.