Reproducción y Control de Medios en Aplicaciones Android

A menudo, es conveniente reproducir contenido multimedia cuando una aplicación no está en primer plano. Por ejemplo, un reproductor de música suele seguir reproduciendo música cuando el usuario bloqueó su dispositivo o está usando otra aplicación. La capacidad de reproducir contenido multimedia es una característica presente en la práctica totalidad de los dispositivos telefónicos actuales, y muchos usuarios prefieren utilizar las capacidades multimedia de su teléfono en lugar de depender de otro dispositivo adicional.

En este artículo, exploraremos cómo añadir contenido multimedia en aplicaciones Android, centrándonos en la reproducción de audio y vídeo, así como en las técnicas para mantener el control y la continuidad de la reproducción, incluso en segundo plano o cuando la aplicación no está activa.

La Clase MediaPlayer para la Reproducción de Medios

La reproducción de contenido multimedia en Android se lleva a cabo principalmente mediante la clase MediaPlayer. Esta clase permite la reproducción de archivos multimedia almacenados como recursos de la aplicación, en ficheros locales, en proveedores de contenido o servidos mediante streaming a partir de una URL. Como desarrolladores, la clase MediaPlayer nos permite abstraernos del formato y del origen del fichero a reproducir.

Tipos de Fuentes de Audio

Podemos reproducir audio proveniente de diferentes fuentes:

  • Un recurso de la aplicación: Incluir un fichero de audio en los recursos de la aplicación es muy sencillo. Simplemente se crea una carpeta raw dentro de la carpeta res y se almacenan allí los ficheros sin comprimir. Se identificarán en el código como R.raw.nombre_fichero (sin especificar la extensión).
  • Un fichero en el dispositivo local o un servidor remoto: Para reproducir un fichero almacenado en el móvil o en un servidor remoto, se debe especificar su URL (local o remota).

Importante: En caso de necesitar reproducir un medio alojado en una URL remota, será necesario que la aplicación solicite el permiso INTERNET en el fichero AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Ciclo de Vida y Métodos Esenciales del MediaPlayer

Inicialización del Reproductor de Medios

Para reproducir un fichero de audio, se deben seguir una secuencia de pasos. En primer lugar, se debe crear una instancia de la clase MediaPlayer e indicar el fichero a reproducir.

Existen dos opciones para inicializar la reproducción multimedia:

  1. Mediante el método setDataSource(): Este método asigna una fuente multimedia a una instancia ya existente de MediaPlayer. Al instanciar la clase, se encuentra en estado idle. Después de indicar el fichero, pasa a estado initialized. Para que el reproductor esté listo para la reproducción (inicializar búferes, etc.), se debe llamar al método prepare(), lo que lo lleva al estado prepared.
  2. Mediante el método create(): Esta opción crea una instancia de MediaPlayer pasando como parámetro el contexto de la aplicación y el identificador del recurso. El método create() se encarga de asignar la fuente de audio y de pasar el reproductor directamente al estado prepared, eliminando la necesidad de llamar a prepare(). Aunque más sencillo, resulta menos flexible.

La llamada a prepare() puede ser costosa y producir un retardo considerable, especialmente con medios externos. Por ello, se debe evitar llamarla desde el hilo principal (hilo de eventos) para no bloquearlo. Una alternativa es crear un hilo secundario para la preparación del medio, o utilizar la variante prepareAsync(), que realiza la preparación de forma asíncrona. Se puede usar un MediaPlayer.OnPreparedListener con el método onPrepared() para ser notificado cuando el reproductor esté preparado y poder iniciar la reproducción. Este listener se registra con setOnPreparedListener().

Cuando se produce un error, el MediaPlayer pasa al estado Error. Es necesario restablecerlo antes de poder usarlo de nuevo.

Control de Reproducción

Una vez inicializada la instancia de MediaPlayer, la reproducción puede comenzar con el método start(). También es posible utilizar los métodos stop() y pause() para detener y pausar la reproducción.

  • Si la reproducción se detuvo con stop(), será imprescindible invocar prepare() antes de poder reproducirlo de nuevo con start().
  • Si la reproducción se detuvo con pause(), solo será necesario hacer una llamada a start() para continuar desde el punto donde se dejó.

Otros métodos útiles de la clase MediaPlayer incluyen:

  • setLooping(boolean): Especifica si el clip de audio debe repetirse automáticamente.
  • setScreenOnWhilePlaying(boolean): Mantiene la pantalla activada durante la reproducción, más relevante para vídeo.
  • setVolume(float leftVolume, float rightVolume): Modifica el volumen de los canales izquierdo y derecho (valores entre 0 y 1).
  • seekTo(int milliseconds): Permite avanzar o retroceder a un punto específico del archivo de audio.
  • getDuration(): Obtiene la duración total del clip de audio.
  • getCurrentPosition(): Obtiene la posición actual de la reproducción.

Gestión de Recursos y Liberación

Un objeto MediaPlayer puede consumir una cantidad significativa de recursos del sistema. Es crucial conservarlo solo el tiempo necesario y llamar explícitamente a release() cuando se haya finalizado la reproducción. Esto es importante porque el recolector de elementos no utilizados solo considera las necesidades de memoria, no otros recursos relacionados con medios.

Por ejemplo, si una actividad crea un reproductor en start(), debe asegurarse de destruirlo en stop() para evitar múltiples instancias simultáneas. Siempre se deben buscar oportunidades para liberar el MediaPlayer, especialmente si la reproducción no será posible durante un período prolongado (por ejemplo, tras perder el foco de audio), liberando el reproductor existente y creándolo de nuevo más tarde.

Esquema del ciclo de vida de un MediaPlayer en Android

Para asegurar que la CPU siga ejecutándose mientras el MediaPlayer reproduce, se puede llamar al método setWakeMode(). Si se transmite contenido multimedia a través de la red usando Wi-Fi, también es aconsejable mantener un WifiLock, el cual debe adquirirse y liberarse manualmente. Al parar o pausar la reproducción, se pueden liberar estos locks para ahorrar batería.

Gestión de Audio en el Sistema

Streams de Audio y Control de Volumen

Android reproduce el audio en diferentes streams según su naturaleza: música, alarmas, notificaciones, llamadas, etc. Cada uno tiene su propio nivel de volumen. Cuando el usuario manipula los botones de control de volumen del dispositivo, modifica el volumen del stream que esté sonando actualmente.

Al reproducir sonido con MediaPlayer, se puede especificar el stream de audio con setAudioStreamType(), llamando a este método en el estado idle antes de especificar la fuente de audio. Es importante destacar que no se puede especificar el stream de audio si se crea el MediaPlayer con el atajo create().

Si ningún audio está sonando, por defecto, los controles de volumen afectarán al stream del tono de llamada. Para indicar a qué stream debe afectar el control de volumen en un contexto específico (por ejemplo, el volumen de música en un videojuego), se utiliza el método setVolumeControlStream(). Cuando la actividad o fragmento que haya llamado a este método esté visible, el control de volumen afectará al stream de música.

Manejo del Foco de Audio (Audio Focus)

Para evitar problemas como la reproducción de música y la alerta de un mensaje simultáneamente, Android utiliza un sistema donde cada aplicación puede solicitar el foco para reproducir audio. Si una aplicación reproduce música y otra solicita el foco, la primera puede detenerse momentáneamente o bajar su volumen (técnica conocida como ducking) mientras la otra reproduce sonido, hasta que se le devuelva el foco.

Se solicita el foco para reproducir audio mediante el servicio AudioManager. Al solicitar el foco, se debe proporcionar un listener de tipo AudioManager.OnAudioFocusChangeListener. Este listener notificará sobre diferentes eventos de pérdida o ganancia de foco:

  • AUDIOFOCUS_LOSS: Pérdida definitiva del foco. Se debe detener y liberar el reproductor.
  • AUDIOFOCUS_LOSS_TRANSIENT: Pérdida temporal del foco. Se debe pausar el reproductor.
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: Pérdida temporal que permite simplemente reducir el volumen del audio actual (ducking).
  • AUDIOFOCUS_GAIN: Ganancia del foco. Se debe inicializar el reproductor si no lo estaba, reanudar la reproducción si se había pausado y restaurar el volumen original.

La característica de audio focus está disponible a partir de Android 2.2 (API 8). Para compatibilidad con versiones anteriores, se debe comprobar la versión de Android en el código.

Detección de Desconexión de Auriculares

En las aplicaciones móviles de música, al desconectar los auriculares, la reproducción suele detenerse automáticamente para evitar ruidos repentinos. Este comportamiento no es automático, sino que debe programarse. Para ello, se debe capturar el intent AUDIO_BECOMING_NOISY mediante un Broadcast Receiver. Dentro del Broadcast Receiver, se puede realizar la acción de detener la reproducción actual.

Reproducción en Segundo Plano y Controles Avanzados con MediaSessionService

Cuando el móvil no se esté utilizando, la pantalla y la CPU pueden apagarse automáticamente para ahorrar batería, deteniendo la música. Para evitarlo, es necesario adquirir un wake lock, que mantiene el móvil activo. Una vez se cuenta con el permiso correspondiente, se puede indicar al reproductor de medios que se necesita un wake lock parcial (la pantalla puede apagarse, pero no la CPU). Si se accede a un fichero remoto, también se debe indicar que no se apague la WiFi mediante un wifi lock.

Curso Android desde cero #39 | Reproducción de audio – Las clases MediaPlayer y SoundPool

Para implementar un reproductor de música o podcasts que funcione en segundo plano, es necesario iniciar el reproductor desde un Servicio en lugar de una actividad. La solución recomendada para la reproducción de contenido multimedia en Android es Jetpack Media3.

Usar MediaSessionService permite que clientes externos, como el Asistente de Google, los controles multimedia del sistema, los botones multimedia en dispositivos periféricos (como Wear OS o Android Auto), descubran el servicio, se conecten a él y controlen la reproducción sin acceder a la interfaz de usuario de la aplicación.

Detalles de MediaSessionService

  • El método onCreate() se llama cuando el primer controlador está a punto de conectarse, creando una instancia del servicio y iniciándolo.
  • El método onDestroy() se llama cuando el servicio se detiene.
  • Opcionalmente, se puede anular onTaskRemoved(Intent) para personalizar lo que sucede cuando el usuario descarta la aplicación de las tareas recientes.
  • En la actividad o el fragmento que contiene la IU del reproductor, se puede establecer un vínculo entre la IU y la sesión multimedia con un MediaController, que permite enviar comandos desde la IU al reproductor dentro de la sesión.
  • Cualquier aplicación puede intentar conectarse a la sesión o biblioteca multimedia. Para restringir el acceso a los controladores del sistema, a los controladores con permiso de control de contenido multimedia y a la propia aplicación, se puede usar ControllerInfo.isTrusted() para una verificación de acceso básica.

Es fundamental que un reproductor que se ejecuta en un servicio en segundo plano interactúe adecuadamente con el sistema para evitar una mala experiencia de usuario. Como con una Activity, todo el trabajo de un Service se realiza en un solo subproceso de forma predeterminada, por lo que los servicios deben procesar los intents entrantes rápidamente y evitar cálculos prolongados.

Notificaciones de Reproducción

Un MediaSessionService crea automáticamente una MediaNotification que funciona en la mayoría de los casos, mostrando los controles de reproducción de la sesión multimedia. Esta notificación no se puede quitar mientras el servicio se ejecuta en primer plano.

Si el reproductor está en pausa, detenido o falló durante más de 10 minutos sin interacciones del usuario, el servicio pasa automáticamente del estado de servicio en primer plano para que el sistema pueda destruirlo.

Los metadatos sobre el elemento que se reproduce actualmente se pueden personalizar modificando MediaItem.MediaMetadata. Para una notificación enriquecida, se recomienda completar campos como title y artworkData o artworkUri del elemento actual con valores disponibles localmente, ya que el acceso a la red podría no estar disponible durante el arranque.

Ejemplo de notificación de reproducción con controles multimedia

Reanudación de la Reproducción

Después de que MediaSessionService finaliza, e incluso después de reiniciar el dispositivo, es posible ofrecer la reanudación de la reproducción para permitir que los usuarios reinicien el servicio y continúen la reproducción donde la dejaron. De forma predeterminada, la reanudación de la reproducción está desactivada. Este método se llama durante el tiempo de arranque para crear la notificación de reanudación de la IU del sistema Android después de reiniciar el dispositivo, con isForPlayback establecido en false.

Controles de Notificación

Aunque la reproducción se produzca en segundo plano, el usuario querrá estar al tanto de la reproducción y poder detenerla. Por ello, es conveniente que el servicio funcione en modo foreground. Se debe crear una notificación de tipo ongoing (evento en curso) que se mostrará en la barra de notificaciones mientras el servicio esté activo. Llamando a startForeground() desde el servicio, dicha notificación se mostrará. A la notificación se le puede añadir un PendingIntent para que, al pulsar sobre ella, se abra la actividad del reproductor, permitiendo al usuario pausar, detener o cambiar la música.

Cuando se desee que el servicio deje de funcionar en modo foreground, se debe llamar a un método específico para ello. También se pueden incluir controles directamente en la propia notificación para parar o reanudar el audio, utilizando un objeto RemoteViews para cargar un diseño personalizado. Al construir la notificación, se puede establecer la vista remota a aplicar con setContent().

Al pulsar los botones de la notificación, se lanzará un intent con acciones como ACTION_PLAY o ACTION_PAUSE, definidas por la aplicación. El servicio debe actualizarse para que se ejecute cuando se lancen estas acciones, añadiéndolas a un <intent-filter>.

A partir de Android 4.0, se podía usar la clase RemoteControlClient para estos controles, y a partir de Android 5.0, este mecanismo fue reemplazado por la clase MediaSession, que es la base de MediaSessionService.

Reproducción de Vídeo mediante VideoView

La reproducción de vídeo es muy similar a la reproducción de audio, con dos particularidades:

  1. No es posible reproducir un clip de vídeo almacenado directamente como parte de los recursos de la aplicación usando un ID de recurso. En su lugar, se puede representar un recurso raw de tipo vídeo mediante una URL con el formato: android.resource://[paquete_app]/[id_recurso].
  2. El vídeo necesita una superficie para poder reproducirse, que se corresponde con una vista dentro del layout de la actividad.

La alternativa más sencilla para la reproducción de vídeo es el uso de una vista de tipo VideoView. Esta encapsula la creación de una superficie para el vídeo y su control mediante una instancia de MediaPlayer.

El primer paso consiste en añadir la vista VideoView a la interfaz gráfica de la aplicación en el archivo de layout. Luego, en el código Java, se accede al elemento mediante findViewById(). Una vez hecho esto, se asigna una fuente (URL o URI) que se corresponderá con el contenido multimedia a reproducir. VideoView se encargará de la inicialización del objeto MediaPlayer.

Una vez inicializada la vista, la reproducción se puede controlar con métodos como start(), stopPlayback(), pause() y seekTo(). La clase VideoView también incorpora el método setKeepScreenOn(boolean) para controlar el comportamiento de la iluminación de la pantalla durante la reproducción del vídeo.

tags: #como #mantener #la #pausa #en #reproduccion