Zurück zum Blog
13. August 2026Sergei Solod12 Min. Lesezeit

Wie ich animierte WebP, GIF und APNG in H.264 MP4 umwandle, ohne Bilder oder Zeitabläufe zu beschädigen

Mein Produktionsprozess rekonstruiert die vollständig angezeigten Bilder, bewahrt die Quellzeitsteuerung, wählt genau eine konstante Bildrate (CFR) für jede fertige MP4-Datei, berechnet die kleinste gemeinsame Bildfläche ohne Hochskalierung, codiert kompatible H.264-Segmente, fügt sie ohne zweite verlustbehaftete Codierung zusammen und prüft sowohl die Datei als auch ihre HTTP-Auslieferung. In einem gemessenen Lauf wurden 217 animierte WebP-Dateien mit insgesamt 1,49 GB zu einer einzigen H.264-MP4-Datei mit 78,49 MB.

H.264FFmpegMP4Animiertes WebPGIFAPNGVideokompressionMedienverarbeitung

Ich habe diese Verarbeitung nicht gebaut, um mit Codecs zu experimentieren. Der Anlass war viel praktischer: Animierte Bilder waren zu einer teuren Art geworden, etwas auszuliefern, das sich faktisch bereits wie ein kurzes stummes Video verhielt.

Meine Inhalte bestehen überwiegend aus kurzen animierten WebP-, GIF- und APNG-Dateien: meist nur wenige Sekunden lang, häufig mit einigen Dutzend sichtbaren Bildern und viel zeitlicher Redundanz. Die meisten Abrufe kommen von Mobilgeräten, dieselben Dateien werden wiederholt angefordert, und die einmalige Rechenzeit beim Codieren ist viel weniger wichtig als die später immer wieder übertragenen Bytes.

Die Schwierigkeit liegt nicht im Aufruf von FFmpeg. Eine animierte Bilddatei muss keine saubere Folge vollständiger Bilder mit einer regelmäßigen Bildrate sein. Sie kann Teilrechtecke, Misch- und Entsorgungsregeln, Alpha, unregelmäßige Verzögerungen, Bilder mit Dauer null, unterschiedliche Ausrichtungen und Zeitinformationen enthalten, die ein allgemeines Prüfwerkzeug irreführend zusammenfassen kann.

Darum behandle ich die Umwandlung als Menge von Invarianten und nicht als einzelnen Befehl:

animiertes WebP / GIF / APNG
        ↓
vollständig angezeigte Bildzustände rekonstruieren
        ↓
Quell-Timing wiederherstellen und bereinigen
        ↓
alle Quellen der finalen Sequenz analysieren
        ↓
eine CFR für die finale MP4 wählen
        ↓
kleinste gemeinsame Bildfläche ohne Hochskalierung berechnen
        ↓
kompatible H.264-Segmente codieren
        ↓
Streamvertrag prüfen
        ↓
per Streamkopie zusammenfügen
        ↓
Paket-Zeitachse normalisieren und prüfen
        ↓
HTTP-Auslieferung prüfen
        ↓
atomar veröffentlichen

Der Codec ist wichtig, aber wichtiger ist, dass die Animation nach der Umwandlung genau das zeigt, was sie vorher gezeigt hat.

Ein gemessenes Produktionsergebnis: 217 animierte WebP-Dateien wurden zu einer einzigen MP4-Datei mit 78,49 MB

Die Eingabe war kein einzelnes 1,49-GB-Video. Es waren 217 getrennte animierte WebP-Dateien mit 10.633 sichtbaren Bildern. Zusammen belegten die Quellanimationen etwa 1,49 GB.

EINGABE
217 animierte WebP-Dateien
1,49 GB insgesamt
10.633 sichtbare Bilder

AUSGABE
1 H.264 MP4
78,49 MB
0,98 Mbit/s
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow

Die resultierende H.264-Datei war 78,49 MB groß und lag bei ungefähr 0,98 Mbit/s. Gegenüber der Summe der Quelldateien entspricht das ungefähr dem Faktor 19 oder rund 94,7% weniger Daten.

Das ist ein echtes Ende-zu-Ende-Ergebnis der Verarbeitung, aber kein sauberer A/B-Vergleich „altes H.264 gegen neues H.264“. Die Darstellung wechselte von Hunderten animierter Bilddateien zu einem einzigen zeitlich komprimierten Video. Deshalb schreibe ich die gesamten 19× weder CRF 28 noch veryslow noch einer einzelnen Kodiereroption zu.

Zuerst rekonstruiere ich die Bilder, die der Betrachter tatsächlich sieht

Die gefährlichste Abkürzung ist die Annahme, jedes gespeicherte Animationsbild sei ein vollständiges Ersatzbild.

Ein Bild eines animierten WebP kann ein positioniertes Rechteck zusammen mit Misch- und Entsorgungsverhalten beschreiben. APNG kennt Offsets, Abmessungen, Dauer sowie Misch- und Entsorgungsoperationen. GIF kann den vorherigen Bildinhalt beibehalten, einen Bereich löschen oder einen früheren Zustand wiederherstellen.

Ein gespeichertes Bild kann deshalb nur ein kleiner Ausschnitt sein, dessen Bedeutung vom bereits aufgebauten Bild abhängt. Solche Ausschnitte wie vollständige Bilder zu codieren ergibt nicht dieselbe Animation in kleiner, sondern eine falsche Animation.

Meine Extraktionsgrenze ist der vollständig angezeigte Bildzustand: die fertig zusammengesetzten Pixel, die ein korrekter Betrachter nach der Entsorgung des vorherigen und der Mischung des aktuellen Bilds sehen würde.

Das ist die erste Korrektheitsgarantie der gesamten Verarbeitung. Ist ein falsch rekonstruierter Teilausschnitt erst in H.264 eingebettet, kann ihn später weder CRF noch ein Preset noch eine Containeroption reparieren.

Die Bilddauer ist Quelldaten, keine zu erratende FPS-Zahl

Animationsformate speichern Zeit unterschiedlich. Animiertes WebP verwendet Dauerwerte pro Bild in 1-ms-Einheiten. GIF speichert Verzögerungen in Hundertstelsekunden. APNG verwendet für jede Bilddauer Zähler und Nenner; ist der Nenner null, behandelt die PNG-Spezifikation ihn als 100.

Diese Verzögerungen bilden die Zeitachse. Eine von einem allgemeinen Werkzeug ausgegebene FPS-Zahl ist nur eine Zusammenfassung und kann irreführend sein.

Ein echter WebP-Fall in meiner Verarbeitung hatte 1264×720 und 49 sichtbare Bilder. Die Verzögerungen wechselten zwischen 62 und 63 ms; die Gesamtdauer betrug 3,063 Sekunden. Das entspricht praktisch 16 fps, weil ein Bild bei 16 fps genau 62,5 ms dauert.

Ein allgemeines Prüfwerkzeug meldete für dieselbe Quelle 25 fps. Hätte ich diese Zahl übernommen, hätte ich entweder die Quellzeitsteuerung verändert oder unnötige Wiederholungsbilder erzeugt.

Außerdem brauche ich eine klare Regel für fehlerhafte oder mehrdeutige Dauerwerte. WebP überlässt die Interpretation einer Dauer von null und oft auch sehr kleiner Werte der jeweiligen Implementierung. GIF kann Verzögerungen von null enthalten. APNG erlaubt einen Zähler von null, was bedeutet, dass das nächste Bild so schnell wie möglich gezeigt werden soll; Betrachter dürfen dennoch eine praktische Mindestdauer setzen.

Meine Normalisierung behält Millisekundengenauigkeit bei, setzt für null oder offensichtlich unbrauchbar kleine Dauern ein kleines Minimum von 10 ms und verwendet 100 ms nur als Rückfallwert, wenn tatsächlich keine sinnvolle Zeitangabe vorhanden ist. Diese Werte sind eine Produktionsregel, kein universeller Standard.

Ich wähle eine konstante Bildrate (CFR) für die gesamte fertige MP4-Datei statt pauschal 30 fps

Nach der Rekonstruktion der sichtbaren Zustände und ihrer Dauern überführe ich die Quellzeitachse in eine Videozeitachse. Ich kodiere nicht blind alles mit 30 fps.

Für alle Quellen, die in derselben finalen MP4 landen, prüfe ich diesen kleinen Kandidatensatz:

10, 12, 15, 16, 18, 20, 24, 25, 30 fps

Der Selektor wählt die niedrigste CFR, die die gesamte finale Sequenz hinreichend gut abbildet. Unterschiedliche finale MP4-Dateien dürfen unterschiedliche Raten verwenden; alle unabhängig codierten Segmente innerhalb einer finalen MP4 verwenden jedoch exakt dieselbe ausgewählte CFR.

Am Beispiel mit 3,063 Sekunden sieht man den Effekt direkt: Bei 16 fps werden ungefähr 49 Ausgabebilder benötigt, bei 30 fps ungefähr 92. Benötigt eine andere Quelle in derselben MP4 tatsächlich 30 fps, verwendet die gesamte Sammlung 30. Innerhalb eines finalen Datenstroms mische ich keine unterschiedlichen Bildraten.

Für die Videospur verwende ich eine Zeitskala von 90.000 Hz, weil jede erlaubte CFR eine ganzzahlige Bilddauer ergibt:

10 fps → 9000 Takte
12 fps → 7500 Takte
15 fps → 6000 Takte
16 fps → 5625 Takte
18 fps → 5000 Takte
20 fps → 4500 Takte
24 fps → 3750 Takte
25 fps → 3600 Takte
30 fps → 3000 Takte

Diese exakte Bildrasterung entsteht durch die Zeitskala der Videospur. Die allgemeine MP4-Zeitskala setze ich aus Konsistenzgründen ebenfalls auf 90.000, sie ist aber eine getrennte Containeruhr. Der Validator prüft die Videopakete gegen das exakte Ganzzahlraster und nicht gegen gerundete Dezimaldauern.

Auflösungsgrenzen sind Obergrenzen, keine vorgeschriebenen Bildflächen

Mein Auslieferungsrahmen liegt ungefähr bei 1280×720 für Querformat, 720×1280 für Hochformat und höchstens 960 Pixel Breite wie Höhe bei gemischter Ausrichtung.

Die unverhandelbare Regel lautet: niemals hochskalieren. Eine 900×600-Quelle wird bei 1280×720 nicht detailreicher; es entstehen nur interpolierte Pixel, die der Kodierer zusätzlich beschreiben muss.

Die zweite Regel ist weniger offensichtlich: 960×960 ist ein Begrenzungsrahmen, keine vorgeschriebene quadratische Bildfläche.

Zuerst berechne ich für jede Quelle aktive Abmessungen, wobei nur verkleinert werden darf. Anschließend erhält die finale Sequenz die kleinste gemeinsame gerade Bildfläche, die alle bereits verkleinerten aktiven Rechtecke aufnehmen kann.

Benötigt die Sequenz zum Beispiel ein 960×540-Querbild und ein 500×900-Hochbild, kann die gemeinsame Fläche 960×900 statt 960×960 sein. Alle Segmente haben weiterhin identische codierte Abmessungen, daher bleibt Datenstromkopie beim Zusammenfügen möglich; gleichzeitig muss keine nutzlose schwarze Fläche codiert werden.

Das Seitenverhältnis bleibt erhalten; freie Fläche wird aufgefüllt statt das Bild zu verzerren. In meiner Verarbeitung ist der Hintergrund schwarz. Da normales H.264/yuv420p den ursprünglichen Alphakanal nicht erhält, wird Transparenz bewusst gegen diesen Hintergrund zusammengesetzt.

Warum H.264 MP4 gut zu diesem Auslieferungsproblem passt

GIF, APNG und animiertes WebP sind keine primitiven Formate. Auch sie können unveränderte Flächen effizient behandeln. „Video ist immer kleiner“wäre deshalb falsch.

H.264 ist jedoch für zeitliche Vorhersage zwischen Bildern konstruiert. Kurze gezeichnete Schleifen mit statischem Hintergrund und kleinen veränderten Regionen sind ein günstiger Fall für Referenzbilder, Zwischenbildvorhersage sowie P- und B-Bilder.

Apples aktuelle Safari-Dokumentation empfiehlt H.264-codierte MP4-Dateien für statisches Video und weist darauf hin, dass animierte GIFs bis zu zwölfmal mehr Bandbreite und etwa doppelt so viel Energie wie ein moderner Videocodec benötigen können. Diese 12× sind ein Beispiel von Apple, nicht meine Messung.

Mein gemessenes 19×-Ergebnis ist daher eine nützliche Produktionsbeobachtung. Trotzdem messe ich im Einzelfall, statt anzunehmen, dass jede bereits kleine animierte WebP-Datei gegen MP4 verlieren muss.

Jede Animation wird als Segment unter einem gemeinsamen Datenstromvertrag codiert

Wenn der Kodierer startet, kennt die Verarbeitung bereits die sichtbaren Bilder, die gemeinsame CFR, die finale Bildfläche und die erwartete Dauer.

Der zentrale Teil meines Segmentbefehls sieht ungefähr so aus:

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

Das maximale GOP liegt bei ungefähr fünf Sekunden und wird aus der gewählten CFR abgeleitet: 80 Bilder bei 16 fps, 120 bei 24 fps und 150 bei 30 fps.

Die Begrenzung -t "$EXPECTED_DURATION" ist nicht kosmetisch. In meiner Bildlisten-Verarbeitung wird das letzte Bild als Abschlussmarke wiederholt, damit die Dauer des vorherigen echten Bilds berücksichtigt wird. Ohne explizite Dauerbegrenzung kann diese Wiederholung als zusätzliches Endbild erscheinen.

Ich habe das mit dem Fall 49 Bilder / 3,063 Sekunden reproduziert. Ohne -t entstanden 50 Bilder bei 16 fps und 94 bei 30 fps. Mit -t 3.063 waren es die erwarteten 49 bei 16 fps und 92 bei 30 fps.

Zusammenfügen per Datenstromkopie ist erst nach strenger Kompatibilitätsprüfung sicher

Der concat-Demultiplexer von FFmpeg erwartet gleiche Datenströme einschließlich Codec und Zeitbasis und verwendet die Dauer jeder Datei, um die nächste zu positionieren. Falsche Dauerinformationen können daher Zeitachsenfehler verursachen.

Ich verwende das Zusammenfügen nicht, um inkompatible Dateien kompatibel zu machen. Ein Segment muss vor der Annahme bereits diesen Vertrag erfüllen:

CFR der Sammlung = identisch
Zeitbasis des Streams = identisch
MP4-Zeitskala der Videospur = identisch
Bildfläche / SAR = identisch
Profil / Level / Pixelformat = identisch
Farbsignalisierung = identisch
avcC / AVC-Zusatzdaten = bytegenau identisch

Ich verwende stitchable=1 von x264, weil die Segmente unabhängig codiert werden. Ich behandle den Schalter jedoch nicht als Beweis für identische AVC-Konfiguration. Vor dem Zusammenfügen vergleiche ich die tatsächlichen Konfigurationsbytes.

Ist der Vertrag erfüllt, benötigt die finale Verbindung keine zweite verlustbehaftete Codierung:

ffmpeg -f concat -safe 0 -i segments.ffconcat \
  -c:v copy \
  -an -sn -dn \
  -movflags +faststart \
  final.mp4

-c:v copy verhindert, dass die bereits fertigen H.264-Segmente erneut dekodiert und komprimiert werden.

Die Validierung prüft sowohl die MP4-Datei als auch ihre Auslieferung

Ich veröffentliche eine Datei nicht allein deshalb, weil FFmpeg mit Rückgabewert 0 beendet wurde.

Der Validator hat reale deterministische Zeitfehler gefunden: Bei 16 fps traten 5580 Takte auf, obwohl der Vertrag 5625 verlangte; später enthielt ein 24-fps-Ergebnis 3751 statt exakt 3750. Die detaillierte Untersuchung dieses Ein-Takt-Problems ist eine eigene Geschichte. Hier reicht die Erkenntnis, dass ein erneuter identischer Lauf einen deterministischen Zeitachsenfehler nicht repariert.

Am Medienobjekt prüfe ich die erwartete Zahl der Datenströme, H.264-Profil und -Level, Pixelformat, exakt geplante Abmessungen, SAR, Farbsignalisierung, 90-kHz-Zeitskala der Videospur, Paketdauern, Bild- und Paketzahlen, Gesamtdauer, PTS/DTS-Beziehungen, identische AVC-Konfiguration der Segmente, moov vor mdat und eine vollständige Decodierung mit strenger Fehlerbehandlung.

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 -

Eine lokal korrekte MP4 kann über das Netz trotzdem falsch ausgeliefert werden. Deshalb prüfe ich zusätzlich den HTTP-Pfad: erwarteten Content-Type, korrekten Content-Length, Byte-Range-Unterstützung, eine gültige Antwort 206 Partial Content und einen korrekten Content-Range.

Wenn ich den Kodiervertrag ändere, führe ich außerdem einen kleinen Test auf echten Geräten und Browsern durch, statt anzunehmen, ffprobe beweise die Gerätekompatibilität. Ich prüfe Start, Springen, Schleifenwiedergabe, Hintergrund/Wiederaufnahme und Bereichswiedergabe auf einem aktuellen iPhone mit Safari, einem einfachen Android-Gerät und gängigen Desktopbrowsern.

Erst wenn Datei und Auslieferung den Vertrag erfüllen, ersetze ich das Produktionsobjekt atomar.

Wo diese Verarbeitung bewusst Informationen verliert

Das ist eine Transformation für die Auslieferung, kein Archivmaster. Alpha wird gegen einen Hintergrund zusammengesetzt. Unregelmäßige Quellzeitsteuerung wird auf eine CFR der finalen MP4 quantisiert. Große Quellen dürfen verkleinert werden. Bereits verlustbehaftetes animiertes WebP erhält eine weitere verlustbehaftete Generation. Audio ist absichtlich nicht vorhanden.

Ich würde genau diese Verarbeitung nicht verwenden, wenn Transparenz über beliebigen Hintergründen erhalten bleiben muss, wenn die exakte unregelmäßige Bildzeitsteuerung selbst inhaltlich wichtig ist, wenn ein Archivmaster entsteht oder wenn die Anwendung bereits eine adaptive Mehrcodec-Videostruktur besitzt, die das Auslieferungsproblem anders löst.

Sehr kleine und bereits stark optimierte animierte WebP-Dateien messe ich ebenfalls zuerst, statt automatisch anzunehmen, MP4 müsse kleiner sein.

Meine heutige Produktionsreihenfolge

  1. Animationsformat erkennen und echte Frame-Steuerdaten lesen.
  2. Vollständig angezeigte Bildzustände entsprechend Misch- und Entsorgungsregeln rekonstruieren.
  3. Jede Bilddauer wiederherstellen und bereinigen.
  4. Die maßgebliche Quellzeitachse mit Millisekundengenauigkeit aufbauen.
  5. Alle Quellen analysieren, die in dieselbe finale MP4 kommen.
  6. Eine gemeinsame CFR aus 10/12/15/16/18/20/24/25/30 wählen.
  7. Sichtbare Zustände auf diese CFR-Zeitachse abbilden.
  8. Aktive Abmessungen nur durch Verkleinern berechnen; niemals hochskalieren.
  9. Die kleinste erforderliche gemeinsame gerade Bildfläche aufbauen.
  10. Freie Fläche ohne Verzerrung auffüllen und Alpha bewusst zusammensetzen.
  11. Jede Quelle als H.264 Main@3.1 / yuv420p / avc1 unter demselben Datenstromvertrag codieren.
  12. Jedes Segment auf seine erwartete Dauer begrenzen.
  13. Segmente ablehnen, deren reale AVC-Konfiguration oder Zeitsteuerung den Vertrag verletzt.
  14. Akzeptierte Segmente mit -c:v copy zusammenfügen.
  15. Finale Paketzeitachse normalisieren und prüfen.
  16. Das Ergebnis vollständig decodieren.
  17. HTTP-Header, Bytebereiche und Teilantworten prüfen.
  18. Nach Änderungen am Kodierprofil einen Geräte- und Browsertest durchführen.
  19. Erst nach allen erfolgreichen Prüfungen atomar veröffentlichen.

Die Zeitachse ist die Referenz, nicht die Dateiendung

Animiertes WebP, GIF oder APNG ist eine zeitlich geordnete Folge angezeigter Bildzustände und nicht bloß ein Ordner voller Bilder mit einer bestimmten Endung.

H.264 kann zeitliche Redundanz sehr effizient nutzen, aber falsche Bildkomposition, erfundene Zeitsteuerung oder inkompatible Segmentmetadaten nicht reparieren. Ein großer Teil der Technik, die diese Verarbeitung zuverlässig macht, liegt vor und nach x264.

Die wichtigste Regel lautet für mich: Die Darstellung sollte erst geändert werden, wenn exakt beschrieben ist, was dabei unverändert bleiben muss.

Primärdokumentation