Gstreamer + Python = tu propio reproductor multimedia en minutos

gstreamerPythonArco
Es muy sencillo construirse un reproductor/codificador/decodificador multimedia usando Gstreamer y Python. Aquí, haremos un reproductor de Ogg Vorbis básico.
Ingredientes
  • python-gst0.10

La instalación, como ya sabrás, es muy compleja y tediosa, pero no hay mas remedio que tragar Sticking out tongue :

# apt-get install python-gst0.10

Introducción

Antes de empezar a escribir código, unas breves líneas de como rula Gstreamer. Gstreamer es básicamente un framework para la manipulación multimedia. Con él se puede hacer casi de todo, desde un simple reproductor de Ogg Vorbis (como aqui veremos) hasta complejas aplicaciones distribuidas de streaming multimedia. Todo depende de los modulos y plugins de que dispongamos.

Gstreamer funciona como un flujo de datos dentro de una tubería. Se selecciona una fuente de datos, un camino para estos (en el que se pueden modificar o no) y un destino. Podemos manipular los eventos que se produzcan en la tubería, controlar su estado e incluso inyectar más datos a nuestro antojo. Interiormente, en la tubería, los datos también pueden seguir diferentes caminos. Por ejemplo, el vídeo puede ir por un lado, mientras que el audio procesarse por otro.

En Gstreamer, todas las partes que usamos para construir la tubería (la fuente, el destino, los codificadores, etc.) se llaman “Elementos”, que tienen ‘enchufes’ (Pads). Entonces, cuando creamos la tubería conectamos unos enchufes con otros, para que el flujo siga su curso. Y estos Pads, tienen establecido un sentido para el flujo de datos: de salida (llamados Source) y de entrada (llamados Sink). Cada elemento de Gstreamer puede tener uno o varios pads, o ninguno (e incluso, es muy común que los pads se creen dinámicamente, segun sean necesarios).

Por ejemplo, podemos crear una tubería con una fuente que lea de un fichero de audio en Ogg Vorbis, más un demultiplexor para el envoltorio del Vorbis (es decir, un elemento que extraiga del Ogg los datos
codificados con Vorbis), más un decodificador de Vorbis, por supuesto. Además necesitamos un conversor de audio y por último, un destino, que podría ser por ejemplo ALSA. Simplemente con esto, nos construimos un reproductor de Ogg/Vorbis, que es lo que vamos a hacer.

Manos a la obra

Pues bien, como todo lo vamos a hacer en Python, lo primero que necesitamos es importar los modulos que vamos a usar. Es decir:

  • gst: evidentemente, y además en su version 0.10, con lo que también importaremos pygst para cargarlo.
  • sys: es muy común.
  • gobject: porque lo usa Gstreamer.

Con lo que tendríamos:

import pygst  
pygst.require("0.10")
import sys, gst, gobject
gobject.threads_init()  

Bien, ahora creamos una función que haga las veces de main, y comprobamos que tenemos los argumentos suficientes:

def main(args):
 
    if len(args) != 2:
        print "Uso: " + args[0] + " <archivo Ogg>"
        return -1

Hasta ahí nada nuevo. Ahora tenemos que crear la tubería. Para esto hay diversas maneras. Podemos crear cada elemento por separado, enlazar los Pads correspondientes y después meterlos todos en un contenedor. De esta forma, tenemos un mejor control sobre lo que hacemos, pero existe una herramienta con la que es mucho más sencillo todo. Se llama ‘parse_launch’. Acepta una cadena con la estructura final de nuestra tubería, y nos devuelve la tubería hecha. Así, para nuestra tubería, la cadena que tendríamos que formar es:

    pipestr = "filesrc location= %s ! oggdemux ! vorbisdec ! audioconvert ! alsasink" % args[1]

Esta cadena contiene cada elemento (con el nombre por el cual está definido) más algun atributo de ese elemento, separados con el símbolo de admiración (!). ‘filesrc’ es el elemento que se encarga de leer de un fichero del disco, y con la propiedad ‘location’ establecemos la ruta al fichero que se tiene que leer; ‘oggdemux’ es el demultiplexor de Ogg, etc.

Pues si le pasamos esta cadena a parse_launch, intentará crearnos la tubería correspondiente, y además completamente funcional.

    try:
        pipeline = gst.parse_launch(pipestr)
    except gobject.GError, e:
        print "No es posible crear la tuberia, " + str(e)
        return -1

Ahora sería útil añadir un pequeño manipulador de eventos, ya que si se produce algún mensaje dentro de la tubería, debemos gestionarlo nosotros. En esta ocasión, solo haremos caso de dos mensajes: EOF, que
nos indica que debemos terminar (End Of Stream, o fin de flujo) y ERROR, que también nos obliga a terminar, pero advirtiendo de que se produjo un error interno. El resto lo ignoramos. Para ello crearemos una mini función interna que lo gestione todo.

    def eventos(bus, msg):
        t = msg.type
        if t == gst.MESSAGE_EOS: 
            loop.quit()
 
        elif t == gst.MESSAGE_ERROR:
            e, d = msg.parse_error()
            print "ERROR:",e
            loop.quit()
 
        return True    

Y la añadimos al bus de comunicaciones que tiene la tubería.

    pipeline.get_bus().add_watch(eventos)

Ya tenemos la tubería completamente creada. Ahora sólo nos queda establecer el estado de la misma. El estado de la tubería especifica que es lo que pasa con el flujo dentro de ella. Disponemos básicamente de NULL, READY, PLAYING y PAUSED. El estado inicial de la tubería es NULL, y si queremos que empieze a funcionar, tendremos que cambiarlo
a PLAYING. Una vez ahí, el flujo de datos puede fluir por todos los elementos.

    pipeline.set_state(gst.STATE_PLAYING)

¿Qué pasa? ¿No debería sonar ya? Hombre, pues no exactamente. Todavía nos queda un pequeño detalle. Como antes dije, Gstreamer usa Gobject, y por tanto, todos sus elementos bailan al son de Gboject. Por ello, es necesario crear un MainLoop y ponerlo en marcha. También sería bueno poder salir de una manera adecuada cuando se pulsa Ctrl+C.

    loop = gobject.MainLoop()
    try:
        print "Reproduciendo..."    
        loop.run()
 
    except KeyboardInterrupt: # Por si se pulsa Ctrl+C
        pass
 
    print "Parando... \n¡Adios!"

Como nota, he de decir que la función ‘run’ de gobject es un bucle de eventos, es decir, el programa se quedará en ese punto hasta que ‘run’ retorne. En nuestro caso, eso sucede cuando se pulsa Ctrl+C o cuando se produce un evento EOS o ERROR (ya que entonces se llama a ‘quit’ de Gobject).

Ya esta sonando todo… bien. Ahora solo nos queda decidir que hacer cuando termine el flujo o se pulse Ctrl+C. Lo lógico es poner la tubería en estado NULL y salir. Eso haremos.

    pipeline.set_state(gst.STATE_NULL)
    return 0

Bien, pues ya tenemos nuestra función principal. Yo añado un par de lineas para que se llame a esa función principal cuando el script se ejecuta como tal, y no cuando se importa como módulo, pero esto es a gusto del consumidor Eye-wink

if __name__ == "__main__":
    sys.exit(main(sys.argv))

¡Y ya está! Así de sencillo Eye-wink

Comentarios

Como ves, es muy fácil. Casi todo el codigo necesario es para manipular eventos o parte del programa principal. La esencia de todo radica en la cadena de texto que se pasa a ‘parse_launch’. Si cambiamos los elementos, creamos tuberías diferentes. Por ejemplo, en vez de dirigir la salida a ALSA, la puedes dirigir a un fichero (con filesink). Esto lo dejo para tu imaginación ;-p . Gstreamer es muy potente, así que con poca cosa se pueden hacer cosas muy interesantes… ¡que te diviertas!

Todo junto

Para que no tengas que copiar y pegar muchas veces, aquí dejo todo el codigo del programa completo, todo juntito, ¡testeado, funcionando y todo!

import pygst  
pygst.require("0.10")
import sys, gst, gobject
gobject.threads_init()  
 
def main(args):
 
    if len(args) != 2:
        print "Uso:", args[0], " <archivo Ogg>"
        return -1
 
    pipestr = "filesrc location= %s  ! oggdemux ! vorbisdec ! audioconvert ! alsasink" % args[1]
 
    try:
        pipeline = gst.parse_launch(pipestr)
    except gobject.GError, e:
        print "No es posible crear la tubería,", str(e)
        return -1
 
    def eventos(bus, msg):
        t = msg.type
        if t == gst.MESSAGE_EOS: 
            loop.quit()
 
        elif t == gst.MESSAGE_ERROR:
            e, d = msg.parse_error()
            print "ERROR:", e
            loop.quit()
 
        return True    
 
    pipeline.get_bus().add_watch(eventos)
 
    pipeline.set_state(gst.STATE_PLAYING)
 
    loop = gobject.MainLoop()
    try:
        print "Reproduciendo..."    
        loop.run()
    except KeyboardInterrupt: # Por si se pulsa Ctrl+C
         pass
 
    print "Parando... \n¡Adios!"
 
    pipeline.set_state(gst.STATE_NULL)
    return 0
 
if __name__ == "__main__":
    sys.exit(main(sys.argv))

Referencias

Más:

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de brue

ayuda, por favor

Tengo unas cuantas dudas de novato sobre gst.

Creando una clase para disparar sonidos, me ha surgido la duda sobre cómo hacerlo de la manera más correcta con gst. Actualmente hereda de GstBaseAudioSrc pero no tengo ni idea aun de cómo implementar el src de la forma correcta. ¿Cómo meto un ogg en cada nuevo objeto? Esto es, coger el audio de un fichero no relentiza todo el proceso? Si el sonido se tiene que disparar 4 veces por segundo y en paralelo con otros 12 sonidos, si lee de disco cada vez nos morimos.

La clase acepta el nombre del fichero y la pipeline que está desde ese punto hasta el sink final, esto es se conecta al resto de la tubería.

¿Hay que crear un multiplexor para cada sonido que vaya al pipeline final?

¿Cúal es la mejor forma de coger el ogg y meterlo en el objeto BaseAudioSrc para su reproducción desde un método hipotético play() ? Esto es, fx.play()

Para estos sonidos no hace falta seek() ya que son cortos y se deberían reproducir en su totalidad.

¿Alguna idea, Oscar? He buscado info sobre BaseAudioSrc pero no me queda muy claro, si me remites a algún sitio interesante o me ayudas directamente te lo agradecería.
--
·brue

brue

Imagen de oscarah

Antes de nada...

Yo no soy experto en Gstreamer. Lo que yo te pueda sugerir puede (y además es probable) estar completamente equivocado (a modo de disclaimer Sticking out tongue)

Supongo que estas dudas te surgen por lo del proyecto FreeBand, no? Mmmm, me parece interesante. Veamos. La forma que tienes ahora de hacerlo es conectando una tubería ya hecha con un objeto que hereda de BaseSrc y que lee del disco. Esto te crea dos problemas, 1) la lectura al disco, que es lenta; 2) solo puedes reproducir un sonido en esa tubería al mismo tiempo. Para el primer problema, si no tienes muchos sonidos y estos no son muy grandes, podrías cargarlos todos en memoria. Si son muchos y empieza a descontrolarse, podrías implementar un sistema de caché para evitar quedarte sin memoria (solo metes en memoria los más usados y tal... ya sabes).

Por otro lado, si necesitas concurrencia, creo que podrías implementar los sonidos como objetos de gstreamer, conectarlos todos a un multiplexor (si, posiblemente necesitarás varios multiplexores) y poner en marcha la tubería. Cuando se necesite lanzar un sonido, simplemente llamas a un método que "active" ese elemento, de forma que cuando la tubería le pida datos (un do_create()), se los ofrezca. Cuando termine la reproducción del sonido, "desactivas" el elemento. Lo de "activar" y "desactivar" lo pongo entrecomillado porque se puede hacer de varias formas: unas al margen de gstreamer, es decir, no usas la api de gstreamer para ello (por ejemplo, retornando "silencio" siempre que se pidan datos) y otras usan los métodos de gst (por ejemplo, desenchufando el objeto como tal del multiplexor).

Creo que si cada objeto "sonido" que crees contiene el audio como tal, la implementación del do_create() es sencilla. No necesitas preocuparte del control de estados ni de cosas como seek().

No sé si me explico bien. Espero haberte ayudado.
Saludos.

PD: se me olvidaba, aunque es posible que lo hayas visto:
http://crysol.inf-cr.uclm.es/node/566
________________________________________________
La "L" de "CRySoL" es de "Libre" no de "Linux".

"aviso: la dereferencia de punteros de tipo castigado romperá las reglas de alias estricto" --GCC 4.3.1

Imagen de brue

Uf uf uf...

Miedo tengo. He hecho unas pruebas con pygame, que accede al hardware casi directamente, y no sé si será por la forma de ver las teclas pulsadas o del número de canales de audio, o del hardware o de vete tú a saber qué... pero con pygame que tira de alsa la latenncia no es óptima. Estoy pensando seriamente si gstreamer es lo mejor para latencias cercanas a 0 ...

En el caso de la edición o reproducción de video, no hay problema, uno se sincroniza con otro y no hay problema... pero cuando es el humano pulsado teclas el que tiene que sincronizarse con el juego ... problemas.

Al final no sé si usaré gstreamer, porque me parece que la latencia es acoj*nante sin soporte bueno por parte del hardware.

¿Sugerencias? Alsa a pelo, OpenAl, SDL (pygame) ????

Al final haré una tesis sobre esto Sad

--
·brue

brue

Imagen de oscarah

Me temo...

que posiblemente tengas razón, no sé que tal irá Gstreamer para cosas en tiempo real, pero mucho me temo que para esas cosas no esté muy pulido Sad
________________________________________________
La "L" de "CRySoL" es de "Libre" no de "Linux".

"aviso: la dereferencia de punteros de tipo castigado romperá las reglas de alias estricto" --GCC 4.3.1

Imagen de oscarah

Ayuda muy útil

Si necesitas en un momento dado reproducir un fichero de a/v y no tienes a mano tu reproductor favorito (o no soporta ese codec), pero sabes que gstreamer si puede con ello, prueba con esto:

'gst-launch playbin uri=LOCATION'

¡Te va a dejar anonadado (por no decir flipando)! Detecta el solito qué tienes en el fichero y crea la tubería que necesite.
________________________________________________
La "L" de "CRySoL" es de "Libre" no de "Linux".

"aviso: la dereferencia de punteros de tipo castigado romperá las reglas de alias estricto" --GCC 4.3.1

Imagen de int-0

Y ahora el mismo reproductor SIN ESCRIBIR CÓDIGO

Bueno... antes de ponernos a programar pipes de gstreamer puede ser útil probarlas primero... para eso tenemos gst-launch-0.10, para el ejemplo anterior:

$ gst-launch-0.10 filesrc location=archivo.ogg ! oggdemux ! vorbisdec ! audioconvert ! alsasink
Con esto tendremos el mismo reproductor que en el caso anterior.
La pipe se describe igual que en python, las conexiones entre pads se realizan de forma automática, los atributos se pueden establecer, como véis en el ejemplo, de la forma atributo = valor y después del tipo de elemento.
Existe una alternativa a gst-launch que es gst-xmllaunch. Nosotros tenemos una pipe descrita en forma de XML y con el programita este podremos ejecutarla... ¿cómo creamos ese XML? pues con gstreamer-editor (es paquete Debian) que es un editor gráfico la mar de chulo con el que podrás enredar con todos los plugins de gstreamer. Inconveniente: es para gstreamer-0.8, por ahora no es compatible con 0.10... una lástima... ¡ah! se me olvidaba: con gst-launch podéis también generar dicho XML con la opción -o archivo.xlm. Esto es todo!

P.D. Mola esto del gstreamer... sigue escribiendo! Laughing out loud
------------------------------------------
For Happy Lusers! Try this as root!
dd if=/dev/zero of=/dev/hda bs=1G count=10
------------------------------------------

------------------------------------------------------------
$ python -c "print 'VG9udG8gZWwgcXVlIGxvIGxlYSA6KQ==\n'.decode('base64')"
------------------------------------------------------------

Imagen de david.villa

Y sin escribir nada

Aplicaciones->Sonido y Vídeo->Totem Sticking out tongue

Creo que la receta iba de aprender a "programar" gstreamer y para eso hay que escribir código...

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.

Imagen de int-0

Yap...

Pero antes de ponerte a programar... tal vez te venga mejor diseñar primero las pipes... las pipes se pueden optimizar mucho y creo que es más interesante diseñarla primero e implementarla después. La mejor forma de ir probando es gst-launch, sin lugar a dudas...

Consultad man gst-launch-0.10 y veréis...
------------------------------------------
For Happy Lusers! Try this as root!
dd if=/dev/zero of=/dev/hda bs=1G count=10
------------------------------------------

------------------------------------------------------------
$ python -c "print 'VG9udG8gZWwgcXVlIGxvIGxlYSA6KQ==\n'.decode('base64')"
------------------------------------------------------------

Imagen de david.villa

Guía para novatos

De gstreamer con Python: Getting started with GStreamer with Python y otro post del mismo autor sobre Pads dinámicos: GStreamer Dynamic Pads, Explained

No soy portavoz de ningún colectivo, grupo o facción. Mi opinión es personal e intransferible.