Fehlerhaft kopierte MP4-Datei reparieren
Neulich kam ein Kumpel und Arbeitskollege mit einem ''kleinen Problem'' zu mir. Er hatte mit seinem Android-Smartphone (Galaxy S3) mehrere Videos gemacht, und eins davon wurde nicht mehr abgespielt, nachdem er die Datei auf der MicroSD-Karte in einen anderen Ordner verschoben hatte. Na gut, dachte ich, da gibt's doch sicher ein kleines Tool für, mit dem sich das schnell wieder reparieren lässt. Tja, Pustekuchen, war wohl nix...
Als erstes habe ich natürlich versucht, die Datei in sämtlichen Playern und Konvertern zu öffnen, die mir so bekannt sind. Keiner davon wollte aber auch nur annähernd irgendwas mit dem Clip anfangen. Also habe ich nach einer Art Repair-Tool für MP4-Dateien gesucht. So wirklich finden konnte ich da nichts, außer ein HD Video Repair Utility. Praktischerweise gab's das direkt als Testversion zum Download, welche immerhin 50% des Videos wiederherstellen kann. Also ausprobiert und... Naja. Ein Video kam dabei raus, aber der Ton war nur die ersten paar Sekunden lang synchron, danach fehlten immer mehr Audio-Samples. Kurzum: In meinem Fall nicht zu gebrauchen.
Also blieb mir nur noch der HexEditor als letztes Werkzeug. Dazu habe ich mir erstmal ein paar Informationen zum MP4-Dateiformat besorgt. Als sehr hilfreich erwiesen sich die folgenden Seiten (sowie ein paar Forenbeiträge):
http://thompsonng.blogspot.de/2010/11/mp4-file-format.html
http://thompsonng.blogspot.de/2010/11/mp4-file-format-part-2.html
http://code.google.com/p/mp4parser/
Kurz zusammengefasst: Eine MP4-Datei besteht aus sogenannten Atoms. Ein Atom enthält, im einfachsten Falle, einen festen Header (bestehend aus der Längenangabe des Atoms und einem Kennzeichen aus 4 Kleinbuchstaben) und dann die entsprechenden Daten. Das Grundgerüst sieht normalerweise so aus:
ftyp
mdat
moov
Wobei ftyp die generellen Infos zu den verwendeten Codecs enthält, mdat die eigentlichen Video-Daten und moov eine Art Tabelle mit den genauen Eckdaten zu jedem Sample (Länge, Position in der Datei, Codierverfahren, etc.). Wenn jetzt eine MP4-Datei unvollständig ist und am Ende die moov-Daten nicht oder nur teilweise vorhanden sind, weiss der Player zum Beispiel nichts mehr mit den mdat-Daten anzufangen, weil er nicht weiss wo Anfang und wo Ende ist. Das kann zum Beispiel der Fall sein, wenn eine Aufnahme abgebrochen wurde, weil plötzlich der Akku leer war oder so. War hier jetzt nicht der Fall, ein kurzer Blick im HexEditor reichte um zu sehen, dass der moov-Teil vorhanden war und auch ähnlich aussah wie bei einem funktionierenden Clip.
Da die genaue Abfolge der Atoms in einer MP4-Datei nicht zu 100% exakt vorgegeben ist, gibt es auch noch folgende Möglichkeit:
ftyp
moov
mdat
Da im mdat-Atom ja feste Offsets enthalten sind, kann man die Atoms natürlich nicht so ohne weiteres vertauschen, aber genau das habe ich als nächstes gemacht: Das moov-Atom mit dem HexEditor direkt zwischen ftyp und moov gesetzt. Dadurch konnte ich die Datei zwar immer noch nicht abspielen, aber zumindest schonmal mit dem MP4-Parser öffnen. Die angezeigten Daten waren alle vollständig, also konnte der Fehler nur noch im mdat-Atom liegen. Na toll, dachte ich. Hier mal ein kleines Beispiel, wie die mdat-Daten im HexEditor aussehen:

Die Markierung zeigt den Anfang eines Samples
Und das über fast 2GB, wo soll man da einen Fehler finden?! Also musste ich wohl oder übel wie ein Decoder an die Sache rangehen und die mdat-Daten mithilfe der moov-Daten entschlüsseln.
Dazu nochmal eine kleine Info: Das mdat-Atom besteht in erster Linie aus sogenannten Chunks. Dabei gibt es Chunks für jeden Track in der Datei, in meinem Falle also zwei (einmal Video, einmal Audio). Diese Chunks sind dann nochmals in Samples unterteilt, wobei die Anzahl der Samples in jedem Chunk variabel ist. Mein zu rettender Clip ist also ca. 1.8GB groß, hat knapp 15 Minuten Laufzeit bei einer Auflösung von 1920x1080 Pixeln und enthält ganze 866 Chunks für die Videospur. Jeder Chunk ist so bummelig 2MB groß - Da kann man im HexEditor immer noch nichts mit anfangen. 2MB sind immer noch eine Menge Gemüse zum durchsuchen, der Screenshot oben zeigt mal gerade 0,7kB... Jeder Chunk besteht ja aber nun aus mehreren Samples, in meinem Falle meistens aus 31. Damit verbleiben immer noch knapp 70kB pro Sample, und das ganze dann über 26.000 Mal! Viel zu viel um das ''mal eben von Hand'' im HexEditor durchzugehen.
Also habe ich mein gutes altes Visual Basic zur Hilfe genommen. Scheisse ja, das ist sicher nicht so elegant wie ein Python- oder Java-Script, aber damit bin ich eben halt vertraut und kann gleich loslegen und erreichen was ich will...
Ich habe also einen kleinen Parser geschrieben, der das moov-Atom einliest, alle relevanten Sample-Daten rauszieht und mit deren Hilfe die jeweils ersten 16 Bytes von jedem Sample ausliest und in eine Log-Datei schreibt. Klingt easy, hat auch nur einen ganzen Abend gedauert bis ich das Ganze soweit am Laufen hatte... ;) Der MP4-Parser war dabei übrigens überaus hilfreich.
Und so sieht das dann im Logfile aus:
13:02:31 chunk:001 sample:001 length:068464 offset:000000032 data:00 01 0B 6C 65 B8 40 7F 8D D8 25 E8 06 A8 73 AF 13:02:31 chunk:001 sample:002 length:043233 offset:000068496 data:00 00 A8 DD 41 E2 21 19 C7 80 F5 B4 02 1B ED CA 13:02:31 chunk:001 sample:003 length:061378 offset:000111729 data:00 00 EF BE 41 E4 41 19 AF 27 B2 A4 20 3E D3 85 13:02:31 chunk:001 sample:004 length:067667 offset:000173107 data:00 01 08 4F 41 E6 61 15 BB 15 08 CC FD 6E 4D 9C 13:02:31 chunk:001 sample:005 length:066737 offset:000240774 data:00 01 04 AD 41 E8 81 15 BE D4 30 89 3D 22 5D 89 13:02:31 chunk:001 sample:006 length:065903 offset:000307511 data:00 01 01 6B 41 EA A1 15 EF D0 DF 29 10 FE DA C0 13:02:31 chunk:001 sample:007 length:066538 offset:000373414 data:00 01 03 E6 41 EC C1 15 E7 76 EE BB 33 6A 4C 3F 13:02:31 chunk:001 sample:008 length:068770 offset:000439952 data:00 01 0C 9E 41 EE E1 15 BE FD 53 E8 36 EA 14 2E 13:02:31 chunk:001 sample:009 length:067051 offset:000508722 data:00 01 05 E7 41 F1 01 15 BB AB 34 12 CD 31 1E 19 13:02:31 chunk:001 sample:010 length:065392 offset:000575773 data:00 00 FF 6C 41 F3 21 15 BE 3B CB 9F 80 4E 05 E0 13:02:31 chunk:001 sample:011 length:066534 offset:000641165 data:00 01 03 E2 41 F5 41 15 9F 58 43 8D E9 6D 76 02 13:02:31 chunk:001 sample:012 length:067386 offset:000707699 data:00 01 07 36 41 F7 61 15 BC E6 CE 98 D5 17 80 E6 13:02:31 chunk:001 sample:013 length:069420 offset:000775085 data:00 01 0F 28 41 F9 81 15 F2 40 D1 14 20 66 A9 D1 13:02:31 chunk:001 sample:014 length:067099 offset:000844505 data:00 01 06 17 41 FB A1 15 BE 3C F7 19 E9 1B DF D3 13:02:31 chunk:001 sample:015 length:067050 offset:000911604 data:00 01 05 E6 41 FD C1 15 C4 80 B2 80 0A 12 66 51 13:02:31 chunk:001 sample:016 length:065352 offset:000978654 data:00 00 FF 44 41 FF E1 15 9E C4 FD 18 35 EC B2 03 13:02:31 chunk:001 sample:017 length:067313 offset:001044006 data:00 01 06 ED 41 E0 01 15 BE 3B 92 63 87 6D D6 6F 13:02:31 chunk:001 sample:018 length:068839 offset:001111319 data:00 01 0C E3 41 E2 21 15 F6 00 2A E1 B6 8F 71 72 13:02:31 chunk:001 sample:019 length:067015 offset:001180158 data:00 01 05 C3 41 E4 41 15 C3 E6 E2 FD 88 8C B8 57 13:02:31 chunk:001 sample:020 length:067102 offset:001247173 data:00 01 06 1A 41 E6 61 15 BD DE 9E 3A 6A 5E BA BF
Natürlich kann man mit so ein paar Sample-Daten alleine nix anfangen, aber es ist doch schonmal ein Muster zu erkennen: Fast alle Samples fangen mit ''00 01'' an und das 5. Byte ist meistens ''41''. Und genau so habe ich dann den ersten Fehler gefunden. Seht her:
13:03:22 chunk:194 sample:5990 length:066827 offset:427429546 data:00 01 05 07 41 E6 61 37 CB FE CD 22 5F 17 AF 2C 13:03:22 chunk:194 sample:5991 length:067683 offset:427496373 data:00 01 08 5F 41 E8 81 37 DA BF 3B FC F2 FA 98 B8 13:03:22 chunk:194 sample:5992 length:067567 offset:427564056 data:00 01 07 EB 41 EA A1 37 DB 4E 80 2C 3A F2 C2 2A 13:03:22 chunk:194 sample:5993 length:067774 offset:427631623 data:00 01 08 BA 41 EC C1 37 DB 39 CE 20 C0 27 8F 60 13:03:22 chunk:194 sample:5994 length:066047 offset:427699397 data:00 01 01 FB 41 EE E1 37 DA E5 F9 C3 CB D1 3B 7D 13:03:22 chunk:194 sample:5995 length:067542 offset:427765444 data:00 01 07 D2 41 F1 01 37 50 11 5D 4E C1 34 F5 61 13:03:22 chunk:194 sample:5996 length:066882 offset:427832986 data:00 01 05 3E 41 F3 21 37 DA 4C CC D1 E2 65 17 3E 13:03:22 chunk:194 sample:5997 length:066948 offset:427899868 data:00 01 05 80 41 F5 41 37 F2 86 A7 7A 36 85 D5 A4 13:03:22 chunk:194 sample:5998 length:068321 offset:427966816 data:00 01 0A DD 41 F7 61 37 DA 46 5F C0 38 AA F4 76 13:03:22 chunk:194 sample:5999 length:067126 offset:428035137 data:00 01 06 32 41 F9 81 37 7D 0E 62 28 8B 4D 21 2A 13:03:22 chunk:194 sample:6000 length:066964 offset:428102263 data:8D 3F 61 F7 A8 4E 11 D4 F5 50 F0 8D AD FB 37 16 13:03:22 chunk:194 sample:6001 length:171903 offset:428169227 data:EC C8 64 77 FA CE 75 4A B2 87 DA FC F9 51 44 09 13:03:22 chunk:194 sample:6002 length:074227 offset:428341130 data:ED D6 44 D5 5E BA DA E0 A9 82 CA D9 06 1B D7 00 13:03:22 chunk:194 sample:6003 length:067298 offset:428415357 data:FE C4 4C 18 FF EC B5 AB C0 50 B4 A6 7C 2A AE 3E 13:03:22 chunk:194 sample:6004 length:070040 offset:428482655 data:BA 25 55 4B DC EF 5F 03 BA 10 56 9E DD 68 63 8B 13:03:22 chunk:194 sample:6005 length:065621 offset:428552695 data:C2 E3 18 05 D8 37 02 AD B3 AA 5A 9D 9E 55 4D 4B 13:03:22 chunk:194 sample:6006 length:065906 offset:428618316 data:7F DB E8 38 65 77 19 02 5F 12 9E 08 2B 01 39 B3 13:03:22 chunk:194 sample:6007 length:069671 offset:428684222 data:65 9E 44 80 4F F5 AB 3C 4C E7 6F D5 93 B6 74 97 13:03:22 chunk:194 sample:6008 length:066243 offset:428753893 data:F6 67 24 E9 36 E1 6D 3D DE 61 81 EA B7 58 FA CE 13:03:22 chunk:194 sample:6009 length:067957 offset:428820136 data:24 74 89 25 15 9D 53 26 1C 98 94 79 9A FF CB 69 13:03:22 chunk:194 sample:6010 length:065729 offset:428888093 data:4F 56 AE 55 31 56 2E EA 01 29 56 10 B7 85 29 A2
Sample 5998 ist wohl noch in Ordnung, weil der Anfang von 5999 auch noch gut aussieht. Aber dann Sample 6000 passt schon gar nicht mehr, also muss irgendwo bei 5999 was schief gelaufen sein. Da ich ja jetzt zu jedem Sample den Offset hatte, konnte ich das 5999ste Sample im HexEditor markieren und nach so etwas wie ''00 01 xx xx 41'' Ausschau halten, um den Anfang des nächsten Samples zu finden. Am einfachsten war eine Suche nach ''00 01'', die auch prompt lediglich ein einziges Ergebnis brachte. Von diesem Offset habe ich dann den Startoffset des Samples abgezogen und bekam folgendes Ergebnis: 4096. Also ist offenbar beim Verschieben der Datei ein ganzer Cluster von 4kB verloren gegangen. Scheint mir eine plausible Erklärung zu sein!
Die Daten sind natürlich verloren, aber bei einem einzelnen Sample verursacht das lediglich einen kleinen Ratzer. Ich habe einfach die vorangegangenen 4kB kopiert und an der Stelle wieder eingefügt, damit die nachfolgenden Offsets wieder stimmen. Wahrscheinlich hätte ich auch mit Nullen auffüllen können, wollte ich aber nicht ;).
Leider wollte das Video danach aber immer noch nicht spielen, also habe ich noch mal mein VB-Tool drüberlaufen lassen. Der 194. Chunk war jedenfalls wieder in Ordnung und auch alle weiteren Chunks bis zu Nummer 416 - das 12.897ste Sample war wieder fehlerhaft. Also den ganzen Vorgang wiederholt, wieder war die Suche nach ''00 01'' erfolgreich, und diesmal hat's dann wirklich geklappt! Das Video läuft wieder!
Glücklicherweise lagen die beiden Fehler mitten in einem Video-Chunk, sonst hätte ich den Audio-Track auch noch durchgehen müssen...
Die Arbeitsweise des Programms ist in etwa so:
- Finde das moov-Atom
- Finde das stco-Atom und erstelle eine Liste der darin enthaltenen Chunk-Offsets
- Finde das stsc-Atom und erstelle eine Liste, wieviele Samples in jedem Chunk enthalten sind
- Finde das stsz-Atom und erstelle eine Liste, wie lang jedes Sample ist
- Berechne die Offsets für jedes einzelne Sample
- Lies von diesen Offsets jeweils ein paar Bytes ein und schreibe sie in ein Logfile
Das Ganze ist jetzt natürlich ''nur'' auf meinen speziellen Fall zugeschnitten. Das MP4-Format ist ja noch wesentlich umfangreicher, aber ein Programm für jeden erdenklichen Fall zu schreiben ist sicherlich ein riesen Aufwand. Jedenfalls hat's geklappt, und vielleicht ist es ja eine Anregung für den Einen oder Anderen von euch in einem solchen Fall nicht gleich die Flinte ins Korn zu werfen.
Auf meinen Quick'n'Dirty-Code kann ich zwar nicht wirklich stolz sein, aber ich will ihn euch für eigene Experimente auch nicht vorenthalten, also gibt's hier den ganzen Mist zum runterladen ;).