A menudo, se recomienda reproducir contenido multimedia mientras una app no está en primer plano. Por ejemplo, un reproductor de música generalmente sigue reproduciendo música cuando el usuario bloquea su dispositivo o usa otra app. La biblioteca Media3 proporciona una serie de interfaces que te permiten admitir la reproducción en segundo plano.
Cómo usar un MediaSessionService
Para habilitar la reproducción en segundo plano, debes contener Player
y MediaSession
en un Service independiente.
Esto permite que el dispositivo siga entregando contenido multimedia incluso cuando tu app no está en primer plano.
Cuando alojas un jugador en un servicio, debes usar un MediaSessionService
.
Para ello, crea una clase que extienda MediaSessionService
y crea tu sesión multimedia dentro de esta.
El uso de MediaSessionService
permite que clientes externos, como Asistente de Google, controles multimedia del sistema o dispositivos complementarios, como Wear OS, descubran tu servicio, se conecten a él y controlen la reproducción, todo sin acceder a la actividad de la IU de tu app. De hecho, puede haber varias apps cliente conectadas al mismo MediaSessionService
al mismo tiempo, cada una con su propio MediaController
.
Implementa el ciclo de vida del servicio
Debes implementar tres métodos de ciclo de vida de tu servicio:
- Se llama a
onCreate()
cuando el primer controlador está a punto de conectarse y se crea una instancia y se inicia el servicio. Es el mejor lugar para compilarPlayer
yMediaSession
. - Se llama a
onTaskRemoved(Intent)
cuando el usuario descarta la app de las tareas recientes. Si la reproducción está en curso, la app puede optar por mantener el servicio ejecutándose en primer plano. Si se pausa el reproductor, el servicio no está en primer plano y se debe detener. - Se llama a
onDestroy()
cuando se detiene el servicio. Se deben liberar todos los recursos, incluidos el jugador y la sesión.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // Create your player and media session in the onCreate lifecycle event override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } // The user dismissed the app from the recent tasks override fun onTaskRemoved(rootIntent: Intent?) { val player = mediaSession?.player!! if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == Player.STATE_ENDED) { // Stop the service if not playing, continue playing in the background // otherwise. stopSelf() } } // Remember to release the player and media session in onDestroy override fun onDestroy() { mediaSession?.run { player.release() release() mediaSession = null } super.onDestroy() } }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // Create your Player and MediaSession in the onCreate lifecycle event @Override public void onCreate() { super.onCreate(); ExoPlayer player = new ExoPlayer.Builder(this).build(); mediaSession = new MediaSession.Builder(this, player).build(); } // The user dismissed the app from the recent tasks @Override public void onTaskRemoved(@Nullable Intent rootIntent) { Player player = mediaSession.getPlayer(); if (!player.getPlayWhenReady() || player.getMediaItemCount() == 0 || player.getPlaybackState() == Player.STATE_ENDED) { // Stop the service if not playing, continue playing in the background // otherwise. stopSelf(); } } // Remember to release the player and media session in onDestroy @Override public void onDestroy() { mediaSession.getPlayer().release(); mediaSession.release(); mediaSession = null; super.onDestroy(); } }
Como alternativa a mantener la reproducción continua en segundo plano, una app puede detener el servicio en cualquier caso cuando el usuario la descarte:
Kotlin
override fun onTaskRemoved(rootIntent: Intent?) { val player = mediaSession.player if (player.playWhenReady) { // Make sure the service is not in foreground. player.pause() } stopSelf() }
Java
@Override public void onTaskRemoved(@Nullable Intent rootIntent) { Player player = mediaSession.getPlayer(); if (player.getPlayWhenReady()) { // Make sure the service is not in foreground. player.pause(); } stopSelf(); }
Proporciona acceso a la sesión multimedia
Anula el método onGetSession()
para otorgar acceso a otros clientes a tu sesión multimedia que se compiló cuando se creó el servicio.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // [...] lifecycle methods omitted override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // [...] lifecycle methods omitted @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } }
Declara el servicio en el manifiesto
Una app requiere permiso para ejecutar un servicio en primer plano. Agrega el permiso FOREGROUND_SERVICE
al manifiesto y, si te orientas al nivel de API 34 y versiones posteriores, también FOREGROUND_SERVICE_MEDIA_PLAYBACK
:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
También debes declarar tu clase Service
en el manifiesto con un filtro de intents de MediaSessionService
.
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
Debes definir un objeto foregroundServiceType
que incluya mediaPlayback
cuando tu app se ejecute en un dispositivo con Android 10 (nivel de API 29) y versiones posteriores.
Controlar la reproducción con un MediaController
En la actividad o el fragmento que contiene la IU del reproductor, puedes establecer un vínculo entre la IU y la sesión multimedia con un MediaController
. La IU usa el controlador multimedia para enviar comandos desde la IU al reproductor dentro de la sesión. Consulta la guía Cómo crear un MediaController
para obtener detalles sobre cómo crear y usar un MediaController
.
Cómo controlar los comandos de la IU
MediaSession
recibe comandos del controlador a través de su MediaSession.Callback
. Cuando inicializas un MediaSession
, se crea una implementación predeterminada de MediaSession.Callback
que controla automáticamente todos los comandos que MediaController
envía a tu jugador.
Notificación
Un MediaSessionService
crea automáticamente un MediaNotification
que debería funcionar en la mayoría de los casos. De forma predeterminada, la notificación publicada es una notificación MediaStyle
que se mantiene actualizada con la información más reciente de tu sesión multimedia y muestra los controles de reproducción. MediaNotification
reconoce tu sesión y se puede usar para controlar la reproducción de cualquier otra app que esté conectada a la misma sesión.
Por ejemplo, una app de transmisión de música que usa un MediaSessionService
crearía un MediaNotification
que muestra el título, el artista y la portada del álbum del elemento multimedia actual que se reproduce junto con los controles de reproducción según la configuración de MediaSession
.
Los metadatos necesarios se pueden proporcionar en el contenido multimedia o declararse como parte del elemento multimedia, como en el siguiente fragmento:
Kotlin
val mediaItem = MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build() ) .build() mediaController.setMediaItem(mediaItem) mediaController.prepare() mediaController.play()
Java
MediaItem mediaItem = new MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( new MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build()) .build(); mediaController.setMediaItem(mediaItem); mediaController.prepare(); mediaController.play();
Las apps pueden personalizar los botones de comando de los controles multimedia de Android. Obtén más información para personalizar los controles multimedia de Android.
Personalización de notificaciones
Para personalizar la notificación, crea un MediaNotification.Provider
con DefaultMediaNotificationProvider.Builder
o crea una implementación personalizada de la interfaz del proveedor. Agrega tu
proveedor a tu MediaSessionService
con
setMediaNotificationProvider
.
Reanudación de la reproducción
Los botones multimedia son botones de hardware que se encuentran en dispositivos Android y otros dispositivos periféricos, como el botón para reproducir o pausar en auriculares Bluetooth. Media3 controla las entradas de los botones de medios por ti cuando se ejecuta el servicio.
Declara el receptor del botón multimedia de Media3
Media3 incluye una API para permitir que los usuarios reanuden la reproducción después de que finalice una app o incluso después de que se reinicie el dispositivo. De forma predeterminada, la reanudación de la reproducción está desactivada, lo que significa que el usuario no puede reanudar la reproducción cuando el servicio no se está ejecutando. Para habilitarlo, comienza por declarar el elemento MediaButtonReceiver
en tu manifiesto:
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
Cómo implementar una devolución de llamada de reanudación de la reproducción
Cuando un dispositivo Bluetooth o la función de reanudación de la IU del sistema Android solicitan la reanudación de la reproducción, se llama al método de devolución de llamada onPlaybackResumption()
.
Kotlin
override fun onPlaybackResumption( mediaSession: MediaSession, controller: ControllerInfo ): ListenableFuture<MediaItemsWithStartPosition> { val settable = SettableFuture.create<MediaItemsWithStartPosition>() scope.launch { // Your app is responsible for storing the playlist and the start position // to use here val resumptionPlaylist = restorePlaylist() settable.set(resumptionPlaylist) } return settable }
Java
@Override public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption( MediaSession mediaSession, ControllerInfo controller ) { SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create(); settableFuture.addListener(() -> { // Your app is responsible for storing the playlist and the start position // to use here MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist(); settableFuture.set(resumptionPlaylist); }, MoreExecutors.directExecutor()); return settableFuture; }
Si almacenaste otros parámetros, como la velocidad de reproducción, el modo de repetición o el modo aleatorio, onPlaybackResumption()
es un buen lugar para configurar el reproductor con estos parámetros antes de que Media3 prepare el reproductor e inicie la reproducción cuando finalice la devolución de llamada.
Configuración avanzada del control y retrocompatibilidad
Una situación común es usar un MediaController
en la IU de la app para controlar la reproducción y mostrar la playlist. Al mismo tiempo, la sesión se expone a clientes externos, como los controles multimedia de Android y Asistente en dispositivos móviles o TVs, Wear OS para relojes y Android Auto en vehículos. La app de demostración de sesiones de Media3 es un ejemplo de una app que implementa esta situación.
Estos clientes externos pueden usar APIs como MediaControllerCompat
de la biblioteca de AndroidX heredada o android.media.session.MediaController
del framework de Android. Media3 es totalmente retrocompatible con la biblioteca heredada y proporciona interoperabilidad con la API del framework de Android.
Cómo usar el controlador de notificaciones multimedia
Es importante comprender que estos controladores heredados o de framework leen los mismos valores de los frameworks PlaybackState.getActions()
y PlaybackState.getCustomActions()
. Para determinar las acciones y las acciones personalizadas de la sesión del framework, una app puede usar el controlador de notificaciones multimedia y establecer los comandos disponibles y el diseño personalizado. El servicio conecta el controlador de notificaciones multimedia a tu sesión, y la sesión usa el ConnectionResult
que muestra el onConnect()
de tu devolución de llamada para configurar acciones y acciones personalizadas de la sesión del framework.
En una situación solo para dispositivos móviles, una app puede proporcionar una implementación de MediaSession.Callback.onConnect()
para establecer los comandos disponibles y el diseño personalizado específicamente para la sesión del framework de la siguiente manera:
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { if (session.isMediaNotificationController(controller)) { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() val playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build() // Custom layout and available commands to configure the legacy/framework session. return AcceptedResultBuilder(session) .setCustomLayout( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward)) ) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default custom layout for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { if (session.isMediaNotificationController(controller)) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); Player.Commands playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS .buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build(); // Custom layout and available commands to configure the legacy/framework session. return new AcceptedResultBuilder(session) .setCustomLayout( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward))) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands without default custom layout for all other controllers. return new AcceptedResultBuilder(session).build(); }
Cómo autorizar a Android Auto a enviar comandos personalizados
Cuando se usa un MediaLibraryService
y para admitir Android Auto con la app para dispositivos móviles, el controlador de Android Auto requiere los comandos disponibles adecuados. De lo contrario, Media3 rechazaría los comandos personalizados entrantes de ese control:
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { val sessionCommands = ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available session commands to accept incoming custom commands from Auto. return AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default custom layout for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available commands to accept incoming custom commands from Auto. return new AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands without default custom layout for all other controllers. return new AcceptedResultBuilder(session).build(); }
La app de demostración de la sesión tiene un módulo de Automotive, que demuestra la compatibilidad con Automotive OS que requiere un APK independiente.