FFmpeg libav Handbuch


Lange suchte ich nach einem Buch, das gekaut werden sollte, um eine FFmpeg- ähnliche Bibliothek namens libav zu verwenden (der Name steht für lib rary a udio v ideo ). Fand ein Lehrbuch " Wie man einen Videoplayer schreibt und in weniger als tausend Zeilen passt ." Leider sind die Informationen dort veraltet, so dass ich selbst ein Handbuch erstellen musste.

Der größte Teil des Codes befindet sich in C, aber keine Sorge: Sie werden alles leicht verstehen und können es in Ihrer Lieblingssprache anwenden. FFmpeg libav hat viele Bindungen zu vielen Sprachen (einschließlich Python und Go). Aber auch wenn Ihre Sprache nicht direkt kompatibel ist, können Sie dennoch über ffi angehängt werden (hier ein Beispiel mitLua ).

Beginnen wir mit einem kurzen Exkurs über Video, Audio, Codecs und Container. Dann fahren wir mit der FFmpeg-Befehlszeile mit dem Crashkurs fort und schreiben schließlich den Code. Gehen Sie direkt zum Abschnitt „Der dornige Weg zum Lernen von FFmpeg libav“.

Es gibt eine Meinung (und nicht nur meine), dass das Streamen von Internetvideos bereits den Taktstock des traditionellen Fernsehens übernommen hat. Wie dem auch sei, FFmpeg libav ist definitiv eine Erkundung wert.

Inhaltsverzeichnis


EDISON Software - Webentwicklung
EDISON.

, , .

! ;-)


— , !


Wenn die Bildfolge mit einer bestimmten Frequenz geändert wird (z. B. 24 Bilder pro Sekunde), entsteht eine Bewegungsillusion. Dies ist die Hauptidee des Videos: eine Reihe von Bildern (Frames), die sich mit einer bestimmten Geschwindigkeit bewegen.

1886 Abbildung.

Audio ist was Sie hören!


Obwohl stille Videos eine Vielzahl von Gefühlen hervorrufen können, erhöht das Hinzufügen von Ton den Grad des Vergnügens dramatisch.

Schall sind Schwingungswellen, die sich in Luft oder einem anderen Übertragungsmedium (wie Gas, Flüssigkeit oder Feststoff) ausbreiten.

In einem digitalen Audiosystem wandelt ein Mikrofon Ton in ein analoges elektrisches Signal um. Dann wandelt ein Analog -Digital-Wandler ( ADC ) - normalerweise unter Verwendung von Pulscodemodulation ( PCM ) - ein analoges Signal in ein digitales um.


Codec - Datenkomprimierung


Ein Codec ist eine elektronische Schaltung oder Software, die digitales Audio / Video komprimiert oder dekomprimiert. Es konvertiert rohes (unkomprimiertes) digitales Audio / Video in ein komprimiertes Format (oder umgekehrt).

Wenn wir uns jedoch dazu entschließen, Millionen von Bildern in eine Datei zu packen und sie als Film zu bezeichnen, können wir eine riesige Datei erhalten. Berechnen

wir : Nehmen wir an, wir erstellen ein Video mit einer Auflösung von 1080 × 1920 (Höhe × Breite). Wir geben 3 Bytes pro Pixel (der Mindestpunkt auf dem Bildschirm) für die Farbcodierung aus (24-Bit-Farbe, wodurch wir 16.777.216 verschiedene Farben erhalten). Dieses Video arbeitet mit einer Geschwindigkeit von 24 Bildern pro Sekunde und einer Gesamtdauer von 30 Minuten.

toppf = 1080 * 1920 //    
cpp = 3 //  
tis = 30 * 60 //   
fps = 24 //   

required_storage = tis * fps * toppf * cpp

Dieses Video benötigt ungefähr 250,28 GB Speicher oder 1,11 Gbit / s! Deshalb müssen Sie einen Codec verwenden.

Ein Container ist eine bequeme Möglichkeit, Audio / Video zu speichern


Das Containerformat (Wrapper) ist ein Metadateiformat, dessen Spezifikation beschreibt, wie verschiedene Daten- und Metadatenelemente in einer Computerdatei nebeneinander existieren.

Dies ist eine einzelne Datei, die alle Streams (hauptsächlich Audio und Video) enthält, die Synchronisation ermöglicht und allgemeine Metadaten (wie Titel, Auflösung) usw. enthält.

In der Regel wird das Dateiformat durch seine Erweiterung bestimmt: Beispielsweise ist video.webm höchstwahrscheinlich ein Video, das den webm-Container verwendet.


Befehlszeile FFmpeg


Unabhängige plattformübergreifende Lösung zum Aufzeichnen, Konvertieren und Streamen von Audio / Video.

Für die Arbeit mit Multimedia haben wir ein erstaunliches Tool - eine Bibliothek namens FFmpeg . Auch wenn Sie es nicht in Ihrem Programmcode verwenden, verwenden Sie es dennoch (verwenden Sie Chrome?).

Die Bibliothek verfügt über ein Konsolenprogramm zur Eingabe einer Befehlszeile namens ffmpeg (in kleinen Buchstaben im Gegensatz zum Namen der Bibliothek selbst). Dies ist eine einfache und leistungsstarke Binärdatei. Sie können beispielsweise von mp4 nach avi konvertieren, indem Sie einfach diesen Befehl eingeben:

$ ffmpeg -i input.mp4 output.avi

Wir haben gerade neu gemischt - von einem Container in einen anderen konvertiert. Technisch kann FFmpeg auch transkodieren, aber dazu später mehr.

Befehlszeilenprogramm FFmpeg 101


FFmpeg hat eine Dokumentation, in der alles perfekt erklärt wird, wie was funktioniert.

Schematisch erwartet das FFmpeg-Befehlszeilenprogramm, dass das folgende Argumentformat seine Aufgabe erfüllt - ffmpeg {1} {2} -i {3} {4} {5}wobei:

{1} - globale Parameter
{2} - Parameter der Eingabedatei
{3} - eingehende URL
{4} - Parameter der Ausgabedatei
{5} - ausgehend Die URL-

Teile {2}, {3}, {4}, {5} geben so viele Argumente wie nötig an. Das Format der Übergabe von Argumenten lässt sich anhand eines Beispiels leichter verstehen:

WARNUNG: Eine Referenzdatei wiegt 300 MB

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ #  
-c: libfdk_aac -c: v libx264 \ #  
-i bunny_1080p_60fps.mp4 \ #  URL
-c: v libvpx-vp9 -c: libvorbis \ #  
bunny_1080p_60fps_vp9.webm #  URL

Dieser Befehl nimmt eine eingehende mp4-Datei mit zwei Streams (Audio, das mit dem aac-Codec codiert wurde, und Video, das mit dem h264-Codec codiert wurde) und konvertiert sie in webm, wobei auch die Audio- und Videocodecs geändert werden.

Wenn Sie den obigen Befehl vereinfachen, sollten Sie berücksichtigen, dass FFmpeg anstelle von Ihnen die Standardwerte akzeptiert. Zum Beispiel, wenn Sie einfach eingeben

ffmpeg -i input.avi output.mp4

Welchen Audio- / Video-Codec wird zum Erstellen von output.mp4 verwendet?

Werner Robitz schrieb eine Codierung und Bearbeitung Leitfaden zum Lesen / Ausführen mit FFmpeg.

Grundlegende Videobetriebe


Bei der Arbeit mit Audio / Video führen wir normalerweise eine Reihe von Aufgaben im Zusammenhang mit Multimedia aus.

Transcodierung (Transcodierung)




Was ist das? Der Prozess des Konvertierens von Streaming oder Audio oder Video (oder von beiden gleichzeitig) von einem Codec in einen anderen. Das Dateiformat (Container) ändert sich nicht.

Wofür? Es kommt vor, dass einige Geräte (Fernseher, Smartphones, Konsolen usw.) das Audio- / Videoformat X nicht unterstützen, jedoch das Audio- / Videoformat Y. Oder neuere Codecs sind vorzuziehen, da sie ein besseres Komprimierungsverhältnis bieten.

Wie? Konvertieren Sie beispielsweise Video H264 (AVC) in H265 (HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4

Transmultiplexing



Was ist das? Konvertieren Sie von einem Format (Container) in ein anderes.

Wofür? Es kommt vor, dass einige Geräte (Fernseher, Smartphones, Konsolen usw.) das X-Dateiformat nicht unterstützen, aber das Y-Dateiformat. Oder neuere Container bieten im Gegensatz zu älteren die modernen erforderlichen Funktionen.

Wie? Konvertieren Sie eine mp4 in webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm

Übertragen



Was ist das? Ändern Sie die Datenrate oder erstellen Sie eine andere Ansicht.

Wofür? Der Benutzer kann Ihr Video sowohl in einem 2G-Netzwerk auf einem Smartphone mit geringem Stromverbrauch als auch über eine Glasfaser-Internetverbindung auf einem 4K-Fernseher ansehen. Daher sollten Sie mehr als eine Option zum Abspielen desselben Videos mit unterschiedlichen Datenraten anbieten.

Wie? erzeugt eine Wiedergabe mit einer Bitrate zwischen 3856K und 2000K.

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4

Typischerweise erfolgt die Transrating in Verbindung mit einer Neukalibrierung. Werner Robitz schrieb einen weiteren obligatorischen Artikel über Geschwindigkeitsregelung FFmpeg.

Transizing (Neukalibrierung)



Was ist das? Auflösungsänderung. Wie oben angegeben, wird das Transsizing häufig gleichzeitig mit dem Transrating durchgeführt.

Wofür? Aus den gleichen Gründen wie beim Übertragen.

Wie? Reduzieren Sie die Auflösung von 1080 auf 480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4

Bonus: adaptives Streaming



Was ist das? Erstellung vieler Berechtigungen (Bitraten) und Aufteilung der Medien in Teile und deren Übertragung über das http-Protokoll.

Wofür? Um flexible Multimedia-Inhalte bereitzustellen, die auch auf einem preisgünstigen Smartphone oder sogar auf einem 4K-Plasma angezeigt werden können, kann es einfach skaliert und bereitgestellt werden (dies kann jedoch zu Verzögerungen führen).

Wie? Erstellen Sie ein reaktionsfähiges WebM mit DASH:

# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the DASH manifest
$ ffmpeg \
 -f webm_dash_manifest -i video_160x90_250k.webm \
 -f webm_dash_manifest -i video_320x180_500k.webm \
 -f webm_dash_manifest -i video_640x360_750k.webm \
 -f webm_dash_manifest -i video_640x360_1000k.webm \
 -f webm_dash_manifest -i video_1280x720_500k.webm \
 -f webm_dash_manifest -i audio_128k.webm \
 -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
 -f webm_dash_manifest \
 -adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
 manifest.mpd

PS: Ich habe dieses Beispiel aus den Anweisungen zum Spielen von Adaptive WebM mit DASH gezogen .

Über hinausgehen


Es gibt keine anderen Verwendungen für FFmpeg. Ich benutze es mit iMovie , um einige YouTube-Videos zu erstellen / bearbeiten. Und natürlich hindert Sie nichts daran, es professionell einzusetzen.

Der dornige Weg, FFmpeg libav zu lernen

Ist es nicht von Zeit zu Zeit erstaunlich, was durch Hören und Sehen wahrgenommen wird?

Biologe David Robert Jones
FFmpeg ist äußerst nützlich als Befehlszeilentool zum Ausführen wichtiger Vorgänge mit Multimediadateien. Vielleicht kann es auch in Programmen verwendet werden?

FFmpeg besteht aus mehreren Bibliotheken, die in unsere eigenen Programme integriert werden können. Wenn Sie FFmpeg installieren, werden normalerweise alle diese Bibliotheken automatisch installiert. Ich werde eine Reihe dieser Bibliotheken als FFmpeg libav bezeichnen .

Der Titel des Abschnitts ist eine Hommage an Zed Shaws Serie The Thorny Path of Learning [...] , insbesondere an sein Buch The Thorny Path of Learning C.

Kapitel 0 - Die einfache Hallo Welt


In unserer Hello World werden Sie die Welt in Konsolensprache nicht wirklich begrüßen. Drucken Sie stattdessen die folgenden Informationen zum Video aus: Format (Container), Dauer, Auflösung, Audiokanäle, und entschlüsseln Sie schließlich einige Frames und speichern Sie sie als Bilddateien.

FFmpeg libav Architektur


Bevor wir jedoch mit dem Schreiben des Codes beginnen, wollen wir uns ansehen, wie die FFmpeg libav-Architektur im Allgemeinen funktioniert und wie ihre Komponenten mit anderen interagieren.

Hier ist ein Diagramm des Videodecodierungsprozesses:

Zunächst wird die Mediendatei in eine Komponente namens AVFormatContext geladen (der Videocontainer ist auch ein Format). Tatsächlich wird nicht die gesamte Datei vollständig heruntergeladen: Oft wird nur der Header gelesen.

Sobald Sie den Mindestheader unseres Containers heruntergeladen haben , können Sie auf dessen Streams zugreifen (diese können als elementare Audio- und Videodaten dargestellt werden). Jeder Stream ist in der AVStream- Komponente verfügbar .

Angenommen, unser Video verfügt über zwei Streams: Audio, das mit dem AAC- Codec codiert wurde, und Video, das mit dem H264-Codec ( AVC ) codiert wurde . Aus jedem Stream können wir Daten extrahieren, die als Pakete bezeichnet werdendie in Komponenten namens AVPacket geladen werden .

Die Daten in den Paketen sind immer noch codiert (komprimiert) . Um die Pakete zu decodieren, müssen sie an einen bestimmten AVCodec übergeben werden .

AVCodec decodiert sie in einen AVFrame , wodurch diese Komponente einen unkomprimierten Frame ergibt. Beachten Sie, dass die Terminologie und der Prozess für Audio- und Videostreams gleich sind.

Anforderungen


Da es manchmal Probleme beim Kompilieren oder Ausführen von Beispielen gibt, verwenden wir Docker als Entwicklungs- / Laufzeitumgebung. Wir werden auch ein Video mit einem großen Kaninchen verwenden . Wenn Sie es also nicht auf Ihrem lokalen Computer haben, führen Sie einfach den Befehl make fetch_small_bunny_video in der Konsole aus .

Eigentlich ist der Code


TLDR zeig mir ein Beispiel für ausführbaren Code, Bruder:

$ make run_hello

Wir werden einige Details weglassen, aber keine Sorge: Der Quellcode ist auf github verfügbar .

Wir werden Speicher für die AVFormatContext- Komponente zuweisen , die Informationen über das Format (Container) enthält.

AVFormatContext *pFormatContext = avformat_alloc_context();

Jetzt öffnen wir die Datei, lesen den Header und füllen AVFormatContext mit minimalen Formatinformationen (beachten Sie, dass Codecs normalerweise nicht geöffnet werden). Verwenden Sie dazu die Funktion avformat_open_input . Es werden AVFormatContext , ein Dateiname und zwei optionale Argumente erwartet : AVInputFormat (wenn Sie NULL übergeben, bestimmt FFmpeg das Format) und AVDictionary (Demultiplexer-Optionen).

avformat_open_input(&pFormatContext, filename, NULL, NULL);

Sie können auch den Namen des Formats und die Dauer des Mediums drucken:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);

Um auf die Streams zugreifen zu können, müssen wir die Daten von den Medien lesen. Dies erfolgt über die Funktion avformat_find_stream_info . Jetzt enthält pFormatContext-> nb_streams die Anzahl der Threads, und pFormatContext-> Streams [i] geben uns den i- ten Fluss in einer Reihe ( AVStream ).

avformat_find_stream_info(pFormatContext,  NULL);

Lassen Sie uns die Schleife in allen Threads durchgehen:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}

Für jeden Stream speichern wir AVCodecParameters , in dem die Eigenschaften des vom i- ten Stream verwendeten Codecs beschrieben werden :

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;


Mithilfe der Eigenschaften der Codecs können wir den entsprechenden finden, indem wir die Funktion avcodec_find_decoder anfordern. Außerdem können wir den registrierten Decoder für die Codec- ID finden und AVCodec zurückgeben - eine Komponente, die weiß, wie der Stream codiert und decodiert wird:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

Jetzt können wir die Codec-Informationen drucken:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);

Mit dem Codec weisen wir Speicher für AVCodecContext zu , der den Kontext für unseren Decodierungs- / Codierungsprozess enthält. Aber dann müssen Sie diesen Codec-Kontext mit CODEC- Parametern füllen - wir tun dies mit avcodec_parameters_to_context .

Nachdem wir den Codec-Kontext ausgefüllt haben, müssen Sie den Codec öffnen. Wir rufen die Funktion avcodec_open2 auf und können sie dann verwenden:

AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);

Jetzt werden wir Pakete aus dem Stream lesen und in Frames dekodieren, aber zuerst müssen wir Speicher für beide Komponenten ( AVPacket und AVFrame ) zuweisen .

AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();

Lassen Sie uns unsere Pakete aus den Streams der Funktion av_read_frame füttern , während sie die Pakete enthält:

while(av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}

Jetzt senden wir das Rohdatenpaket (komprimierter Frame) über den Codec-Kontext mit der Funktion avcodec_send_packet an den Decoder :

avcodec_send_packet(pCodecContext, pPacket);

Und lassen Sie uns mit der Funktion avcodec_receive_frame einen Frame mit Rohdaten (einen unkomprimierten Frame) vom Decoder über denselben Codec-Kontext abrufen :

avcodec_receive_frame(pCodecContext, pFrame);

Wir können die Rahmennummer, PTS, DTS, den Rahmentyp usw. Drucken:

printf(
    "Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
    av_get_picture_type_char(pFrame->pict_type),
    pCodecContext->frame_number,
    pFrame->pts,
    pFrame->pkt_dts,
    pFrame->key_frame,
    pFrame->coded_picture_number,
    pFrame->display_picture_number
);

Und schließlich können wir unseren dekodierten Rahmen in einem einfachen grauen Bild speichern. Der Prozess ist sehr einfach: Wir werden pFrame-> Daten verwenden , wobei der Index den Farbräumen Y , Cb und Cr zugeordnet ist . Wählen Sie einfach 0 (Y), um unser graues Bild zu speichern:

save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

Und voila! Jetzt haben wir ein 2 MB Graustufenbild:


Kapitel 1 - Audio und Video synchronisieren ↑

Im Spiel zu sein ist, wenn ein junger JS-Entwickler einen neuen MSE-Videoplayer schreibt.
Bevor wir mit dem Schreiben von Transcodierungscode beginnen, sprechen wir über die Synchronisation oder darüber, wie der Videoplayer den richtigen Zeitpunkt zum Abspielen eines Frames findet.

Im vorherigen Beispiel haben wir mehrere Frames gespeichert:


Wenn wir einen Videoplayer entwerfen, müssen wir jedes Frame in einem bestimmten Tempo abspielen. Andernfalls ist es schwierig, das Video zu genießen, da es zu schnell oder zu langsam abgespielt wird.

Daher müssen wir eine Logik für eine reibungslose Wiedergabe jedes Frames definieren. In dieser Hinsicht hat jeder Rahmen eine Zeitdarstellung Marke ( PTS - aus p RÄSENTATION t ime s Stampf), die immer mehr in Betracht in den Variablen genommen istZeitbasis , eine rationale Zahl (wobei der Nenner als Zeitskala - Zeitskala bezeichnet wird ) geteilt durch die Bildrate ( fps ).

Mit Beispielen ist es einfacher zu verstehen. Lassen Sie uns einige Szenarien simulieren.

Für fps = 60/1 und timebase = 1/60000 erhöht jedes PTS die Zeitskala / fps = 1000 , sodass die tatsächliche PTS- Zeit für jeden Frame betragen kann (vorausgesetzt, sie beginnt bei 0):

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033

Fast das gleiche Szenario, aber mit einer Zeitskala von 1/60:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050

Für fps = 25/1 und timebase = 1/75 erhöht jedes PTS die Zeitskala / fps = 3 , und die PTS- Zeit kann sein:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56

Jetzt können wir mit pts_time einen Weg finden, dies synchron mit dem Sound von pts_time oder mit der Systemuhr zu visualisieren . FFmpeg libav stellt diese Informationen über seine API bereit:

fps = AVStream->avg_frame_rate
tbr = AVStream->r_frame_rate
tbn = AVStream->time_base


Aus Neugier wurden die von uns gespeicherten Frames in DTS- Reihenfolge gesendet (Frames: 1, 6, 4, 2, 3, 5), aber in PTS- Reihenfolge reproduziert (Frames: 1, 2, 3, 4, 5). Beachten Sie auch, wie viel billiger B- Frames im Vergleich zu P- oder I- Frames sind:

LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]

Kapitel 2 - Remultiplexing


Remultiplexing (Neuanordnung, Remuxing) - der Übergang von einem Format (Container) zu einem anderen. Zum Beispiel können wir MPEG-4-Video mit FFmpeg einfach durch MPEG-TS ersetzen:

ffmpeg input.mp4 -c copy output.ts

Die MP4-Datei wird demultiplext, während die Datei nicht dekodiert oder codiert wird ( -c Kopie ), und am Ende erhalten wir die mpegts-Datei. Wenn Sie das -f- Format nicht angeben , versucht ffmpeg, es anhand der Dateierweiterung zu erraten.

Die allgemeine Verwendung von FFmpeg oder libav folgt einem solchen Muster / einer solchen Architektur oder einem solchen Workflow:

  • Protokollebene - Akzeptieren von Eingabedaten (z. B. eine Datei, aber auch RTTP- oder HTTP-Download)
  • — , , ,
  • — (, ),
  • … :
  • — ( )
  • — ( ) ( )
  • — , , ( , , )


(Dieses Diagramm ist stark von der Arbeit von Leixiaohua und Slhck inspiriert. ) Erstellen wir

nun ein Beispiel mit libav, um den gleichen Effekt wie bei der Ausführung dieses Befehls zu erzielen:

ffmpeg input.mp4 -c copy output.ts

Wir werden von der Eingabe ( input_format_context ) lesen und sie in eine andere Ausgabe ( output_format_context ) ändern :

AVFormatContext *input_format_context = NULL;
AVFormatContext *output_format_context = NULL;

Normalerweise beginnen wir damit, Speicher zuzuweisen und das Eingabeformat zu öffnen. In diesem speziellen Fall öffnen wir die Eingabedatei und weisen der Ausgabedatei Speicher zu:

if ((ret = avformat_open_input(&input_format_context, in_filename, NULL, NULL)) < 0) {
  fprintf(stderr, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}

Wir werden nur Streams von Video, Audio und Untertiteln remultiplexen. Daher legen wir fest, welche Flows in einem Array von Indizes verwendet werden:

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));

Unmittelbar nachdem wir den erforderlichen Speicher zugewiesen haben, müssen wir alle Streams durchlaufen und für jeden einen neuen Ausgabestream in unserem Kontext des Ausgabeformats mit der Funktion avformat_new_stream erstellen . Bitte beachten Sie, dass wir alle Streams markieren, die keine Video-, Audio- oder Untertitel sind, damit wir sie überspringen können.

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}

Erstellen Sie nun die Ausgabedatei:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}

Danach können Sie Streams Paket für Paket von unseren Eingaben in unsere Ausgabestreams kopieren. Dies geschieht in einer Schleife, solange es Pakete ( av_read_frame ) gibt. Für jedes Paket müssen Sie PTS und DTS neu berechnen , um es schließlich ( av_interleaved_write_frame ) in unseren Kontext des Ausgabeformats zu schreiben .

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.pos = -1;

  //https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
  ret = av_interleaved_write_frame(output_format_context, &packet);
  if (ret < 0) {
    fprintf(stderr, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}

Zum Abschluss müssen wir den Stream-Trailer mit der Funktion av_write_trailer in die Ausgabemediendatei schreiben :

av_write_trailer(output_format_context);

Jetzt können wir den Code testen. Der erste Test ist die Konvertierung des Formats (Videocontainers) von MP4 in eine MPEG-TS-Videodatei. Grundsätzlich erstellen wir eine Befehlszeile ffmpeg input.mp4 -c, um output.ts mit libav zu kopieren.

make run_remuxing_ts

Es klappt! Glaube mir nicht ?! Überprüfen Sie mit ffprobe :

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s

Um zusammenzufassen, was wir getan haben, können wir jetzt zu unserer ursprünglichen Vorstellung zurückkehren, wie libav funktioniert. Wir haben jedoch einen Teil des Codecs verpasst, der im Diagramm angezeigt wird.


Bevor wir dieses Kapitel beenden, möchte ich einen so wichtigen Teil des Remultiplexing-Prozesses zeigen, bei dem Sie Parameter an den Multiplexer übergeben können. Angenommen, Sie möchten das MPEG-DASH-Format bereitstellen, sodass Sie fragmentiertes mp4 (manchmal auch als fmp4 bezeichnet ) anstelle von MPEG-TS oder normalem MPEG-4 verwenden müssen.

Die Verwendung der Befehlszeile ist einfach:

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4

In der libav-Version ist dies fast so einfach. Wir übergeben die Optionen einfach beim Schreiben des Ausgabeheaders unmittelbar vor dem Kopieren der Pakete:

AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
ret = avformat_write_header(output_format_context, &opts);

Jetzt können wir diese fragmentierte mp4-Datei generieren:

make run_remuxing_fragmented_mp4

Um sicherzustellen, dass alles fair ist, können Sie die erstaunliche Tool- Site gpac / mp4box.js oder http://mp4parser.com/ verwenden , um die Unterschiede zu erkennen - laden Sie zuerst mp4 herunter.

Wie Sie sehen können, hat es einen unteilbaren mdat- Block - dies ist der Ort, an dem sich die Video- und Audio-Frames befinden. Laden Sie jetzt fragmentiertes mp4 herunter, um zu sehen, wie es mdat-Blöcke erweitert:

Kapitel 3 - Transcodierung


TLDR zeig mir den Code und die Ausführung:

$ make run_transcoding

Wir werden einige Details überspringen, aber keine Sorge: Der Quellcode ist auf github verfügbar .

In diesem Kapitel erstellen wir einen in C geschriebenen minimalistischen Transcoder, der Videos mit den libav-Bibliotheken FFmpeg, insbesondere libavcodec , libavformat und libavutil , von H264 nach H265 konvertieren kann .


AVFormatContext ist eine Abstraktion für das Mediendateiformat, d. H. Für einen Container (MKV, MP4, Webm, TS) repräsentiert
AVStream jeden Datentyp für ein bestimmtes Format (z. B. Audio, Video, Untertitel, Metadaten).
AVPacket ist ein Fragment komprimierter Daten, die von AVStream empfangen werden und beispielsweise mit AVCodec decodiert werden können : av1, h264, vp9, hevc) Generieren von Rohdaten namens AVFrame .

Transmultiplexing


Beginnen wir mit einer einfachen Konvertierung und laden dann die Eingabedatei.

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);

Konfigurieren Sie nun den Decoder. Mit AVFormatContext erhalten wir Zugriff auf alle Komponenten von AVStream . Für jede Komponente können wir ihren AVCodec abrufen und einen bestimmten AVCodecContext erstellen . Und schließlich können wir diesen Codec öffnen, um zum Dekodierungsprozess zu gelangen.

AVCodecContext enthält Medienkonfigurationsdaten wie Datenrate, Bildrate, Abtastrate, Kanäle, Tonhöhe und viele andere.

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}

Sie müssen auch die Ausgabemediendatei für die Konvertierung vorbereiten. Weisen Sie zunächst Speicher für die Ausgabe AVFormatContext zu . Erstellen Sie jeden Stream im Ausgabeformat. Kopieren Sie die Codec-Parameter vom Decoder, um den Stream ordnungsgemäß zu verpacken.

Setzen Sie das Flag AV_CODEC_FLAG_GLOBAL_HEADER , das dem Encoder mitteilt, dass er globale Header verwenden kann, und öffnen Sie schließlich die Ausgabedatei zum Schreiben und Speichern der Header:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);

Wir erhalten AVPacket vom Decoder, passen die Zeitstempel an und schreiben das Paket korrekt in die Ausgabedatei. Trotz der Tatsache, dass die Funktion av_interleaved_write_frame " Write Frame " meldet , speichern wir das Paket. Wir schließen den Permutationsprozess ab, indem wir den Stream-Trailer in eine Datei schreiben.

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);

Umcodierung


Im vorherigen Abschnitt gab es ein einfaches Programm zur Konvertierung. Jetzt werden wir die Möglichkeit hinzufügen, Dateien zu codieren, insbesondere Videos von h264 nach h265 zu transkodieren .

Konfigurieren Sie den Encoder, nachdem der Decoder vorbereitet wurde, aber bevor Sie die Ausgabemediendatei organisieren.

  • Erstellen Sie einen Video- AVStream im Encoder avformat_new_stream .
  • Wir verwenden AVCodec mit dem Namen libx265 , avcodec_find_encoder_by_name .
  • Erstellen Sie einen AVCodecContext basierend auf dem erstellten Codecode avcodec_alloc_context3 .
  • Legen Sie die grundlegenden Attribute für eine Transcodierungssitzung fest und ...
  • ... öffnen Sie den Codec und kopieren Sie die Parameter aus dem Kontext in den Stream ( avcodec_open2 und avcodec_parameters_from_context ).

AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);

Es ist notwendig, den Decodierungszyklus zum Transcodieren eines Videostreams zu erweitern:

  • Wir senden ein leeres AVPacket an den Decoder ( avcodec_send_packet ).
  • Holen Sie sich den unkomprimierten AVFrame ( avcodec_receive_frame ).
  • Wir beginnen mit der Neukodierung des Rohrahmens.
  • Wir senden den Rohrahmen ( avcodec_send_frame ).
  • Wir erhalten eine Komprimierung basierend auf unserem AVPacket- Codec ( avcodec_receive_packet ).
  • Legen Sie den Zeitstempel fest ( av_packet_rescale_ts ).
  • Wir schreiben in die Ausgabedatei ( av_interleaved_write_frame ).

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}

Wir haben den Medienstrom von h264 auf h265 konvertiert . Wie erwartet ist die Version der h265-Mediendatei kleiner als die h264, während das Programm zahlreiche Möglichkeiten bietet:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_base_moof";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> AAC
   * MP4 - MPEG-TS
   */
  StreamingParams sp = {0};
  sp.copy_audio = 0;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.audio_codec = "aac";
  sp.output_extension = ".ts";

  /* WIP :P  -> it's not playing on VLC, the final bit rate is huge
   * H264 -> VP9
   * Audio -> Vorbis
   * MP4 - WebM
   */
  //StreamingParams sp = {0};
  //sp.copy_audio = 0;
  //sp.copy_video = 0;
  //sp.video_codec = "libvpx-vp9";
  //sp.audio_codec = "libvorbis";
  //sp.output_extension = ".webm";

Hand aufs Herz, ich gestehe, dass es etwas komplizierter war, als es am Anfang schien. Ich musste den FFmpeg-Befehlszeilen-Quellcode auswählen und viel testen. Wahrscheinlich habe ich irgendwo etwas verpasst, weil ich force-cfr für h264 verwenden musste und immer noch einige Warnmeldungen angezeigt werden, zum Beispiel, dass der Rahmentyp (5) zwangsweise in den Rahmentyp (3) geändert wurde.

Übersetzungen im Edison Blog:


All Articles