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.
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.
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:
#0:0
Input-Datei #0, Stream #0#0:1
Input-Datei #0, Stream #1ffmpeg 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.
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.
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.
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.
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.
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.
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
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 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
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.
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.