30 | 04 | 2025

Die Technik

Die FFMPEG library

Das Utility ffmpeg.

FFMpeg bietet mehr als nur eine C-Library. Möchte man schnell etwas transcodieren, neu kodieren, muxen, etc., dann muss nicht immer gleich ein neues Tool entwicklt werden. FFMpeg bringt hierfür unter dem gleichen Namen ein hervorragendes Tool mit. Der Hacken - es ist sehr komplex, deswegen ein kleines Beispiel:

Angenommen wir haben einen Film in den demuxten Dateien film.m2v und film.mp2 vorliegen. Möchte man einfach nur den Film muxen reicht ein:

ffmpeg -i film.m2v -i film.mp2 -f mpeg -vcodec copy -acodec copy film.mpg

Mit dieser Grundlage kann man nun experimentieren. Will man einen Film etwa transcodieren, dann müssen die codecs angepasst werden. Welche es überhaupt auf Ihrem System gibt erfahren Sie, wenn Sie:

ffmpeg -formats

eingeben. Aufpassen! Als erstes werden die Containerformate aufgelistet, dann erst alle Codecs (leider Audio und Video gemischt => also in Ruhe durchforsten, welche Codecs für Sie in Fage kommen!). Will man einen Film zum Beispiel mit Hilfe des neuen H.264-Codecs transcodieren, dann hilft hier:

ffmpeg -i film.m2v -i film.mp2 -f mpeg -vcodec libx264 -acodec copy film.mpg

Besser natürlich, man wechselt auch das Containerformat. MPG-Programmströme funktionieren zwar auch mit MPEG-4 Inhalten, nur kommen einige Player damit nicht klar. Dementsprechend muss das Containerformat auch noch angepasst werden:

ffmpeg -i film.m2v -i film.mp2 -f matroska -vcodec libx264 -acodec copy film.mkv

Und das Resultat ist ein Matroska-File mit H.264 Video und MP2-Audio.

Viel Spass mit Ihrer Sammlung

Notwendige Kentnisse im Umgang mit Binärdaten

Im Unterschied zu Textdateien lassen sich Binärdateien nicht ohne weiteres Parsen, in dem man Strings vergleicht. Binärdateien kann man nur unter Verwendung von bekannten Adressen auswerten. Die größte Schwierigkeit taucht im Umgang mit Binärdateien genau dann auf, wenn sich die gesuchten Informationen nicht in einem Byte, sondern einem der Bits verstecken.

Beispiel:

Ein Datenstrom der Folge 0111011101111001111111010101011011111111 wird beim auslesen Byteweise eingelesen, wie in folgender Abbildung dargestellt.

Byteweises auslesen einer Datei

Die folgende Abbildung wird vielleicht durch das Folgende Codefragment noch verständlicher: 

fread(buffer, size, 1, mFileP);

Das Beispiel zeigt, wie Binärdaten der Größe size aus einer Datei gelesen werden. Greife ich z.B. mit buffer[0] auf den Buffer buffer zu, dann enthält buffer[0] die obige Zeile 01110111 und buffer[2] enthält analog 11111101.

Möchte ich jetzt an eine Information herankommen, welche sich im 3. Byte an 2. Stelle befindet und 3 Bits lang ist, dann benötige ich die sogenannten Bitshift-Operatoren. Nehmen wir ferner an, an dieser besagten Stelle befindet sich eine Integer Zahl. Der Ermittlung dieser Zahl erfolgt mit:

int zahl = (buffer[2] & 0x38) >> 3;

Das sieht zugegebener Maßen sehr schwierig und kompliziert aus, ist aber nachvollziehbar. Zunächst wird der Ausdruck in der Klammer ausgewertet. Dort steht eine Und-Verknüpfung zwischen dem Buffer und der 0x38 drin. Bei der 0x38 handelt es sich um eine sogenannte Bitmaske (0x38 entspricht dem Binärcode 0011 1000). Die Verknüpfung die dort ausgeführt wird kann mit folgender Grafik verdeutlicht werden:

Die UND-Verknüpfung

Die & (UND)-Verknüpfung wird bitweise durchgeführt. Der Grund ist einfach. Wir wollen dabei alle unnötigen Informationen verlieren. Uns interessieren nur die Informationen ab dem 3. Bit. Wir sind jetzt aber noch nicht ganz fertig. Um den korrekten Wert zu erfahren, müssen wir noch diese Information drei Bits nach rechts verschieben. Das geschieht mit Hilfe der >> 3. Mathematisch gesehen ist das Shiften einer Zahl gleichbedeutend mit der binären Division durch 2. Für unsere zukünftige Vorstellung reicht es zu wissen, dass wir bei solchen Operationen die Informationen nach rechts, bzw. analog bei "<<" nach links verschieben. Den Grund entnimmt man dabei immer den entsprechenden Dokumenten wie z.B. der ISO13818-1. Dort ist immer genau definiert, an welchen Positionen der vielen Bits und Bytes, die wirklich wichtigen Infos stecken.

Ein letztes Beispiel zur Festigung:

Ziel ist die Ermittlung der PID in einem Transport-Stream Header. Dafür werfen wir ein Blick in die ISO-13818-1 hinein und finden folgenden Pseudo-Code:

Ermittlung der PID
Die Mnemonics (Abkürzungen) verraten uns, an welchen Stellen des Bitstroms wir uns "einhacken müssen". Dargestellt ist hier der typische Aufbau eines Transport-Stream-Paketes. Das Sync Byte belegt, wie der Name es bereits nahe legt, ein Byte (8 Bit). Danach folgen mit einem Bit der transport_error_indicator, der payload_unit_start_indicator und die transport_priority. Anschließend gibt es 13 Bits. Das Kürzel uimsbf bedeutet unsigned integer most significant bit first. Es handelt sich demnach um einen ganzzahlig positiven Integerwert, die größeren Potenzen stehen vorn (typische Network/Big Endian Byte Order).

Nehmen wir an, wir haben einen Puffer buffer, und an 0. Stelle steht der Headeranfang eines Transportstream-Paketes. Die Pid selbst wollen wir in einer int-Variable speichern. Wir definieren:

int pid = ((buffer[1] & 0x1F) << 8) | buffer[2];

Die Erklärung ist denke ich klar? Ganz einfach. Die ersten 3 Bits des Buffers buffer[1] sind für die PID uninteressant. Wir ignorieren diese in dem wir die Bitmaske 0x1F (0001 1111) benutzen. Bis dahin haben wir jedoch nur die ersten fünf Bits ausgelesen. Acht fehlen noch. Schaffen wir also Platz, in dem wir die bisherige Zahl acht Stellen nach links shiften. Anschließend wird einfach per ODER (|)-Verknüpfung der Rest des Buffers buffer[2] angefügt.

Fertig!

PS:

Alle Shiftoperationen im gesamten Projekt beruhen auf diesen Überlegungen. Auch in anderen Hardware nah programmierten Projekten finden sich diese Muster wieder.

Die Schwierigkeiten bei der Erstellung einer solchen Applikation

Wenn ich mich auf die Erstellung der Schnittapplikation DreamDVD beschränke, so lassen sich folgende Punkte anführen, welche ein Entwickler einer solchen Applikation zu knacken hat:

  1. Wie parse ich einen Transport-Stream um an die Videodaten heranzukommen?
  2. Welche Informationen sind für mich überhaupt interessant?
  3. Wie funktioniert die Synchronisierung (Video+Audio) in einem solchen Stream?
  4. Wie erstelle ich ein neues Containerformat (MPG-Datei/Programmstream)?
  5. Ausblick (kommende Standards)
Diese Probleme gab es für mich zu lösen. Auf den folgenden Seiten möchte ich Punkt für Punkt durchgehen und Ihnen anhand meiner Quelltexte Lösungsmöglichkeiten aufzeigen.

Die Informationen des Transport-Streams und das Auslesen dieser Informationen

Der allgemeine Aufbau eines Transport Streams

Bevor ich zur Lösungsimplementation komme, möchte ich Ihnen hier erläutern, wie ein solcher Transport Stream technisch aufgebaut ist.

Der Transportstream vereinfacht

Vereinfacht kann man sagen, handelt es sich hier um eine Datei, die aus lauter Paketen besteht. Jedes Paket ist genau 188 Bytes groß wobei die ersten 4 Byte für den Header (den Kopf) reserviert sind. Darin enthalten sind

  1. Das Sync Byte (0x47)
  2. Die Programm PID
  3. Der Coninuity Counter

Das Sync Byte 0x47 (0x steht für 47 im Hexadezimalsystem und die Schreibweise ist in C/C++ gebräuchlich) taucht alle 188 Bytes auf. Mit Hilfe des Sync Bytes bin ich in der Lage, Anfang und Ende eines Paketes zu erkennen. Sobald ich während des Parsens merke, dass nach den 188 Byte noch kein neues Sync-Byte auftaucht, ist während der Übertragung etwas schief gelaufen.

Die Programm PID benötige ich, um die verschiedenen Pakete zuzuordnen. In der Praxis folgen die Video-, Audio- und sonstigen Daten keinem gewöhnlichem Muster, sondern tauchen in unterschiedlicher Reihenfolge auf (na gut, ein bisschen System gibt es da schon).

Der Continuity Counter existiert wegen der Fehlererkennung. Hieran kann man erkennen, ob Pakete doppelt gesendet wurden, z.B. wenn ein Übertragungsfehler vom System erkannt wurde.

Ich möchte an dieser Stelle noch die ISO13818-1 erwähnen, in der man den ganz genauen Aufbau einer solchen Datei nocheinmal nachlesen kann. Zu beziehen ist diese Datei in meiner Download-Section. Für die weiteren Ausführungen wird sie nicht benötig, da ich auf die wichtigsten Details hier eingehen werde.

Das Öffnen eines Videos in DreamDVD

Mein Programm benutzt eine Hilfsbibliothek, die libdigitaltv (früher: libtsstream), eine Bibliothek die ich für den gesamten Backendprozess entwickelt habe. Diese Bibliothek enthält vordefinierte Klassen, die sich mit dem Parsen, Demultiplexen und Multiplexen beschäftigen.

Zunächst wird eine Instanz der Klasse CTSFile erstellt. Sie übernimmt die Verwaltung über die TS-Dateien. Das war notwendig, weil die Dreambox in der Lage ist, nach einer bestimmten Anzahl Gigabytes die Dateien zu splitten. Möchte man den gesamten Film schneiden, muß man genau wissen, wie diese einzelnen Dateien zusammengefügt werden.

Danach benötigt man eine Instanz der Klasse CTSParser. Diese Instanz übernimmt die Aufgabe des Parsens einer Datei. Später verwendet man auch diese Instanz zum extrahieren bestimmter Video- und Audiodaten.

Sobald die Methode parse() aufgerufen wird, beginnt der eigentliche Parsingprozess. Dieser Methode werden auch noch Parameter übergeben. Diese dienen lediglich der Informationsbereitstellung, damit man später in einem Informationsfenster nachvollziehen kann, an welcher Stelle das Programm gerade steht. Der folgende Quellcodeausschnitt zeigt den Anfang der Methode parse().

CIndexFile* idf = new CIndexFile(mTSFiles);
if (idf->fileExists())
{

    info += "TSParser: Index-Datei gefunden! Verwende diese ...\n";
    idf->createTracesFromIndexFile(mTraces);
    // MPEG2-Parser aktualisieren
    CTSVideoTrace* trace = this->GetVideoTrace();
    trace->setIndexFile(idf);
    return;
} 

Hier wird zunächst überprüft, ob es eventuell eine Indexdatei gibt. Diese Datei wird gleich im eigentlichen Parse-Vorgang angelegt, und enthält die Adressen der möglichen Schnittstellen. Der Parsevorgang wird eingeleitet mit:

int blength = 16384*1024;
uint8_t* buffer = (uint8_t*) malloc(blength + 1);
if (buffer)
{
    memset(buffer, '\0', blength + 1);
    mTSFiles->read(buffer, blength);
    int pstart = getPackStart(buffer, 0, blength);

    // Buffer parsen
    int offset = pstart;
    bool parseStop = false;
    while (offset < blength || !parseStop)
    {
    if (parseStop) break;
    this->parseTSPacket(buffer, offset, parseStop, blength, info);
    offset = offset + 188;
    }
    ...
}

Die Methode getPackStart(...) stellt immer sicher, dass wir wirklich am Beginn eines Transport Streams stehen. Dafür guckt sich diese Methode die aufeinanderfolgenden fünf Pakete an und prüft, ob diese jeweils mit einem Sync Byte beginnen. Bei erfolgreicher Ausführung liefert Sie die Adresse des nächstfolgenden Paketstarts zurück. Der Vorteil ist, ich kann an eine beliebige Stelle in Stream springen, ich bekomme immer den nächstmöglichen Paket-Start als Adresse zurückgeliefert.

Die nächste interessante Methode ist die Methode parseTSPacket(Puffer, Offset, parseStop, blength, info). Puffer enthält unsere aus der Datei geladenen Binärdaten, das Offset die Stelle an der ich gerade in der Datei stehe, parseStop einer booleschen Variable mit der ich den ganzen Vorgang stoppen kann und blength, einer Integerzahl welche die Größe des Puffers enthält.

Wie sie die Codezeilen lesen müssen, erfahren Sie in meinen Vorbemerkungen. Die Methode parseTSPacket(...) liest ermittelt jedenfalls die PID. Anschließend wird darin überprüft, ob schon die entsprechenden Video- und Audio-PIDs des aufgenommenen Senders identifiziert wurden. Wenn nicht, dann wird sobald ein Paket mit der PID 0 gefunden wird, die Methode parsePAT aufgerufen.

PAT steht für Program Association Table. In dieser Tabelle befinden sich die Informationen über die betreffenden PMTs (Program Map Tables). Und eben diese PMT's müssen wir auslesen, um zu wissen welche Audio- und welche Video-PID's existieren im Stream.

Die Methode parsePAT untersucht jetzt das betreffende Paket nach den verschiedenen PMT-PIDs. Alle gefundenen PMT's werden jetzt in einer Liste gespeichert. Im nächsten Schritt wird mit Hilfe der Methode identifyPMT, diese eben erstellte Liste ausgewertet und dir richtige hier gültige PMT-PID zugeordnet. Ich habe mich für diesen Weg entschieden, weil ich weis, dass in meinen Dreambox-TS-Dateien eigentlich nur eine PMT existiert. In der PAT stehen natürlich mehrere drin, weil die Dreambox diese bei der Aufnahme unbearbeitet übernimmt, obwohl sie nur ein Programm aufnimmt.

 

 

Was ist ein Transport Stream?

Beschäftigt man sich mit der Bedeutung des Begriffes, so läßt sich daraus ableiten, dass es sich um einen Datenstrom handelt, mit dem etwas transportiert wird. Letztlich ist es auch nicht mehr und nicht weniger. In der Literatur findet man in diesem Zusammenhang des öfteren auch den Begriff Containerformat. 

Ein Containerformat kann man sich vorstellen wie ein langer Zug mit Containern, in denen Waren, hier Informationen verpackt werden. Die Informationen des Transport Streams sind in der Regel das Videomaterial und Audiomaterial der Fernsehstationen, welche unseren Receiver erreichen.

Wenn wir mit Hilfe unseres digitalen Receivers diese Ströme direkt aufzeichnen, so werden diese in Form einer .ts-Datei abgelegt. 

Weitere Beiträge...

  1. Die Technik