Tutorial: Mapping mit ffmpeg

Einleitung

Es werden drei Test-Dateien aus Internet-Streams aufgenommen, daran wird erklärt, wie das Mapping von Streams mit ffmpeg funktioniert. Ebenfalls wird erläutert, wie eine gezielte Kodierung bestimmter Streams erreicht wird, wie die Sprache einzelner Streams gesetzt wird und wie man einen Audio-Stream als "default" setzt.

Am Ende wird noch das Video optimiert.

Im Ergebnis hat man dann aus den aufgenommenen Internet-Streams eine perfekte Videodatei, die den eigenen Wünschen entspricht.

Aufwärm-Übungen

Bevor es richtig los geht, schauen wir uns an, wie wir an drei geeignete Beispiel-Dateien kommen. Es bietet sich an, einen öffentlich-rechtlichen Stream aufzuzeichnen. Das kann ffmpeg auch. Die Eingabe-Option ffmpeg -i kann als Parameter nicht nur eine Datei, sondern auch einen Videostream aus dem Internet aufnehmen. Damit der Stream aber sinnvoll beendet wird, sollte man eine Aufnahmezeit angeben. Für unseren Test wählen wir eine kurze Aufnahmezeit von einer Minute.

Mit diesem Befehl erhalten wir eine Liste der Streams unter dieser Adresse:

ffmpeg -i "https://mcdn.one.ard.de/ardone/hls/master.m3u8"

Stream-urls ändern sich häufig, auch der Aufbau der Streams. Falls dieser Stream nicht funktioniert, oder nicht mehr so aussieht wie in diesem Beispiel, einfach einen ähnlichen Stream suchen und ausprobieren. Streams öffentlich-rechtlicher Sender lassen sich auf vielfältige Art und Weise recht einfach finden.

Die Ausgabe ist sehr komplex, wir kürzen hier den Weg ab und beschäftigen uns nicht mit den Details. Das Interessante ist z.B. dieser Block, der zeigt, welche Streams eigentlich unter dieser url laufen. Wir werden uns mit den drei markierten näher beschäftigen, man kann natürlich auch die anderen aufzeichnen.

Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-360p-1328/00132/master-360p-1328_00691.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-360p-1328/00132/master-360p-1328_00692.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-540p-1728/00132/master-540p-1728_00691.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-540p-1728/00132/master-540p-1728_00692.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-720p-3328/00132/master-720p-3328_00691.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-720p-3328/00132/master-720p-3328_00692.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-1080p-5128/00132/master-1080p-5128_00691.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-1080p-5128/00132/master-1080p-5128_00692.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-270p-828/00132/master-270p-828_00691.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-270p-828/00132/master-270p-828_00692.ts' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-original/00132/master-original_00691.aac' for reading
Opening 'https://mcdn-one.akamaized.net/ardone/hls/master-original/00132/master-original_00692.aac' for reading

Indem wir uns nun Details zu dem markierten Stream anzeigen lassen, steigen wir tiefer in den Kaninchenbau:

ffmpeg -i https://mcdn-one.akamaized.net/ardone/hls/master-1080p-5128/00132/master-1080p-5128_00691.ts

Das ergibt folgende Informationen zum Stream:

Stream #0:0[0x1e1]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 50 fps, 50 tbr, 90k tbn
Stream #0:1[0x1e2](deu): Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 127 kb/s

Nun nehmen wir diesen Stream für 1 Minute auf und speichern die Aufnahme in der Datei video1.mp4. Der Parameter -c:v copy sagt ffmpeg, einfach den Video-Stream zu kopieren (und nicht in ein anderes Format umzuwandeln), der Parameter -c:a copy weist ffmpeg an, genau so mit der Audio-Spur zu verfahren. Der Parameter -t 00:01:00 sagt ffmpeg, nur eine Minute des Streams aufzunehmen.

ffmpeg -i https://mcdn-one.akamaized.net/ardone/hls/master-1080p-5128/00132/master-1080p-5128_00691.ts -c:v copy -c:a copy -t 00:01:00 video0.mp4

Wir verfahren - zu testzwecken - ebenso mit einem weiteren Stream, z.B. den in niedrigerer Auflösung:

ffmpeg -i https://mcdn-one.akamaized.net/ardone/hls/master-540p-1728/00132/master-540p-1728_00692.ts -c:v copy -c:a copy -t 00:01:00 video1.mp4

Und holen uns den Audiostream der Originalsprache, wobei wir nur den Audio-Stream speichern:

ffmpeg -i https://mcdn-one.akamaized.net/ardone/hls/master-original/00132/master-original_00691.aac -c:a copy -t 00:01:00 tonspur2.aac

Als Ergebnis haben wir nun drei Dateien:

Mit diesen Dateien experimentieren wir nun weiter, um verschiedene Streams aus diesen Dateien neu zu mischen.

Stream-Nummerierung mit ffmpeg

Als erstes müssen wir uns damit befassen, wie ffmpeg Streams nummeriert. Das ist wichtig, denn das muss man verstehen, um Streams aus verschiedenen Dateien nachher so zusammen zu führen, wie man es möchte.

Eine Video-Datei besteht aus mehreren Streams, d.h. "Spuren". Normalerweise besteht ein Video aus einer Video- und Tonspur. Die Informationen über eine Videodatei kann man sich mit folgendem Befehl anzeigen lassen:

ffmpeg -i video0.mp4

Die wesentliche Ausgabe lautet z.B.:

Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 1284 kb/s, 50 fps, 50 tbr, 90k tbn (default)
Stream #0:1[0x2](deu): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s (default)

ffmpeg fängt immer an, mit 0 zu zählen. Daher ist:

ffmpeg kann mehrere Imput-Dateien verwenden. Wenn wir zwei Video-Dateien und eine Tondatei nehmen, lautet der Befehl:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac

Dieser Befehl wird alle Streams der drei Dateien anzeigen. Es ist wichtig, sich die Art zu merken, wie ffmpeg Streams nummeriert: die erste Ziffer ist die Nummer der Datei, die zweite Ziffer ist die Nummer der Streams, bei beiden fängt die Zählung mit 0 an:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video0.mp4':
Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 1284 kb/s, 50 fps, 50 tbr, 90k tbn (default)
Stream #0:1[0x2](deu): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s (default)
Input #1, mov,mp4,m4a,3gp,3g2,mj2, from 'video1.mp4':
Stream #1:0[0x1](und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 960x540 [SAR 1:1 DAR 16:9], 506 kb/s, 50 fps, 50 tbr, 90k tbn (default)
Stream #1:1[0x2](deu): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s (default)
Input #2, aac, from 'audio2.aac':
Stream #2:0: Audio: aac (LC), 48000 Hz, stereo, fltp, 127 kb/s

Diese Ausgabe sagt uns:

Damit haben wir gelernt, wie Streams nummeriert werden. Nun machen wir damit weiter, diese Streams in einer neuen Datei zu kombinieren.

Mapping mit ffmpeg

Unserem Befehl oben fügen wir nun ein Mapping und eine Ausgabe-Datei an. Wir möchten im neuen Video, das wir out.mp4 nennen, den Videostream und den Audiostream aus der Datei video0.mp4, den Audio-Stream aus video0.mp4 und den Audio-Stream tonspur2.aac verbinden. Das Ergebnis soll also eine Videodatei mit drei Tonspuren sein.

Wir verwenden also folgende Streams: #0:0 Video, #0:1 Audio 0, #1:1 Audio 1 und #2:0 Audio 2:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 out.mp4

Dieser Befehl funktionert, ist aber noch nicht optimal. Wir werden ihn in den folgenden kleinen Schritten noch optimieren.

Reihenfolge der Streams festlegen

Die Reihenfolge der Streams in out.mp4 wird durch die Reihenfolge der -map Parameter definiert. Soll also die Tonspur aus tonspur2.aac nachher die erste Tonspur sein, muss man die Reihenfolge der -map Parameter entsprechend ändern und diesen Stream nach vorne direkt hinter den Videostream stellen:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 2:0 -map 0:1 -map 1:1 out.mp4

Nun sind die Streams in der gewünschten Reihenfolge, allerdings wird das Video und die Tonspuren unter Umständen neu kodiert, weil die Quellformate nicht identisch mit den erwarteten Standard-Werten für die Ausgabe überein stimmen. Das wollen wir aber vermeiden, weil eine Re-Kodierung immer mit Qualitätsverlust verbunden ist.

Man muss wissen, dass nicht alle Container ("mp4" ist ein Container) mit allen Formaten von Video- oder Tonspuren kompatibel sind. Falls also ffmpeg eine entsprechende Fehlermeldung generiert, muss man neu kodieren. In unserem Beispiel sind h264 und aac mit mp4 kompatibel. Deswegen können wir auf eine Re-Kodierung verzichten.

Re-Kodierung vermeiden

Man muss ffmpeg mitteilen, dass die Streams kopiert, und nicht neu kodiert werden sollen. Das erreicht man, indem man die "Codecs" entsprechend festlegt. Ein "Codec" ist vereinfacht gesagt das Programm, dass ein bestimmtes Video- oder Tonformat kodiert. Für den Zweck, einfach zu kopieren, hat ffmpeg den Codec copy. Mit -c:v legt man den Codec für Video fest, mit -c:a den Codec für Audio.

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a copy out.mp4

Damit werden die Streams ohne eine Re-Kodierung zusammengeführt, was sehr schnell geht.

Einzelne Streams gezielt neu kodieren

Die eingefügten Parameter -c:v copy -c:a copy bewirken, dass alle Video- und Audiostreams kopiert werden. Folgender Befehl bewirkt das Gleiche, sagt aber ffmpeg ganz genau, was es für jeden Stream einzeln machen muss:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a:0 copy -c:a:1 copy -c:a:2 copy out.mp4

Die Notierung -c:a:0 copy sagt, dass der Codec für Ton für den Stream 0 copy ist. Die Nummerierung der Ausgabe-Streams beginnt wieder mit 0.

Soll nun der Stream aus der Audiodatei tonspur2.aac in mp3 umkodiert werden, ändern wir entsprechend den Codec für den Stream Nummer 2, wobei der Video-Stream und die anderen beiden Audio-Streams wieder nur kopiert werden:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a:0 copy -c:a:1 copy -c:a:2 libmp3lame out.mp4

Im Ergebnis haben wir dann die zugegebenermaßen etwas exotische Datei, die einen h264-Videostream, zwei aac Audiostreams und einen mp3 Audiostream hat.

Stream-Sprache benennen

Nun stellen wir uns vor, unser Beispiel ist wie folgt: video0.mp4 ist ein Film mit deutscher Tonspur, video1.mp4 der gleiche Film mit englischer Tonspur und tonspur2.mp3 eine (z.B. bereits extrahierte) spanische Tonspur. Unser Beispiel oben hat bereits Video und Ton ganz gut zusammengeführt, allerdings müssen wir beim Abspielen immer raten, weil out.mp4 keine Informationen enthält, welcher Audio-Stream welche Sprache ist. Diese Information können wir natürlich mit ffmpeg ergänzen:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a:0 copy -c:a:1 copy -c:a:2 copy -metadata:s:a:0 language=deu -metadata:s:a:1 language=eng -metadata:s:a:2 language=spa out.mp4

Die Sprachkürzel sind dreistellig nach ISO 639-2.

Standard Audio-Stream festlegen

Der Befehl ffmpeg -i out.mp4 gibt uns nun folgende Information:

Stream #0:1[0x2](deu): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s (default)
Stream #0:2[0x3](eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 126 kb/s (default)
Stream #0:3[0x4](spa): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s

Die Datei out.mp4 hat noch den Fehler, dass zwei Audio-Streams als default gesetzt sind. Normalerweise sollte es einer sein, und nicht mehrere. Der Grund ist, dass dieser Wert ("disposition") immer aus der Quelldatei kopiert wird, falls er dort gesetzt ist und wenn man ihn nicht ändert. Falls keiner der Audio-Sreams diesen Wert gesetzt hat, wird immer der erste Audio-Stream als "default" gesetzt. Wir erinnern uns, die Audio-Streams #0 und #1 stammen aus einzelnen Video-Dateien, dort war es logisch, dass sie die Standard-Audio-Streams waren. Mit folgenden Parametern legen wir zusätzlich die Bedeutung der Audio-Streams manuell fest:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a:0 copy -c:a:1 copy -c:a:2 copy -metadata:s:a:0 language=deu -metadata:s:a:1 language=eng -metadata:s:a:2 language=spa -disposition:1 default -disposition:2 0 -disposition:3 0 out.mp4

Mit der "0" im Parameter -disposition:2 0 löscht man den dort ggf. gesetzten Wert.

Das Ergebnis von ffmpeg -i out.mp4 ist nun, dass es nur einen Default-Audio-Stream gibt, so wie es sein sollte:

Stream #0:1[0x2](deu): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s (default)
Stream #0:2[0x3](eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 126 kb/s
Stream #0:3[0x4](spa): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 125 kb/s

Länge vereinheitlichen

In unserem Beispiel haben alle drei Streams die gleiche Länge. Es kann aber sein, dass z.B. ein Stream kürzer ist als die anderen. Teilweise können solche Unterschiede auch minimal sein. Falls man nun ffmpeg anweisen möchte, die Ausgabe-Datei zu beenden, sobald der erste Stream endet, fügt man als Parameter am Ende -shortest ein. Dieser Paramter muss sich direkt vor der Ausgabe-Datei befinden:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a:0 copy -c:a:1 copy -c:a:2 copy -metadata:s:a:0 language=deu -metadata:s:a:1 language=eng -metadata:s:a:2 language=spa -disposition:1 default -disposition:2 0 -disposition:3 0 -shortest out.mp4

Nun haben wir eine Video-Datei, bei der alle Streams die identische Länge haben.

Das moov atom bewegen

Das sogenannte moov atom enthält wichtige Informationen für den Player, um das Video korrekt abzuspielen. Wenn dieser Datenblock fehlerhaft ist, spielt ein Video nicht ab. Ebenso kann - wenn man ein Video z.B. übers Netz streamen will - es hinderlich sein, wenn dieser Datenblock sich am Ende der Videodatei befindet, was übrigens das Standardverhalten von ffmpeg ist. In diesem Fall muss die Videodatei einmal komplett eingelesen werden, bevor sie abspielt. Beim Streaming würde das eine unnötige Latenz verursachen und es möglicherweise lange dauern, bis das Video startet. Kurz gesagt macht es Sinn, dieses moov atom an den Anfang der Videodatei zu schieben. Das erreicht man wie folgt:

ffmpeg -i video0.mp4 -i video1.mp4 -i tonspur2.aac -map 0:0 -map 0:1 -map 1:1 -map 2:0 -c:v copy -c:a:0 copy -c:a:1 copy -c:a:2 copy -metadata:s:a:0 language=deu -metadata:s:a:1 language=eng -metadata:s:a:2 language=spa -disposition:1 default -disposition:2 0 -disposition:3 0 -movflags faststart -shortest out.mp4

Ffmpeg teilt es uns dann auch am Ende mit:

Starting second pass: moving the moov atom to the beginning of the file

Das Ende

Nun ist es ein langer ffmpeg-Befehl geworden, den dieser Text Stück für Stück erarbeitet hat. Das Ergebnis ist eine perfekte Videodatei mit den erwünschten Streams.

Kodierung für kleine Dateigröße

Falls der Festplatten-Speicher knapp wird, kann man noch sehr rechenintensiv den Videostream von h264 auf h265 umkodieren und die Audio-Kodierung vereinheitlichen. Die Ersparnis kann bei ca. 60% liegen, je nach Video und Ton.

ffmpeg -i out.mp4 -c:v libx265 -crf 28 -preset slow -c:a aac -q:a 2 out2.mp4

Falls man über eine ffmpeg-Version verfügt, die den Fraunhofer FDK acc (libfdk_acc) Encoder beinhaltet, liefert dieser deutlich bessere Ergebnisse:

ffmpeg -i out.mp4 -c:v libx265 -crf 28 -preset slow -c:a libfdk_aac -vbr 4 out2.mp4

In vielen Distributionen ist dieser Encoder nicht enthalten, ggf. muss man sich hierfür ffmpeg selbst kompilieren. Das ist aber eine andere Geschichte.