Seite 1 von 1

Ansatz gesucht zum gepufferten Pipelining eines HTTP-Streams

Verfasst: Mittwoch 31. Januar 2007, 11:48
von blumi
Hallo,

ich habe ein Problem, zu dem es (nach meinem Anfaengerhaften Wissensstand) zu viele moegliche Ansaetze gibt und ich nicht weiss, welcher der einfachste/effizienteste ist.

Mein Ziel: Wiedergabe von Internetradio via mpg321.

mpg321 ist ein Kommandozeilenplayer unter Linux. Leider kommt er nicht selbst mit HTTP-Streams klar (zum Beispiel somaFM: http://64.236.34.97:80/stream/1018 ), wohl aber kann er Daten, die ueber STDIN eintrudeln, wiedergeben.

Auf der Kommanozeile fuehrt somit ein
wget -qO - http://64.236.34.97:80/stream/1018 | mpg321 -
zu einem Teilerfolg. Teilerfolg deshalb, weil die Daten nicht gepuffert werden und es deshalb oft zu kurzen Aussetzern kommt.

Python benutze ich, weil ich schon das gesamte Rahmenprogramm (inkl. Jukebox fuer lokale MP3s und LCD-Ansteuerung) damit geschrieben habe und es gut funktioniert.

Der Ablauf muesste m.E. so werden:
(1) initialisiere Puffer mit 1/2MB
(2) subprocess: lese HTTP-Stream in Puffer ein
(3) wenn Puffer zu 80% gefuellt:
(4) while Puffer nicht leer:
(4.1) starte 'Pufferinhalt | mpg321 -'
(4.2) gib den Pufferstatus an LCD aus
(5) stop HTTP stream and kill subprocess


Wo kann ich ansetzen?
Sollte ich fuer das Lesen des HTTP Streams des externe wget benutzen oder python-interne Libs benutzen?
Wie kann ich den Puffer realisieren?

Danke,
Blumi

Nimm streamripper

Verfasst: Mittwoch 31. Januar 2007, 12:00
von sunmountain
Nimm streamripper.
Der druckt die Namen der gerade laufenden Songs auf stdout
aus, speichert die auf Wunsch und kann den Stream als "Proxy" weitergeben.

http://streamripper.sf.net

Vielleicht ist es aber am einfachsten, ein kleines Kommandozeilenpufferprogramm zu schreiben:

wget -qO - http://64.236.34.97:80/stream/1018 | puffer.py | mpg321 -

Verfasst: Mittwoch 31. Januar 2007, 12:35
von blumi
@sunmountain: Gute Idee. Das Aufnehmen von Internetradio hatte ich sowieso als "FutureFeature" angedacht, was ich auch per streamripper erledigen wollte.

Ansonsten - hast Du eine Idee, wie "puffer.py" aussehen koennte? os.mkfifo() oder asynchat.fifo()-class oder ...

Blumi

Verfasst: Mittwoch 31. Januar 2007, 12:58
von BlackJack
Ich würde so einen Puffer mit einem Thread und einer Queue lösen. Die entsprechenden Module sind `threading` und `Queue`. Ungetestet:

Code: Alles auswählen

from __future__ import division
from Queue import Queue
from threading import Thread
from time import sleep


class BufferedReader(object):
    def __init__(self, file_obj, max_blocks=1024, blocksize=1024):
        self.file_obj = file_obj
        self.max_blocks = max_blocks
        self.blocksize = blocksize
        self.blocks = Queue(max_blocks)
        self.eof_reached = False
        self.preload_phase = True
        Thread(target=self._reader).start()
    
    def _reader(self):
        while True:
            data = self.file_obj.read(self.blocksize)
            self.blocks.put(data)
            if not data:
                self.eof_reached = True
                break
    
    def _get_fill_level(self):
        return self.max_blocks / len(self.blocks)
    
    fill_level = property(_get_fill_level)
    
    def get_block(self):
        if self.preload_phase:
            while not (self.fill_level > 0.8 or self.eof_reached):
                sleep(0.01)
            self.preload_phase = False
        return self.blocks.get()

Verfasst: Mittwoch 31. Januar 2007, 13:17
von blumi
@BlackJack: Repekt, das ist doch mal ein Ansatz, tausend dank! Ich werde mich also mal in Queue einlesen.

Was hattest Du als Quelle fuer file_obj gedacht? Eine Pipe vom externen wget oder urllib2.urlopen('http://xxx')?

Blumi

Verfasst: Mittwoch 31. Januar 2007, 13:23
von BlackJack
blumi hat geschrieben:Was hattest Du als Quelle fuer file_obj gedacht? Eine Pipe vom externen wget oder urllib2.urlopen('http://xxx')?
Irgendein Objekt mit einer `read()`-Methode. Wenn `urlopen()` wie gewünscht funktioniert, spart man sich eine externe Abhängigkeit.

Verfasst: Mittwoch 31. Januar 2007, 13:36
von blumi
Ich probiere es heut abend mal aus (bin z.Zt. nicht daheim) und gebe Feedback.

Blumi

PS: Python rocks!

Verfasst: Donnerstag 1. Februar 2007, 12:46
von skypa
Sehr intressant ;)

Kannst du mal später den kompletten Code einfügen?
Rein für Lehrzwecke :roll:

Verfasst: Donnerstag 1. Februar 2007, 13:05
von blumi
Mach ich. Dauert aber noch bis alles rund ist. Momentan ist etwas anderes wichtiger (irgendwas mit Uni, Prüfungen oder so, weiß auch nicht :roll: )

Verfasst: Samstag 3. Februar 2007, 00:35
von blumi
kurzer Zwischenbericht:

der Ansatz von BlackJack war an sich nicht schlecht - nach ein paar Umstellungen an seinem Code (len(self.blocks) ging z.B. nicht), lief er.
Eingebunden sah es ungefaehr so aus:

Code: Alles auswählen

conn = urllib2.urlopen('http://xxx:8000')
br = BufferedReader(conn, max_blocks=128, blocksize=2048)
r,w,e = popen2.popen3('mpg321 -', bufsize=4096, mode='b')
while not br.eof:
    bl = br.get_block()
    if (bl == None):
        print('BUFFERING')
    else:
        print('PLAYING')
        w.write(bl)
    sleep(0.1)
for fd in (conn, r, w, e):
    fd.close()
Der Ansatz war gut. Allerdings gab es mit der blocksize bei BufferedReader, beim popen und mpg321 sowie der korrekten sleep-Dauer zu viele Variablen dabei, um den Datenstrom allgemeingueltig fluessig zu bekommen. Das gab teilweise lustige Effekte -- bei der Wiedergabe von Sekunde 1 bis 10 kam etwa Sekunde 1,2,3,4,2,5,6,7,8,6,9,10 an. Das lag wahrscheinlich an Pufferueberlaeufen.

Dann probierte ich, lediglich auf die in popen eingebauten Puffer zu setzen:

Code: Alles auswählen

p1 = subprocess.Popen(["wget", "-qO", "-", "http://xxx:8000"], bufsize=-1, stdout=subprocess.PIPE)
sleep(5)
p2 = subprocess.Popen(["mpg321", "-"], bufsize=-1, stdin=p1.stdout)
output = p2.communicate()[0]
Dieser Code steht fast 1:1 so in der [url=ttp://docs.python.org/lib/node536.html]Python Doc[/url]. Das Ergebnis war in Anbetracht der Einfachheit des Codes erstaunlich gut, aber oben genannter Effekt war immer noch zu bemerken.

Nach einem kurzen Ausflug zu dem Ansatz "Hintergrundprozess mit streamripper, der einen lokalen HTTP-Relay erstellt, welcher daraufhin mit wget geholt und an mpg321 weitergereicht wird", endete ich bei der Ansteuerung von XMMS, die absolut komplikationslos verlief. Dennoch bin ich nicht ganz gluecklich damit, weil ich eigentlich unabh. vom Windowmanager bleiben wollte.

Nebenbei sei bemerkt, dass XMMS bei der Wiedergabe nur 1%-3% Prozessorlast erzeugt, waehrend mpg321 (das Kommandozeilentool) satte 15%-20% verbrennt(*). Wow. Aber das hat nichts mit Python zu tun.

Wenn ich das komplette Programm rund gemacht habe, werde ich es im Forum vorstellen.

Bis dann,
Blumi

(*) CPU: VIA C3 mit 1GHz