GStreamer

GStreamer es una poderosa biblioteca multimedia para reproducir, crear y manipular sonido, video y otros medios. GStreamer soporta la codificación y decodificación de numerosos formatos de forma predeterminada, y el soporte para formatos adicionales se pueden agregar a través de complementos (plug-ins). Precisamente esos plugins le dotan de una modularidad que lo convierten en un framework extremadamente potente y versátil, aunque a costa de aumentar su nivel de complejidad.

En definitiva, se trata de un framework multimedia, libre y multiplataforma (escrito en lenguaje de programación C, usando la biblioteca GObject) que permite crear aplicaciones audiovisuales de distintos tipos de medios: vídeo, sonido, codificación, etc. Por ejemplo, con GStreamer se puede desde reproducir música hasta realizar tareas más complejas como mezclar audio y vídeo. Ejemplos de algunas aplicaciones que utilizan GStreamer son amaroK, Banshee, Empathy, Kaffeine, Listen, Parole, Pitivi, Rhythmbox, Sound Converter y Totem.

GStreamer es lanzado bajo la LGPL y provee una API para escribir aplicaciones, aunque creo (no estoy seguro) que para Genie su desarrollo (la versión 1.x) está todavía todavía en fase experimental y sigue utilizando la versión anterior (gstreamer-0.10).

Primeros pasos

Instalación

Antes de empezar a trastear, necesitamos instalar sus librerías: libgstreamer1.0-0 y libgstreamer1.0-dev (a veces las podemos encontrar como gstreamer o lib32-gstreamer), paquetes disponibles en los repositorios oficiales de la mayoría, si no en todas, las distribuciones Linux. Para otros sistemas operativos (Windows, Mac OS X, Android…) visita Download GStreamer y Installing GStreamer. En algunos casos puede ser necesario o recomendable instalar otros paquetes adicionales: gst-libav (contiene muchos codificadores y decodificadores) y gir1.2-gst-plugins-base (complementos esenciales). También puede venir bien instalar gstreamer1.0-tools. Otros paquetes como gst-plugins-bad y gst-plugins-ugly no son necesarios.

Puedes hacer una primera comprobación de tu instalación escribiendo esto en consola:

$ gst-inspect-1.0 fakesrc

Esto debe imprimir un montón de información en la consola, pero si por el contrario devuelve “no such element or plugin”, posiblemente GStreamer no se ha instalado correctamente. Otra prueba en línea de comandos que soltará otra parrafada para comprobar el estado de la instalación es:

$ gst-launch-1.0 -v fakesrc silent=false num-buffers=3 ! fakesink silent=false

Y por último, para hacer un test de video (esto debe mostrar una ventana de video tipo carta de ajuste):

$ gst-launch-1.0 videotestsrc ! videoconvert ! autovideosink

Para comprobar el sonido, en caso de utilizar Pulseaudio (dependiendo de tu sistema de salida de sonido cambia pulsesink por alsasink -ALSA-, osssink -OSS-, oss4sink -OSSv4-, jackaudiosink -JACK- o autoaudiosink -selección automática de la salida de audio-), escribe en consola :

$ gst-inspect-1.0 pulsesink

Y puedes hacer un test de sonido (molesto) con (comprueba el volumen y que no estás usando los auriculares):

$ gst-launch-1.0 audiotestsrc ! audioconvert ! audioresample ! pulsesink

Conceptos básicos

Para no andar muy perdido al principio, es importante conocer algunos términos fundamentales, aunque solo sea por encima (y a lo bruto):

Elementos (Gst.Element)

Un elemento es una clase de objetos muy importante en GStreamer. Por lo general, se crea una cadena de elementos vinculados entre sí y los datos fluyen a través de esta cadena de elementos.

Es la parte básica de las entidades involucradas en un flujo de medios. Hay un elemento en el que se origina y un elemento donde termina el flujo de datos, y también otros elementos adicionales entre ellos que lo manipulan.

Cada elemento tiene una función específica, que puede ser la lectura de datos de un archivo, la decodificación de esos datos o la salida de esos datos a la tarjeta de sonido (o cualquier otra función). Y al encadenar varios de estos elementos, se crea una canalización (pipeline) que realiza una tarea específica, por ejemplo, reproducción o captura de medios.

GStreamer viene con una gran colección de elementos por defecto, haciendo posible el desarrollo de una gran variedad de aplicaciones multimedia, aunque si es necesario, también se pueden escribir nuevos elementos.

Elementos de origen (source)

Los elementos de origen o fuente generan los datos para su uso posterior. Cuentan únicamente con puerto de entrada.

Elemento source

Elementos intermedios

Se trata de elementos filtros y similiares (filters, convertors, demuxers, muxers y codecs) con puertos (uno o varios) de entrada y salida.

Elemento filter

Elementos finales (sink)

Los elementos sink (de sumidero o fregadero) son puntos finales en el procesamiento de medios. Aceptan datos a través de un puerto de salida para implementar tareas como la reproducción de sonido o la salida de video.

Elemento sink

Enlaces o puertos (Gst.Pad)

Cada elemento tiene por lo menos una puerta, que es source (fuente), sink (fregadero o sumidero), o ambas cosas. Source es de donde fluyen los datos, y snik es a donde fluyen los datos. Estas puertas se llaman pads. Un elemento puede tener más de un pad, por ejemplo, para elementos que pueden producir flujos de audio y vídeo.

Los Pads son puntos de entrada y salida del elemento, donde se pueden conectar otros elementos. Se utilizan para crear enlaces y flujo de datos entre elementos.

Se pueden ver como “enchufes” o “puertos” en un elemento donde se pueden hacer enlaces con otros elementos, y a través de los cuales los datos pueden fluir hacia o desde esos elementos.

Los Pads tienen capacidades específicas de manejo de datos. Sólo se permiten los enlaces entre dos Pads cuando los tipos de datos permitidos de ambos son compatibles. Por ejemplo, los elementos source y sink solo tienen Pads de source y sink, respectivamente. Los tipos de datos se describen con Gst.Caps.

Elementos enlazados

Cajones (Gst.Bin) y canales o tuberías (Gst.Pipeline)

Gst.Bin es un elemento que puede contener otro elemento, lo que les permite ser administrados como un grupo. Es un contenedor para una colección de elementos.

Esto supone que se puede controlar un contenedor como si fuera un elemento y a su vez controlar sus elementos contenidos como subclases de él mismo. Por ejemplo, se puede cambiar el estado de todos los elementos de un contenedor cambiando el estado del mismo contenedor.

Gst.Bin

Gst.Pipeline es un contenedor especial de nivel superior, un canalizador de elementos. Proporciona un bus (Gst.Bus) a la aplicación y gestiona la sincronización (Gst.Clock) de los elementos que contiene. Por ejemplo, cuando se establece en estado PLAYING, se inicia el flujo de datos y se realiza el procesamiento de medios. Una vez iniciado, Gst.Pipeline se ejecuta en un hilo independiente hasta que el flujo es interrumpido o alcanza el final de los datos.

Gst.Pipeline

Ejemplo de pipeline que contiene un demuxer y dos ramas, una para el audio y otra para el vídeo:

Gst.Pipeline

Comunicación

GStreamer proporciona varios mecanismos para la comunicación y el intercambio de datos entre la aplicación y la fuente de información.

  • Gst.Buffer. Son objetos para transmitir datos entre elementos en la canalización. Los búferes siempre viajan desde source a sinks (downstream: hacia abajo).
  • Gst.Event. Son objetos enviados entre elementos o desde la aplicación a los elementos. Los eventos pueden viajar hacia arriba (upstream) y hacia abajo (downstream).
  • Gst.Message. Los mensajes son objetos publicados por elementos en el bus de mensajes de la canalización, donde se almacenarán para su recogida por la aplicación. Los mensajes se utilizan para transmitir información como errores, etiquetas, cambios de estado, estado de almacenamiento en búfer, redirecciones, etc. desde los elementos a la aplicación.
  • Las consultas (queries) permiten a las aplicaciones solicitar información como la duración o la posición actual de reproducción desde el canalizador. Los elementos también pueden utilizar consultas para solicitar información propia (como el tamaño o la duración del archivo).

Toda esta comunicación entre el flujo de datos y la aplicación está manejado por el sistema bus (Gst.Bus).

Comunicación en GStreamer

Arrancando GStreamer con Genie

A la hora de compilar con valac:

valac --pkg gstreamer-1.0 nombre_archivo.gs

Al inicio del código: uses Gst

Y para arrancar GStreamer: Gst.init (ref args)

Vamos a hacer una primera prueba para comprobar que funciona video y sonido:

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
 
uses Gst
 
init
	Gst.init (ref args)
	video: Gst.Element
 
	video = Gst.parse_launch (
		"playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm")
 
	video.set_state (State.PLAYING)
 
	bus:Gst.Bus = video.get_bus ()
	bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)
	video.set_state (State.NULL)

Paso a paso:

Primero le decimos al compilador que vamos a usar la librería GStreamer:

uses Gst

Inicializamos GStreamer:

Gst.init (ref args)

Creamos el objeto video que es un objeto de Gst.

video: Gst.Element

Gst.Element es una clase abstracta básica para construir otros componentes para utilizar en GStreamer. Por eso, quizá Gst.Element a la hora de programar es el objeto más importante, como componente básico para la canalización de medios.

Después ya podemos crear una canalización de medios (media pipeline). El parámetro fundamental de Gst.parse_launch es pipeline_description con el que describimos el tipo de canal (los otros parámetros, opcionales, son context y flags).

Al utilizar Playbin podemos reproducir archivos de audio y video, para lo que le señalamos la dirección del archivo a reproducir con la propiedad uri:

video = Gst.parse_launch (
	"playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm")

Ahora iniciamos la reproducción definiendo el estado de nuestro elemento:

video.set_state (State.PLAYING)

Los valores posibles son:

  • VOID_PENDING. No hay ningún estado definido.
  • NULL. El estado inicial de un elemento.
  • READY. El elemento está listo para ir a PAUSED.
  • PAUSED. El elemento está en pausa, listo para aceptar y procesar datos.
  • PLAYING. El elemento está siendo reproducido.

Después le tenemos que decir qué tiene que hacer cuando termine la reproducción (o encuentre un error) y para ello utilizamos Gst.Bus, que es un objeto responsable de la entrega de paquetes de mensajes en forma de transmisión de entrada-salida (errores, fin de secuencia, etiquetas encontradas, cambios de estado, etc.) publicando los correspondientes mensajes.

Con timed_pop_filtered se obtiene un mensaje del bus cuyo tipo coincida con el tipo de mensaje especificado con su máscara, durante un tiempo de espera determinado (si el tiempo de espera es CLOCK_TIME_NONE, esta función continuará hasta que se muestre un mensaje coincidente en el bus).

En nuestro caso, se espera hasta recibir un mensaje de error o un mensaje EOS (End-Of-Stream: final del flujo de datos) en el bus:

bus:Gst.Bus = video.get_bus ()
bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)

Y finalmente, cuando se obtiene el mensaje, cambiamos el estado de nuestro elemento:

video.set_state (State.NULL)

Con el mismo esquema, ahora podemos escuchar un archivo de sonido (en este caso, en formato ogg) de nuestro propio equipo (debe ser una ruta absoluta, por ejemplo file:///home/genie/movie.avi):

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
 
uses Gst
 
init
	Gst.init (ref args)
 
	cancion: Gst.Element
 
	cancion = Gst.parse_launch (
		"playbin uri=file:///home/jcv/Programacion/Genie/gstreamer/Goodbye.ogg")
 
	cancion.set_state (State.PLAYING)
 
	bus:Gst.Bus = cancion.get_bus ()
	bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)
	cancion.set_state (State.NULL)

Date cuenta que este código hace lo mismo que si pones en consola (desde el directorio donde está el archivo de audio):

$ gst-launch-1.0 playbin uri=file:///home/jcv/Programacion/Genie/gstreamer/Goodbye.ogg

Y a la inversa, a partir de esta orden en consola

$ gst-launch-1.0 filesrc location=vivaldi.mp3 ! mad ! audioconvert ! pulsesink

escribimos el siguiente código:

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
 
uses Gst
 
init
	Gst.init (ref args)
	pipeline: Gst.Element
 
	pipeline = Gst.parse_launch (
		"filesrc location=vivaldi.mp3 ! mad ! audioconvert ! pulsesink")
 
	pipeline.set_state (State.PLAYING)
	bus:Gst.Bus = pipeline.get_bus ()
	bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)
	pipeline.set_state (State.NULL)

Fundamentos de programación

De acuerdo a los conceptos que hemos visto hasta ahora, cualquier aplicación básica se fundamenta en un código que pasa por 3 fases:

  1. Crear distintos elementos Gts.
  2. Incorporar los elementos creados a un canal común (Pipeline) y conectarlos a través de sus puertos (source y sink) para el procesamiento de los medios.
  3. Entregar el flujo de datos al bucle principal de la aplicación y crear sus condiciones de ejecución (por ejemplo, para la activación y parada).

Crear elementos

La forma más sencilla de crear un elemento es mediante Gst.ElementFactoty_make(factoryname, name). Esta función toma como parámetros dos strings: un nombre “de fábrica” que define el tipo de elemento y un nombre para el elemento recién creado.

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
uses Gst
 
init
	elemento: Gst.Element
 
	Gst.init (ref args)
 
	// creamos el elemento Gts
	elemento = Gst.ElementFactory.make ("fakesrc", "source")	           
 
	if elemento == null
		stdout.printf ("Error al crear un elemento del tipo 'fakesrc'\n")
	else
		print ("Ha sido creado el elemento %s.", elemento.get_name())

Canalizar y conectar los elementos

Ahora el objetivo consiste en enlazar los elementos creados para establecer un canal de medios a través del cual fluyen los datos. Para ello, incluimos todos los elementos en un canal común y los conectamos entre ellos.

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
uses Gst
 
init	
	origen: Gst.Element
	filtro: Gst.Element
	salida: Gst.Element
 
	Gst.init (ref args)
 
	// creamos el canal contenedor (por ahora vacío)
	var canal = new Pipeline("mi_canal")
 
	// creamos los elementos
	origen = Gst.ElementFactory.make ("fakesrc", "source")
	filtro = Gst.ElementFactory.make ("identity", "filter")
	salida = Gst.ElementFactory.make ("fakesink", "sink")	
 
	// agregamos los elementos al canal
	canal.add(origen)
	canal.add(filtro)
	canal.add(salida)
 
	// conectamos los elementos
	canal.link_many (origen, filtro, salida)
 
	// comprobamos la conexión
	if (origen.link (salida) != true)
		stdout.printf ("Los elementos no están conectados.\n")

Cuando conectamos elementos es importante asegurarse de haber agregado previamente los elementos a un bin o a un pipeline con add antes de intentar vincularlos porque al agregar un elemento a un contenedor se desconectará de los vínculos ya existentes. En principio, no se pueden vincular elementos que no estén en el mismo contenedor.

Otra manera de incorporar los elementos sería:

canal.add_many (origen, filtro, salida)

De cualquiera de las dos maneras, acabamos de crear una sencilla cadena de tres elementos conectados entre sí. Aunque si esto fuera una aplicación realmente no haría nada: necesitamos cambiar el estado de los elementos (por defecto su estado es NULL). Antes ya vimos los estados posibles de los elementos (NULL, READY, PAUSED y PLAYING) y sabemos que podemos cambiarlo con Gst.Element.set_state(), por ejemplo:

origen.set_state(State.PLAYING)

Aunque mejor sería cambiar el estado del contenedor:

canal.set_state(State.PLAYING)

Pero tampoco así sería suficiente; necesitaríamos a Gst.Bus para que funcionase, ya que es el encargado de entregarlo al bucle principal de la aplicación. Entonces, al activar el estado del contenedor, automáticamente se procesarán los datos sin necesidad de ninguna forma de iteración y Gst.Bus gestionará los mensajes disponibles sobre el proceso.

Por lo general, el cambio de estado del contenedor se propaga a todos sus elementos de forma automática (salvo que agreguemos dinámicamente un elemento a un canal ya en ejecución, y entonces tendremos que establecer el estado de ese nuevo elemento con Gst.Element.set_state() o con Gst.Element.sync_state_with_parent()).

No hay que olvidar que los contenedores (cajones -Gst.Bin- y canales o tuberías -Gst.Pipeline-) son también elementos (elementos contenedores). Y puesto que un contenedor es un elemento en sí mismo, su compartimiento se puede manejar de la misma manera que manejamos cualquier otro elemento.

Así, un contenedor, al combinar un grupo de elementos vinculados, permite tratar directamente con él y no con sus elementos individuales, lo que es especialmente útil cuando tratamos con contenedores complejos.

En definitiva, el contenedor también gestiona los elementos contenidos en él y realiza cambios de estado en los elementos, y también recoge y reenvía mensajes de bus.

Los contenedores se crean de la misma manera que se crean otros elementos, es decir, usando una “fábrica de elementos” aunque como elementos especiales disponen de sus propias funciones new Gst.Bin(nombre) y new Gst.Pipeline(nombre).

Como hemos visto, podemos agregar elementos a un contenedor utilizando Gst.Bin.add(elemento) y Gst.Pipeine.add(elemento). Con add_many() añadimos una lista de elementos. Para eliminar un elemento del contenedor utilizamos Gst.Bin.remove(elemento) y Gst.Pipeine.remove(elemento) y con remove_many() eliminamos una lista de elementos.

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
uses Gst
 
init	
	source: Gst.Element
	sink: Gst.Element
 
	Gst.init (ref args)
 
	var pipeline = new Gst.Pipeline("tuberia")
	var bin = new Gst.Bin("cubo")
	source = Gst.ElementFactory.make ("fakesrc", "source")
	sink = Gst.ElementFactory.make ("fakesink", "sink")
 
	// primero añadimos los elementos al cajón
	bin.add_many (source, sink)
	// y ahora agregamos el cajón a la tubería
	pipeline.add (bin)
 
	// enlazamos los elementos
	pipeline.link_many(source, sink)
 
	// comprobamos la conexión
	if (!source.link (sink))	
		stdout.printf ("Los elementos no están conectados.\n")

Hay varias funciones para buscar elementos en un contenedor. Los más utilizados son Gst.Bin.get_by_name() y Gst.Bin.get_by_interface(). También se puede iterar sobre todos los elementos que contiene un contenedor mediante la función Gst.Bin.iterate_elements(). Además, se pueden crear contenedores personalizados para realizar tareas específicas.

Como decía antes, los contenedores administran el estado de todos los elementos contenidos en ellos. Al establecer un bin o un pipeline en un determinado estado con set_state(), nos aseguramos que todos los elementos contenidos en él también se ajustan a ese mismo estado (con el grifo de la tubería es suficiente para abrir y cerrar el paso del agua).

Y puesto que los contenedores administran el estado de todos los elementos contenidos en ellos, si establecemos un bin (o un pipeline, que es un tipo especial de contenedor) en un determinado estado de destino usando gst_element_set_state(), nos aseguramos de que todos los elementos contenidos en él también se establecerán en este estado. Esto significa que normalmente sólo es necesario establecer el estado de la tubería de nivel superior.

Bus, mensajes y más

Bus es un sistema que se encarga de reenviar mensajes desde los subprocesos de transmisión a la aplicación (y le entrega el flujo de datos a su bucle principal). Es el mensajero responsable de la entrada-salida de paquetes de mensajes a la aplicación.

Cada pipeline contiene un bus por defecto, por lo que las aplicaciones no necesitan crear un bus ni nada. Lo único que las aplicaciones deben hacer es establecer un controlador de mensajes en un bus que comprueba periódicamente si detecta nuevos mensajes cuando la aplicación está en marcha.

Los datos que fluyen a través de una tubería (pipeline) consisten en una combinación de búferes y eventos. Los búferes contienen los datos de los medios mientras que los eventos contienen información de control, como por ejemplo buscar información y notificadores de fin de flujo (end-of-stream).

Ya hemos utilizado antes Gst.Bus.timed_pop_filtered() para detectar cuando termina una reproducción. También se suelen utilizar Gst.add_watch(), Gst.add_signal_watch(), Gst.Bus.peek() y Gst.Bus.pool(), que iremos viendo en los ejemplos de código.

GStreamer tiene algunos tipos de mensajes predefinidos que se pueden pasar a través del bus. Todos los mensajes tienen origen (Gst.Message.src), tipo (Gst.MessageType) y marca de tiempo (Gst.Message.timestamp). La fuente del mensaje se puede utilizar para ver qué elemento emitió el mensaje.

En general, los mensajes pueden ser:

  • Notificaciones de error, advertencia e información sobre el estado del procesamiento de datos. Todos estos mensajes contienen un GError con el tipo de error principal y mensaje, y se pueden obtener usando Gst.Message.parse_error(), Gst.Message.parse_warning() y Gst.Message.parse_info().
  • Notificación de fin de secuencia (End-Of-Stream): se emite cuando la transmisión ha finalizado.
  • Etiquetas (Tags): mensaje emitido cuando se encuentran metadatos en el flujo (por ejemplo, el nombre del artista o título de la canción, o el bitrate). Se utiliza Gst.Message.parse_tag() para analizar las etiquetas.
  • Cambios de estado (State-changes): emitidos después de un cambio exitoso de estado. Gst.MessageType.STATE_CHANGED() se utiliza para ver el estado antiguo y el nuevo.
  • Buffering: se emite durante el almacenamiento en caché de los flujos de red.
  • Mensajes de elementos: son mensajes especiales que son exclusivos de ciertos elementos y normalmente representan funciones adicionales.
  • Mensajes específicos de la aplicación.

Como hemos visto, los pads son los puertos del elemento con su mundo exterior. Los datos fluyen o se procesan desde el pad de origen (source) de un elemento hasta el pad receptor (sink) de otro elemento. El tipo específico de medios que un elemento puede manejar se expresa por las capacidades de su pad.

El tipo de pad se define por dos propiedades: su dirección y su disponibilidad. En cuanto a la dirección, ya hemos visto que GStreamer define dos posibilidades para el pad: source (fuente u origen) y sink (sumidero o fregadero). Así, un elemento recibe datos sobre su pad sink y genera datos que vuelca sobre su pad source.

En cuanto a la disponibilidad, un pad puede tener tres disponibilidades posibles: siempre (always, siempre presente o activo), a veces (sometimes, solo existe o se activa -o no- en ciertos casos) y bajo petición (on-request, solo si se solicita explícitamente por la aplicación).

Los pads con disponibilidad 'sometimes' se conocen como pads dinámicos, y son utilizados en aquellos casos en los que se crea un elemento que puede tener sus pads inactivos. En esos casos podemos incorporar en el código una función como un controlador para informar cuando el elemento ha creado o activado un nuevo pad 'sometimes'.

El siguiente código puede servir como ejemplo de pads dinámicos y también para ejemplificar otras cuestiones tratadas anteriormente. Se trata de una adaptación de basic-tutorial-3.c a Genie realizada para esta Wiki con ayuda de Valadoc.

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
uses Gst
 
pipeline: Gst.Pipeline
source: Gst.Element
convert: Gst.Element
sink: Gst.Element
 
// CONTROLADOR DEL NUEVO PAD
 
// función llamada por la señal del nuevo pad
def pad_added_control(src:Gst.Element, new_pad:Gst.Pad)	
	sink_pad:Gst.Pad = convert.get_static_pad ("sink")
	stdout.printf ("Recibido nuevo '%s' desde '%s':\n", new_pad.name, src.name)
 
	// comprueba si convert ya está vinculado, y entonces no hace nada aquí
	if (sink_pad.is_linked ())
		stdout.printf ("  Ya están vinculados. Ignoring.\n")
		return
 
	// comprueba el tipo del nuevo pad
	new_pad_caps:Gst.Caps = new_pad.query_caps (null)
	new_pad_struct: weak Gst.Structure = new_pad_caps.get_structure (0)
	new_pad_type:string = new_pad_struct.get_name ()
 
	// solo aceptamos nuevo pad tipo de audio
	if (!new_pad_type.has_prefix ("audio/x-raw"))
		stdout.printf ("  Tipo de enlace recibido: '%s', ignorado.\n", new_pad_type)
		return
 
	// intenta enlazar
	ret: Gst.PadLinkReturn = new_pad.link (sink_pad)
	if (ret != Gst.PadLinkReturn.OK)
		stdout.printf ("  Enlace tipo '%s' pero ha fallado.\n", new_pad_type)
	else
		stdout.printf ("  Tipo de enlace recibido '%s', enlace logrado.\n", new_pad_type)
 
 
// INICIO EJEMPLO TUBERÍA DINÁMICA
 
init
 
	// arrancamos GStreamer
	Gst.init (ref args)
 
	// creamos los elementos
	source = Gst.ElementFactory.make ("uridecodebin", "source")
	convert = Gst.ElementFactory.make ("audioconvert", "convert")
	sink = Gst.ElementFactory.make ("autoaudiosink", "sink")
 
	//creamos el pipeline vacío
	pipeline = new Gst.Pipeline ("test-pipeline")	
	if (pipeline == null or source == null or convert == null or sink == null)
		stdout.printf ("Advertencia: no todos los elementos han sido creados.\n")
 
	// contruímos la tubería
	// pero ahora NO estamos enlazando source (lo haremos más tarde)
	pipeline.add_many (source, convert, sink)
	if (!convert.link (sink))
		stdout.printf ("Advertencia: los elementos no se pueden vincular.\n")
 
	// establecemos la dirección del archivo a reproducir
	source.set ("uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm")
 
	// NUEVO PAD: conecta la señal del nuevo pad a la función de control
	source.pad_added.connect (pad_added_control)
 
	// empieza la reproducción
	ret:Gst.StateChangeReturn = pipeline.set_state (Gst.State.PLAYING)
		if (ret == Gst.StateChangeReturn.FAILURE)
			stdout.printf ("ERROR: No se puede ajustar el estado de la tubería a PLAYING.\n")
 
	// BUS
	bus: Gst.Bus = pipeline.get_bus ()
	terminate:bool = false
	do
		msg:Gst.Message = bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE,
				Gst.MessageType.STATE_CHANGED | Gst.MessageType.ERROR | Gst.MessageType.EOS)
 
		// analiza mensajes
		if (msg != null)
			case (msg.type)
				when Gst.MessageType.ERROR
					err: GLib.Error
					debug_info: string
					msg.parse_error (out err, out debug_info)
					stdout.printf ("Error recibido desde el elemento %s: %s\n", msg.src.name, err.message)
					stdout.printf ("Debugging information: %s\n", (debug_info != null)? debug_info : "none")
					terminate = true
					break
				when Gst.MessageType.EOS
					stdout.printf ("Final de la reproducción alcanzado.\n")
					terminate = true
					break
				when Gst.MessageType.STATE_CHANGED
					// por conocer los mensajes de cambio de estado de la tubería
					if (msg.src == pipeline)
						old_state: Gst.State
						new_state: Gst.State
						pending_state: Gst.State
						msg.parse_state_changed (out old_state, out new_state, out pending_state)
						stdout.printf ("El estado de la tubería ha cambiado desde %s a %s.\n",
							Gst.Element.state_get_name (old_state),
							Gst.Element.state_get_name (new_state))
					break
				default
					// no deberíamos llegar hasta aquí
					stdout.printf ("Mensaje inesperado recibido.\n")
 
	while (!terminate)
 
	// liberamos recursos
	bus = null
	pipeline.set_state (Gst.State.NULL)
	pipeline = null

¡Dale al PLAY!

Después del tocho anterior, en esta sección se van a repasar esos conceptos y fundamentos utilizando códigos de ejemplo.

Hola mundo! con GStreamer

Un código que ya hemos visto, ahora completo con algunos comentarios:

// compila con valac --pkg gstreamer-1.0 test3.gs
 
uses Gst
 
init
	pipeline: Gst.Element
	bus: Gst.Bus
	msg: Gst.Message
 
	// arranca GStreamer
	Gst.init (ref args)
 
	// construye una pipeline
	pipeline = Gst.parse_launch (
		"playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm")
 
	// inicia la reproducción
	pipeline.set_state (Gst.State.PLAYING)
 
	// espera hasta error o fin de reproducción
	bus = pipeline.get_bus ()
	msg = bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)
 
	// libera recursos
	if (msg != null)
		msg = null	
	bus.unref()
	pipeline.set_state (Gst.State.NULL)
	pipeline = null

Como hemos visto, en GStreamer usualmente se construye una tubería ensamblando manualmente varios elementos individuales, pero cuando la tubería es bastante fácil y no necesita ninguna característica avanzada, se puede tomar un atajo: Gst.parse_launch(). Esta función toma una representación textual de una tubería y la convierte en una tubería real.

La tubería construida está compuesta de un único elemento llamado playbin. Playbin es un elemento especial que actúa como fuente (source) y como sumidero (sink), conformando una tubería entera que crea y conecta todos los elementos necesarios para reproducir sus medios. Con playbin podemos usar el parámetro uri, que nos puede servir para http:// y para file://.

Finalmente, antes de terminar la aplicación, dejamos las cosas ordenadas liberando referencias y los recursos asignados.

Pipeline

El ejemplo anterior mostró cómo construir un pipeline automáticamente. Ahora vamos a construir un pipeline manualmente instanciando cada elemento, agrupándolos y enlazándolos.

Este código de ejemplo abre una ventana y muestra un video de prueba, sin audio, tipo carta de ajuste.

// compila con valac --pkg gstreamer-1.0 test3.gs
uses Gst
 
init
	pipeline: Gst.Pipeline
	source: Gst.Element
	sink: Gst.Element
	bus: Gst.Bus
	msg: Gst.Message
	ret: Gst.StateChangeReturn
 
	// arranca GStreamer
	Gst.init (ref args)
 
	// crea los elementos
	source = Gst.ElementFactory.make ("videotestsrc", "source")
	sink = Gst.ElementFactory.make ("autovideosink", "sink")
 
	// crea la tubería vacía
	pipeline = new Gst.Pipeline ("test-pipeline")
	if (pipeline == null or source == null or sink == null)
		stdout.printf ("Error al crear los elementos.\n")
 
	// construye la tubería
	pipeline.add_many (source, sink)
	if (source.link (sink) != true)
		stdout.printf ("Los elementos no han sifo enlazados.\n")
		pipeline = null
 
	// modifica las propiedades del elemento sorce
	source.set ("pattern", 0)
 
	// Start playing
	ret = pipeline.set_state (Gst.State.PLAYING)
	if (ret == Gst.StateChangeReturn.FAILURE)
		stdout.printf ("ERROR: Pipeline no se puede poner en estado PLAYING.\n")
		pipeline = null
 
	// espera hasta error o fin de reproducción (EOS)
	bus = pipeline.get_bus ()
	msg = bus.timed_pop_filtered (Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)
 
	// analiza mensajes
	if (msg != null)
		//error: GLib.Error
		//debug: string
		case (msg.type)
			when Gst.MessageType.ERROR
				error: GLib.Error
				debug: string
				msg.parse_error (out error, out debug)			
				stdout.printf ("Error recibido desde el elemento %s: %s\n", msg.src.name, error.message)
				stdout.printf ("Debugging information: %s\n", (debug != null)? debug : "none")
				error = null
				debug = null
				break
			when Gst.MessageType.EOS
				stdout.printf ("End-Of-Stream reached.\n")
				break
			default				
				stdout.printf ("Unexpected message received.\n")
				break
 
		msg = null
 
	// libera recursos	
	bus = null
	pipeline.set_state (Gst.State.NULL)
	pipeline = null

La mayoría de los elementos de GStreamer tienen propiedades que son atributos que se pueden modificar para cambiar el comportamiento del elemento (propiedades de escritura usando set()) o para averiguar información del elemento (propiedades legibles, usando get()).

En la línea de código que modifica la propiedad del elemento 'source', cambiamos el valor de la propiedad pattern de videotestsrc, que controla el tipo de video de prueba que el elemento emite (prueba a cambiar el 0 por otros números y verás distintos tipos de test de video).

Los nombres y valores posibles de todas las propiedades de un elemento se pueden encontrar usando la herramienta gst-inspect-1.0. Así, por ejemplo, escribiendo en consola:

$ gst-inspect-1.0 videotestsrc

nos devuelve, entre otra información, el nombre de las propiedades de videotestsrc y sus posibles valores. Concretamente en cuanto a la propiedad pattern, ahora conocemos que admite estos valores que generan distintos tipos de tests:

(0): smpteSMPTE 100% color bars
(1): snowRandom (television snow)
(2): black100% Black
(3): white100% White
(4): redRed
(5): greenGreen
(6): blueBlue
(7): checkers-1Checkers 1px
(8): checkers-2Checkers 2px
(9): checkers-4Checkers 4px
(10): checkers-8Checkers 8px
(11): circularCircular
(12): blinkBlink
(13): smpte75SMPTE 75% color bars
(14): zone-plateZone plate
(15): gamutGamut checkers
(16): chroma-zone-plateChroma zone plate
(17): solid-colorSolid color
(18): ballMoving ball
(19): smpte100SMPTE 100% color bars
(20): barBar
(21): pinwheel Pinwheel
(22): spokesSpokes
(23): gradientGradient
(24): colorsColors

En el código también hemos añadido un chequeo de error más que comprueba si pipeline ha cambiado correctamente de estado a PLAYING.

Reproductor de audio Ogg

Ejemplo que engloba gran parte de lo visto hasta ahora para construir un reproductor simple de audio Ogg.

// compila con valac --pkg gstreamer-1.0 nombre_archivo.gs
 
uses Gst
 
loop: MainLoop
 
pipeline: Gst.Pipeline
source: Gst.Element
demuxer: Gst.Element
decoder: Gst.Element
conv: Gst.Element
sink: Gst.Element
bus: Gst.Bus
 
// NUEVO PAD
def on_pad_added(src:Gst.Element, pad:Gst.Pad)
	// crea enlace dinámico
	sink_pad:Gst.Pad = decoder.get_static_pad ("sink")	
	// intenta enlazar demuxer/decoder
	ret: Gst.PadLinkReturn = pad.link (sink_pad)
	if (ret != Gst.PadLinkReturn.OK)
		stdout.printf ("Enlace frustrado.\n")
	else
		stdout.printf ("Enlace conseguido.\n")
 
 
// BUS Y CONTROLADOR DE MENSAJES
def bus_call(bus:Gst.Bus, msg:Gst.Message):bool
	case (msg.type)
		when Gst.MessageType.EOS
			stdout.printf ("End of stream\n")
			loop.quit ()
			break
		when Gst.MessageType.ERROR
			error: GLib.Error
			debug: string
			msg.parse_error (out error, out debug)
			stdout.printf ("Error: %s: %s\n", msg.src.name, error.message)
			loop.quit ()
			break
		default
			break
	return true
 
// INICIO EJEMPLO REPRODUCTOR DE AUDIO OGG
init	
 
	// arranca GStreamer
	Gst.init (ref args)
 
	// creamos el bucle principal de la aplicación
	loop = new MainLoop()
 
	// creamos los elemento de gstreamer
	pipeline = new Gst.Pipeline ("audio-player")
	// elemento que lee archivos desde el disco
	source   = Gst.ElementFactory.make ("filesrc",       "file-source")
	// elemento que analiza el archivo y lo decodifica en audio
	// oggdemux requiere pads dinámicos
	demuxer  = Gst.ElementFactory.make ("oggdemux",      "ogg-demuxer")
	// elemento decodificador de audio Vorbis
	decoder  = Gst.ElementFactory.make ("vorbisdec",     "vorbis-decoder")
	conv     = Gst.ElementFactory.make ("audioconvert",  "converter")
	// elemento de salida de audio, que detecta automáticamente el dispositivo de audio
	sink     = Gst.ElementFactory.make ("autoaudiosink", "audio-output")
 
	if (pipeline == null or source == null or demuxer == null or
		decoder == null or conv == null or sink == null)
		stdout.printf ("Algún elemento no puede ser creado.\n")
 
	// establecemos el archivo que genera el elemento source
	source.set("location", "Goodbye.ogg")	
 
	// bucle y controlador de mensajes
	bus: Gst.Bus = pipeline.get_bus ()
	priority:int = Priority.DEFAULT	
	bus.add_watch(priority, bus_call)
 
	// añadimos todos los elementos dentro de pipeline
	// file-source | ogg-demuxer | vorbis-decoder | converter | audio-output
	pipeline.add_many (source, demuxer, decoder, conv, sink)
 
	// enlazamos todos los elementos
	// file-source -> ogg-demuxer ~> vorbis-decoder -> converter -> audio-output
	source.link (demuxer)
	decoder.link_many (conv, sink)	
	demuxer.pad_added.connect (on_pad_added)
	/* vinculamos dinámicamente demuxer a decoder
      porque Ogg puede contener varios flujos (por ejemplo audio y video).
      El pad de origen se creará por demuxer bajo petición
      de una función que se ejecuta a la señal de pad_added */
 
	pipeline.set_state (Gst.State.PLAYING)
 
	// activamos el bucle principal	
	loop.run()
 
	// liberamos recursos
	bus = null
	pipeline.set_state (Gst.State.NULL)
	pipeline = null
🔝