Erstellt am 20.02.2022
AACStreaMP3
Falls ihr meine Seite hier schon etwas länger verfolgt, wisst ihr sicher auch, dass ich seit Jahren die VIP1710 als Internetradio betreibe und diese nach wie vor vortreffliche Dienste leistet. Ein Manko der Box ist der langsame Prozessor, bzw. einige fehlende Hardwarebeschleuniger die es so gut wie unmöglich machen AAC kodierte Audiostreams abzuspielen. Im ursprünglichen Anwendungsfall der VIP1710 wurde das Decoding auf einem dedizierten DSP vorgenommen. Ich habe aber leider zu wenig Dokumentation, Know How oder Zeit einen eigenen Decoder für Audio auf dem DSP zu implementieren.
Kürzlich haben die Betreiber von Klassikradio ihre Streams umgestellt und jetzt kann man den Sender nur noch per AAC stream empfangen. Es musste also eine Lösung her, mit der der Stream umkodiert werden kann, sodass ich den Sender nach wie vor mit der VIP1710 hören kann.
Ich habe also auf meinem Homeserver ein kleines Python Programm erstellt, welches wie ein Streamingserver auf eine HTTP Anfrage wartet. Sobald eine Anfrage eingeht, startet das Programm eine GStreamer Pipeline, die sich mit dem Klassikradio Server verbindet, den AAC Stream dekodiert und anschließend als MP3 Stream wieder kodiert. Das GStreamer Framework ist dabei erstaunlich mächtig: Es übernimmt die komplette Kommunikation mit dem Streamingdienst, also nicht nur wird der Audiostream selber umkodiert, es werden auch Titelinformationen ausgewertet und zurück in den MP3 Stream gespeist.
Einführung in GStreamer
Wahrscheinlich habt ihr von GStreamer bisher eher nur periphär gehört (höchstwahrscheinlich unter Linux), z.B. wenn euer Browser bestimmte Medieninhalte nicht abspielen wollte. GStreamer ist ein Mediaframework mit dessen hilfe Mediendaten (also Video und Audio) verarbeitet werden können. Dies geschieht in einer Pipeline in der die Daten von der Quelle (Source) zur Ausgabe (Sink) fließen. Jede Station auf dem Weg von Quelle zur Ausgabe hat dabei ihre eigene Source und Sink. Das muss man dann auch beachten, wenn man mehrere Plugins mit einander Verbinden (Link) möchte. Es macht keinen Sinn zwei Sinks miteinander zu verbinden. Schauen wir uns also eine Pipeline an um einen AAC Stream abzuspielen. Jedes Rufezeichen markiert dabei einen Link zum nächsten Plugin.:
gst-launch-1.0 souphttpsrc location=http://live.streams.klassikradio.de/klassikradio-deutschland ! faad ! autoaudiosink
Mit gst-launch-1.0 habt ihr eine einfache Möglichkeit Pipelines direkt auszuprobieren. Hier versuchen wir eine Verbindung zum Streamingserver von Klassikradio herzustellen souphttpsrc, den Datenstream mit faad zu dekodieren und dann an eine Audioausgabe autoaudiosink zu schicken.
Wenn ihr den Befehl so abschickt werdet ihr aber leider feststellen, dass das so noch nicht klappt. Wenn man das Debugging etwas hoch schraubt (--gst-debug=3) dann werdet ihr folgende Meldung sehen:
gst-launch-1.0 --gst-debug=3 souphttpsrc location=http://live.streams.klassikradio.de/klassikradio-deutschland ! faad ! autoaudiosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
0:00:00.019581918 12923 0x5646a2afc320 WARN structure gststructure.c:2069:priv_gst_structure_append_to_gstring: No value transform to serialize field 'session' of type 'SoupSession'
Got context from element 'souphttpsrc0': gst.soup.session=context, session=(SoupSession)NULL, force=(boolean)false;
0:00:02.870639889 12923 0x5646a2b060c0 WARN GST_CAPS gstpad.c:3235:gst_pad_query_accept_caps_default: caps: application/x-icy, metadata-interval=(int)8192 were not compatible with: audio/mpeg, mpegversion=(int)2; audio/mpeg, mpegversion=(int)4, stream-format=(string){ raw, adts }
...
0:00:02.870724747 12923 0x5646a2b060c0 WARN GST_CAPS gstpad.c:3235:gst_pad_query_accept_caps_default: caps: application/x-icy, metadata-interval=(int)8192, content-type=(string)audio/aac were not compatible with: audio/mpeg, mpegversion=(int)2; audio/mpeg, mpegversion=(int)4, stream-format=(string){ raw, adts }
Schauen wir uns die Fehlermeldung etwas genauer an, fallen die zwei Zeilen mit application/x-icy und audio/aac were not compatible with: audio/mpeg auf. Aber moment? Das ist doch ein MPEG4 ADTS stream? Schauen wir uns die Streamdaten doch nochmal genauer an:
curl -I http://edge53.streams.klassikradio.de/klassikradio-deutschland | fgrep Content-Type
Content-Type: audio/aac
curl http://edge57.streams.klassikradio.de/klassikradio-deutschland | head -c $((1024 * 5)) > out.stream
...
gst-typefind-1.0 out.stream
out.stream - audio/mpeg, framed=(boolean)false, mpegversion=(int)4, stream-format=(string)adts, level=(string)2, base-profile=(string)lc, profile=(string)lc, channels=(int)2, rate=(int)48000
Hmmm... was passiert hier? Wie es scheint gibt die Source souphttpsrc den Content-Type direkt an faad weiter. Der Decoder akzeptiert aber nur audio/mpeg. Wir benötigen also noch ein Zwischenplugin, das uns die Daten vom Streamingserver parsed and dann nur den Datenstream mit der Musik an faad weiterleitet. Die Information, wie dieses Plugin heißen könnte, liefert uns wiederum die Fehlermeldung von weiter oben: application/x-icy. Das ist ein Icecast Streaming Server und damit benötigen wir das icydemux Plugin:
gst-launch-1.0 souphttpsrc location=http://live.streams.klassikradio.de/klassikradio-deutschland ! icydemux ! faad ! autoaudiosink
Mit dieser Zeile solltet ihr dem Radio lauschen können. Was geschieht hier also:
- Zunächst erstellen wir eine Quelle, in diesem Fall souphttpsrc, welche sich auf eine beliebige Webadresse verbinden kann.
- Da dies ein ICY Streamserver ist, müssen die Daten zunächst in den icydemuxer, dieser extrahiert Metadaten, wie z.B. den Musiktitel oder das Genre des Streams
- Der eigentliche Stream ist MPEG4 ADTS kodiert wie wir weiter oben gesehen haben.
ADTS steht hierbei für Audio Data Transport Stream und ist ein Format das zum Audiostreaming verwendet wird. Um die Audiodaten aus diesem Stream zu extrahieren, kommt das nächste Plugin zum Einsatz: faad.
Dieses extrahiert den AAC Stream aus den ADTS Frames und konvertiert ihn in das Audioformat das GStreamer intern verwendet (also quasi die rohen Audiodaten, 48kHz, Stereo). - Der letzte Schritt in der obigen Pipeline ist diese Daten an autoaudiosink zu senden. Dieses Plugin versucht automagisch die passende Ausgabeschnittstelle (also z.B. ALSA oder Pulseaudio) zu finden und die Daten dorthin zu schicken.
Für mein Projekt sollten die Audiodaten aber in ein anderes Format encodiert werden und dann zurück an den Webclient geschickt werden:
souphttpsrc location=http://klassikradio... ! icydemux ! faad ! lamemp3enc quality=2 target=bitrate bitrate=128 cbr=true ! id3mux ! appsink
Anstatt sie also an die autoaudiosink zu senden, wird es an den Lame MP3 Encoder lamemp3enc geschickt, dem ich dann auch gleich noch sage, er soll mit 128 kBit/s enkondieren. Außerdem wäre es doch schön, wenn wir die Metadaten mit Musiktitel, welche vorher bereits vom icydemux extrahiert wurden, wieder in den Stream einbinden könnten. Das geschieht schließlich mit dem id3mux plugin. Es fügt dem MP3 Stream ID3 tags bei.
Der letzte Schritt ist die appsink. Dieses Modul erlaubt es unserem Pythoncode die Daten entgegen zu nehmen. Dabei wird der Code immer dann informiert, wenn ein neues Audiosample im Buffer bereit steht. Alles was wir dann noch tun müssen, ist den Buffer auszulesen und an die Socketverbindung, die wir vom Webserver mit dem Client (also dem Internetradio) haben, zu schicken.
Ein Exceptionhandler stellt fest, ob der Client noch eine aktive Verbindung zum Webserver hat. Wenn nicht, dann wird die GStreamer Pipeline gestoppt und beendet.
Natürlich gab es am Ende auch noch ein kleines Problem mit GStreamer:
Die Streamadresse von Klassikradio zeigt auf einen Load-Balancer (Server: streaMonkey streaming Server Loadbalancer Native) welcher den Streamingclient auf die eigentliche Adresse verweist (z.B. edge53.streams.klassikradio.de). GStreamer öffnet zunächst also einen Socket zur Streamadresse, wird von da weitergeleitet und öffnet einen zweiten Socket zur neuen Adresse. Wenn man den Abspielvorgang am Radio stoppt, sollte die GStreamer Pipeline eigentlich geschlossen werden. Es wird aber irgendwie nur der letzte Socket geschlossen (also jener der die Daten liefert). Der Socket von der Load-Balancer Adresse bleibt hingegen auf unbestimmte Zeit offen.
Abhilfe schafft hier also ein kleines Stück code am Anfang der routine, das die Adresse direkt über das requests modul in Python auflöst.
url = 'http://live.streams.klassikradio.de/klassikradio-deutschland'
print( "Checking stream URL" )
resp = requests.head( url, allow_redirects=False )
while( resp.status_code == 300 or resp.status_code == 302 ):
url = resp.headers['Location']
print( "Redirected to " + url )
resp = requests.head( url, allow_redirects=False )
Hoffentlich ist dieses kleine Projekt nützlich für euch. Ihr könnt die Pipeline auch verändern, solltet ihr andere Codices nutzen wollen. Es sollte sogar möglich sein, einen Videostream zusammen zu setzen. Man könnte z.B. einen MJPEG Stream einer Kamera auslesen und als MPEG stream an einen Mediaplayer senden. GStreamer ist ein wirklich mächtiges Wekzeug und zusammen mit dem Webserver könntet ihr viele verschiedene Services realisieren.