Home UC3M
Home IT

Multimedia en MIDP 2.0

 INTRODUCCIÓN

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.

Figura1
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.



 CLASE Manager

Manager es como ya se ha dicho el punto de partida para procesar o reproducir datos multimedia. Se nos ofrecen dos opciones:

  • Reproducción de tonos simples

    La clase Manager nos ofrece el método playTone que a partir de una serie de parámetros reproduce un tono simple. Este método se explicará con más detalle posteriormente, cuando se vean todos los métodos de la clase Manager.

  • Creación de objetos Player

    Es este mecanismo el que permite aprovechar toda la funcionalidad que la clase Manager nos ofrece. Podremos obtener objetos Player a partir de un InputStream (cuando tengamos un flujo de bits) o a partir de un localizador multimedia (Media Locator). Los métodos disponibles para realizar estas acciones son los siguientes:

    Manager.createPlayer(java.io.InputStream is, String type)
    Manager.createPlayer(String locator)
    

    Los objetos Player así creados serán utilizados para el control y la reproducción multimedia con la única restricción de que los datos manipulados deben pertenecer específicamente al tipo sobre el que se creó el objeto. Esto es conocido como el content-type de los datos. Algunos ejemplos son:

    • Ficheros "wave audio": audio/x-wav
    • Ficheros AU:audio/basic
    • Ficheros MP3:audio/mpeg
    • Ficheros MIDI:audio/midi
    • Secuencias de tonos simples:audio/x-tone-seq

    Si observamos los dos casos anteriores de creación de objetos Player vemos que en el primero de ellos se indica EXPLÍCITAMENTE el content-type de los datos sobre los que se crea el reproductor. En el segundo de los casos la declaración del tipo de datos es IMPLÍCITA, está incluida en el localizador.

    Más adelante se entrará a analizar más a fondo el interfaz Player.

API de la clase Manager

Los métodos de los que dispone esta clase son:

  • createPlayer(InputStream is, String contentType): crea un objeto Player para la reproducción multimedia de los datos ofrecidos por un flujo de bits de entrada. Como ya se ha comentado, el parámetro contentType define el tipo de los datos recibidos. Si tiene un valor null, Manager tratará de determinar el tipo más adecuado. Sin embargo esta operación no resulta trivial en muchos casos, lo que puede significar que no sea posible llevarla a cabo. En estos casos se producirá una MediaException.


  • createPlayer(String locator):crea un objeto Player a partir de un localizador que tendrá el siguiente formato:
    < protocol >:< location >
    
    Donde,
    • protocol: indica el protocolo utilizado para recibir los datos multimedia
    • location: indica la localización de los datos multimedia. Es aquí donde se indica implícitamente el content type de los datos.


    Ejemplo:

      Player p = Manager.createPlayer("http://webserver/music.mp3");


  • 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.


  •  EL INTERFAZ Player

    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.

    Figura2
    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


     EL INTERFAZ PlayerListener

    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:

     EL INTERFAZ Controllable

    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.



     EL INTERFAZ Control


    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).


     EL INTERFAZ ToneControl


    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:


     EL INTERFAZ VolumeControl


    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.
     EJEMPLOS

    Ejemplo de utilizacion de las clases e interfaces anteriormente descritas
     ENLACES

    API MIDP/CLDC


    inicio | tablón| contacta