Multimedia en MIDP 2.0
En este tutorial estudiamos uno de los aspectos
más interesantes de MIDP 2.0: las nuevas capacidades multimedia que
ofrece a través de los paquetes javax.microedition.media y javax.microedition.media.control.
El nuevo API multimedia que definen estos paquetes es un subconjunto
del MMAPI (Mobile Media API) enteramente compatible con el API
completo. El API Multimedia constituye una interfaz flexible, simple y
potente para el manejo de capacidades multimedia. Mientras que MMAPI
permite reproducir y grabar tanto datos de audio como de video, los
paquetes aquí estudiados ofrecen un número reducido de acciones
relacionados con la reproducción de audio.
Como ya se ha comentado, el hecho de poder trabajar con datos multimedia
constituye una de los aspectos más novedosos y ventajosos de MIDP 2.0
frente a la versión 1.0. En los últimos tiempos hemos podido constatar
la gran evolución que han experimentado los terminales móviles. Es
posible desde cualquier terminal móvil reproducir secuencias de tonos
con una calidad infinitamente superior a cualquier dispositivo con
algunos meses de antigüedad. Por tanto, el hecho de que MIDP 2.0
responda a esta demanda de una herramienta adecuada para poder explotar
estas nuevas capacidades, supone un gran acierto y abre nuevas vías al
programador.
Sin embargo, como veremos a continuación, el conjunto de acciones que
podemos llevar a cabo puede parecer excesivamente limitado. En este
punto debemos recordar que la definición de MIDP 2.0 abarca un rango de
dispositivos considerable y que no todos ellos tienen la misma
capacidad para reproducir audio o vídeo. Por este motivo MIDP 2.0 se
limita a definir cuál es el mínimo que todos ellos deben satisfacer
pudiendo, individualmente, en función del tipo de dispositivo y del
fabricante, utilizar el MMAPI completo.
Para definir la arquitectura del API multimedia debemos saber que su
funcionamiento se basa en los siguientes conceptos básicos:
- Reproductor (Player): sabe cómo reproducir unos datos
multimedia concretos. Por ejemplo necesitaremos uno para reproducir
archivos de audio, otro para tonos simples, etc.
- Controlador (Control): utilizado para modificar el
comportamiento de un reproductor concreto. A partir de una instancia de
un reproductor podemos obtener su controlador asociado.
- Gestor (Manager): es el objeto que cohesiona todos los
anteriores y sirve como punto de entrada al sistema ya que dispone de
métodos estáticos que permiten crear reproductores y/o fuentes de
datos.
Figura 1: Arquitectura del API Multimedia |
Así, una vez expuesta la filosofía básica de este API podemos pasar a analizar cuáles son sus clases y métodos. |
|
Manager es como ya se ha dicho el punto de partida para procesar o reproducir datos multimedia. Se nos ofrecen dos opciones:
API de la clase Manager Los métodos de los que dispone esta clase son:
getSupportedContentTypes(String protocol):
devuelve una lista con los distintos content types soportados por un
determinado protocolo. Si el protocolo indicado no es válido el
resultado será una lista vacía. Si por el contrario no se especifica
ningún protocolo (pasamos un String de valor null) se devuelven todos los tipos soportados por la implementación.
getSupportedProtocols(String contentType):
devuelve una lista con los protocolos soportados por un determinado
content type. Los protocolos devueltos se pueden utilizar directamente
en el localizador para crear un objeto Player.
Si el argumento contentType es null se devolverán todos los protocolos
soportados por la implementación. Si el content type indicado no es
válido o no es soportado se devolverá una lista de protocolos vacía.
playTone(int nota, int duracion, int volumen):
reproduce un tono con una nota, una duración y un volumen
determinados. A la hora de utilizar este método debemos tener en cuenta
que puede conducir a un considerable consumo de recursos en
dispositivos sin soporte hardware para la reproducción de tonos.
|
|
Dado que Player
es un interfaz no podemos instanciar objetos directamente sino que,
como ya hemos podido ver, debemos obtenerlos a partir de un objeto Manager.
Ya sabemos cómo podemos hacer esto así que a partir de ahora nos
centraremos en conocer que funcionalidad nos ofrece este interfaz. La
acción más sencilla que podemos llevar a cabo es ejecutar el método start()
que dará comienzo a la reproducción lo antes posible. Veremos más
adelante todos los pormenores de este y del resto de métodos del
interfaz.
Player nos permite no sólo reproducir audio sino que además permite controlar dicha reproducción e incluso su propio ciclo de vida.
CICLO DE VIDA DEL REPRODUCTOR
Resulta de gran utilidad definir un ciclo de vida para los
reproductores ya que de esta manera el programador será capaz de tener
cierto control sobre una serie de operaciones que son susceptibles de
consumir gran cantidad de recursos.
El ciclo de vida de un objeto Player consta de cinco estados:
- UNREALIZED
- REALIZED
- PREFETCHED
- STARTED
- CLOSED
Cuando un Player
es creado se encuentra en el estado UNREALIZED. Es este estado el
reproductor no dispone de información suficiente para localizar los
recursos necesarios para comenzar la reproducción. Una vez haya
localizado los datos pasará al estado REALIZED. Por ejemplo, en caso de
querer reproducir un fichero de audio de un servidor a través de una
conexión http, la transición se produciría al recibir la respuesta del
servidor a la petición que previamente se cursó ya que en este punto el
reproductor estaría en condiciones de empezar a recibir los datos a
reproducir.
El siguiente estado por el que pasaría es el de PREFETCHED. Se llega a
él cuando el reproductor ha recibido suficiente cantidad de datos para
comenzar con la reproducción. La utilidad de definir este estado radica
en que llegan a él los reproductores que están listos para empezar con
la reproducción de datos. De esta manera se reduce el tiempo de
latencia desde que se indica que se inicie la reproducción y el
instante en que realmente comienza. En este punto, cuando haya dado
comienzo la reproducción se llegaría al estado STARTED.
En la secuencia de estados que acabamos de describir no aparece el
estado CLOSED. El reproductor puede llegar a este estado desde
cualquiera de los anteriores cuando se indique que no va a ser
utilizado nunca más. En este caso el reproductor liberará la mayoría de
los recursos que tenía reservados.
Figura 2: Ciclo de vida de un objeto Player |
El interfaz Player
ofrece una serie de métodos que permiten realizar las transiciones
anteriormente descritas (en ambos sentidos) a nivel de programador.
Además de estos métodos encontramos otros que permiten acceder a la
información del reproductor. A la hora de utilizar estos métodos
debemos observar cuidadosamente el estado en que se encuentre el objeto
Player ya que la información disponible en cada uno de los estados no es la misma.
API DEL INTERFAZ Player
Los métodos que en este caso tenemos a nuestra disposición son:
- addPlayerListener(PlayerListener listener): añade un objeto PlayerListener a este Player de manera que se puedan capturar los eventos que este genere.
NOTA: la clase PlayerListener se explicará más adelante.
- close(): cierra el objeto Player
y libera los recursos que éste tenía asignados. Cuando el método
regresa de su ejecución el reproductor estará en estado CLOSED y no
podrá ser usado más. Además se generará un evento que podrá ser
capturado por los PlayerListener registrados por el reproductor.
- deallocate():
libera los recursos escasos o exclusivos que hayan sido asignados al
reproductor, como el dispositivo de audio. Al regresar de la ejecución
de este método el reproductor estará en el estado UNREALIZED (si el
reproductor se encontraba bloqueado en la llamada realize()) o
UNREALIZED (en el resto de casos). Por este motivo, si al invocar este
método ya se encontrase en uno de esos estados, la petición sería
ignorada.
- getContentType(): indica de qué tipo es el contenido de los datos multimedia asociados a este Player.
- getDuration():
devuelve la duración de la reproducción. Si no pudiese ser determinada
(por ejemplo si se estuviésen reproduciendo datos en tiempo real) se
devolvería la constante TIME_UNKNOWN.
- getMediaTime(): devuelve el media time actual del reproductor. En caso de no poder determinarlo devolvería TIME_UNKNOWN.
- getState(): devuelve el estado actual del reproductor.
- prefetch():
el reproductor adquiere los recursos necesarios para la reproducción y
procesa la máxima cantidad de datos posible para reducir el tiempo de
latencia al realizar el start. Esta última acción será llevada
a cabo incluso cuando se invoque este método estando ya en el estado
PREFETCHED. De esta manera aseguramos que el tiempo de latencia siempre
será mínimo.
Cuando el método regresa de la ejecución el reproductor se encuentra en
estado PREFETCHED. Si al llamar a este método el objeto Player se encontrase en estado UNREALIZED se produciría una llamada implícita a realize(). En cambio, si el estado inicial fuese STARTED la petición sería ignorada.
- realize():
construye porciones del reproductor sin asignarle en exclusiva los
recursos necesarios para la reproducción. Al regresar de la ejecución
de este método el reproductor se encontrará en estado REALIZED. Si se
ejecuta este método desde los estados REALIZED, STARTED o PREFETCHED la
petición será ignorada.
- removePlayerListener(PlayerListener listener): elimina un objeto PlayerListener asociado al reproductor.
- setLoopCount(int count): permite indicar el número de veces que un Player
debe reproducir su contenido en forma de bucle (al llegar al final de
los datos se continúa sin interrupción por el principio). El valor por
defecto de todo reproductor es 1. El valor 0 es un argumento no
permitido que producirá una excepción en caso de ser utilizado y el
valor -1 hará que la reproducción tenga una duración indefinida.
- setMediaTime(int time):
fija el tiempo de reproducción o "media time". Indica en que punto de
los datos debe comenzar la reproducción referido al instante inicial
(tiempo de reproducción = 0 para el inicio de los datos).
- start(): inicia la reproducción lo antes posible. Si el reproductor fue detenido con anterioridad por una llamada a stop() al invocar este método se continuará con la reproducción en el punto que se detuvo.
Al terminar la ejecución de este método el reproductor debe haber
comenzado a reproducir los datos y se debe haber generado un evento
STARTED. No necesariamente se producirá que el Player se encuentre en el estado STARTED ya que puede ocurrir que la duración sea muy corta o haya muy pocos datos por reproducir.
- stop():
detiene el reproductor en el instante de reproducción actual (como se
dijo anteriormente es a partir de este punto desde donde se reaunudaría
la reproducción si invocásemos start()). A la vuelta de este método el reproductor estará en estado PREFETCHED y se habrá generado un evento STOPPED
|
|
En el apartado anterior en el que analizamos con detalle el interfaz Player apareció el concepto de PlayerListener.
Este interfaz implementa un mecanismo de detección de eventos
síncronos generados por los reproductores que nos permiten tener acceso
a su estado y control sobre las acciones que ejecuta. Es un mecanismo
muy útil ya que de no disponer de él no podríamos saber cómo y cuándo
se produce la transición entre estados de un reproductor.
Para que una aplicación pueda trabajar con este tipo de eventos debe implementar el interfaz PlayerListener y debe asociar un listener a un Player concreto utilizando el método addPlayerListener(PlayerListener listener)
que vimos anteriormente. Este interfaz define una serie de eventos
definidos como Strings. Sin embargo se permite la definición de eventos
propietarios, es decir, eventos definidos por una implementación
concreta, aunque estos tendrán una definición diferente para evitar
colisiones con los anteriores. Este interfaz consta de un único método:
|
|
Ya pudimos ver en la introducción de este
tutorial que uno de los elementos básicos de la arquitectura del API
multimedia es el Controlador o Control. Por tanto necesitamos una
entidad que nos permita acceder a dicho controlador y dicha entidad
será la interfaz Controllable, que nos permite acceder a los controles de los objetos que la implementen (en nuestro caso se tratará del objeto Player).
Los métodos que encontramos en esta clase son:
- getControl(String controlType): devuelve el controlador (objeto Control)
del objeto que implemente el interfaz. Este método nos permite indicar
el tipo de controlador al que queremos acceder. Esto es necesario ya
que será más de un tipo de controlador el que implemente la interfaz
genérica Control (que veremos más adelante).
- getControls():
devuelve una lista con todos los controladores del objeto que
implemente este interfaz. Una vez devuelta la lista, antes de empezar a
trabajar con ellos debemos comprobar de qué tipo es cada controlador.
|
|
Este interfaz nos permite definir unos objetos
que proporciona una agrupación lógica de una serie de funciones de
procesado multimedia. Como pudimos ver en la introducción este objeto
controlador estará asociado a un reproductor concreto y será utilizado
para extender sus funciones de procesado multimedia. La manera de
acceder a los controladores es a partir del interfaz Controllable. Así, Player heredará de Controllable
de manera que podamos obtener como hemos dicho los controladores
asociados al reproductor. Además un mismo reproductor puede implementar
más de un tipo de Control.
MIDP 2.0 define dos subinterfaces de Control:
Estos dos nuevos interfaces pertenecen al paquete javax.microedition.media.control (que junto al paquete javax.microedition.media conforma el API multimedia de MIDP 2.0). |
|
ToneControl
es un interfaz que permite la reproducción de secuencias de tonos
definidas por el programador. Estas secuencias se definen como una
lista de pares (nota - duración). Esta lista se codifica como
un array de bytes. Al tratarse de una interfaz no disponemos de un
método constructor sino que se debe obtener utilizando el método getControl(String controlType) del interfaz Controllable (o de Player como subinterfaz del anterior )
El método que nos ofrece este interfaz para el manejo de secuencias de tonos es:
|
|
Este interfaz que permite manipular el volumen del audio reproducido por un
Player. Los aspectos más destacados de
VolumeControl son:
- Fijación del volumen a un valor determinado. Se indicará dicho nivel con un entero entre 0 (silencio) y 100 (volumen máximo).
- Mute on/off. Sin modificar el nivel de volumen puede o no silenciar la reproducción.
- Eventos de cambio de volumen. Cada vez que cambie el nivel de
volumen de la reproducción se producirá un evento VOLUME_CHANGED que
podrá ser capturado por el listener asociado al Player.
Los métodos que nos ofrece este interfaz para implementar estas acciones son los siguientes:
- getLevel():
devuelve el nivel de volumen del reproductor. Puede devolver -1 si y
sólo si el reproductor está en estado REALIZED y aún no se ha fijado un
volumen determinado.
- isMuted(): indica si el reproductor ha sido forzado o no a permanecer en silencio.
- setLevel(int level):
fija el volumen a un nivel determinado. El nivel indicado debe estar
comprendido entre 0 y 100. Si superase por arriba o por abajo estos
márgenes se fijaría automáticamente a 100 o 0 respectivamente. La
invocación de este método provoca la generación de un evento si se
produce correctamente el cambio de volumen.
- setMute(boolean mute):
fuerza al reproductor a estar o no en silencio. Este método no
modifica el nivel de volumen pero sí genera un evento VOLUME_CHANGED.
No hay comentarios.:
Publicar un comentario