Gstreamer: eine Einführung
Aktuell beschäftige ich mich mit Gstreamer. Mein Ziel ist es dabei mithilfe von Gstreamer und Snowmix [2] eine Pipeline aufzubauen, mithilfe derer ich mit meiner vorhandenen Hardware auf Yotube oder Twitch streamen kann. Das aktuelle ffmpeg kommt mit meiner Hardware bzw. video4linux2 nicht mehr so ganz klar, weshalb Tools wie OBS-Studio ausfallen.
Diese Einführung soll lediglich Gstreamer motivieren und die Grundfunktionsweise erklären. Details lasse ich dabei erst mal außen vor, um einen einfachen Einstieg in Gstreamer zu ermöglichen. Auch Streaming-Verfahren, Codecs, etc werden hier nicht behandelt, ich setze ein grundlegenden Wissen darüber voraus. Ich verwende Arch Linux und gstreamer 1.0. Die erforderlichen Pakete sind stark von der Linux-Distribution abhängig, meist gstreamer und gstreamer-plugins-*. Helfen wird sicherlich auch gstreamer-libav. Unter Arch Linux heißen die Gstreamer 1.0-Pakete gst-*.
Gstreamer als Pipeline
Machen wir uns zuerst die Funktionsweise von Gstreamer bewusst. Gstreamer ist als eine Pipeline aufgebaut worden, die Elemente (elements) der Pipeline wird dabei meist durch Plugins realisiert. Die meisten Plugins haben Quelle(n) und Senke(n) (source, sink). Durch die Verbindung zwischen den Elementen können Videodaten von einem Element der Kette zum nächsten gebracht werden. Gstreamer setzt dabei oftmals auf shared memory, d.h. auf Speicher, der von allen Elementen adressiert werden kann, wodurch ein Übergeben von Daten auf die Dereferenzierung eines Zeigers hinausläuft.
Die Elemente der Pipeline erhalten Daten vom vorherigen Element, verarbeiten die Daten und geben die Daten weiter an das nachfolgende Element der Pipeline. Quellen erzeugen Videodaten, Senken zerstören Daten (wobei meist zuvor noch eine Aktion wie Anzeigen mit den Daten gemacht wird). Jede Pipelinestufe benötigt Zeit, wodurch eine Latenz entsteht zwischen dem Eingeben der Daten und dem Ausgeben der Daten. Gstreamer kann nicht nur mit Videodaten arbeiten sondern auch mit Audiodaten; will man beides gemeinsam aus einer Datei auslesen, muss das Signal vor der Anzeige / dem Abspielen demultiplext werden, also die Audiodaten müssten von den Videodaten getrennt werden (demux), dann kann die Verarbeitung in zwei (oder mehr) Strängen der Pipeline parallel weiterlaufen.
Folgende Darstellung der Pipeline ist aus [1] entnommen:
Eine erste Pipeline
Als erstes Experiment verwenden wir die testvideosrc, die ein Testbild erzeugt und die autovideosink, die die eingegebenen Videodaten anzeigt. Um die Pipeline zu erstellen verwende ich hier gst-launch-1.0, was für die Gstreamer Entwickler eine Art Test-tool darstellt. Mithilfe der Programmiersprache C (oder anderen Sprachen), kann mit dem Gstreamer SDK programmiert werden um anspruchsvollere Anwendungen zu erzeugen. Damit beschäftige ich mich hier erst mal nicht, dafür empfehle ich die Gstramer Dokumentation [3].
Die erste Pipeline verwendet die testvideosrc um Videodaten zu erzeugen, die in die autovideosink eingespeist werden:
gst-launch-1.0 videotestsrc ! autovideosink
Die Pipeline-Stufen werden mit ! verbunden. Es sollte ein Fenster erscheinen, was das Testbild anzeigt:
Ein erster Stream
Hier soll nun noch gezeigt werden, wie die Videodaten mittels dem Motion-JPEG-Verfahren komprimiert und mit RTP über das Netzwerk verschickt werden und dort mittels gstreamer angezeigt werden. Dazu erweitern wir schrittweise unsere Pipeline. Zunächst können wir die Pipeline durch eine Operation und die Gegenoperation erweitern, bspw. werden wir die Bilder der videotestsrc mit jpegenc enkodieren, und mit jpegdec dekodieren. Am Ergebnis sollte sich nur marginale qualitative Unterschiede zeigen.
gst-launch-1.0 videotestsrc ! jpegenc ! jpegdec ! autovideosink
Über weitere Parameter an jpegenc kann der Enkodierschritt weiter parametrisiert werden. Bspw. können wir eine JPEG-Qualität angeben:
gst-launch-1.0 videotestsrc ! jpegenc quality=20 ! jpegdec ! autovideosink
Die Qualität des angezeigten Bildes sollte stark schlechter sein, als die oben gezeigte. Wir erweitern die Pipeline weiter durch rtpjpegpay und rtpjpegdepay um die JPEG-Daten in RTP-Pakete einzuteilen.
gst-launch-1.0 videotestsrc ! jpegenc ! rtpjpegpay ! rtpjpegdepay ! jpegdec ! autovideosink
Und zu guter Letzt verschicken wir die Daten per UDP an uns selbst (127.0.0.1) an den Port 5200, in der Hoffnung am Zielrechner gibt es einen Prozess der diesen Port belauscht und Videodaten anzeigen will. Jetzt müssen wir die Pipeline allerdings aufteilen in einen Server-Teil und einen Client-Teil, weil udpsink kein Quellpad und udpsrc kein Senkenpad mehr besitzt. Pad ist die Bezeichnung der Quellen bzw. Senken in einem Plugin. Zwischen den Pads können in Plugins noch so genannte Caps (Capabilities) festgelegt werden, die dem nachfolgenden Element mitteilen, welche Art von Stream er erwarten soll. Setzen wir das gesamte Wissen zusammen erhalten wir für den Server:
gst-launch-1.0 videotestsrc ! jpegenc quality=80 ! rtpjpegpay ! udpsink host=127.0.0.1 port=5200
Und für den Client müssen wir die Caps angeben, weil rtpjpegdepay sonst nicht weiß um welche Daten es sich handelt, die aus udpsrc herausfallen. Gstreamer ist so generisch gehalten, dass damit prinzipiell jede Art von Stream verarbeitet werden kann, d.h. udrsrc weiß erstmal nicht, welche Daten es da eigentlich bekommt.
gst-launch-1.0 udpsrc port=5200 caps=application/x-rtp ! rtpjpegdepay ! jpegdec ! autovideosink
Um bspw. Dateien zu öffnen, kann filesrc anstelle von videotestsrc verwendet werden, um Video4Linux2 Geräte zu verwenden benutzt man v4l2src mit dem Parameter device.
Literatur
[1] http://www.z25.org/static/_rd_/videostreaming_intro_plab/
[2] http://snowmix.sourceforge.net/
[3] https://gstreamer.freedesktop.org/data/doc/gstreamer/head/manual/html/index.html