Cómo reproducir contenido en segundo plano con un MediaSessionService

A menudo, se recomienda reproducir contenido multimedia mientras una app no está en primer plano. Para Por ejemplo, un reproductor de música suele seguir reproduciendo música cuando el usuario bloquea su dispositivo o usa otra app. La biblioteca Media3 ofrece una serie de 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 dentro de un Service independiente Esto permite que el dispositivo siga entregando contenido multimedia incluso cuando la app no esté en en primer plano.

MediaSessionService permite que la sesión multimedia se ejecute de forma independiente de la actividad de la app.
Figura 1: El elemento MediaSessionService permite el contenido multimedia sesión para que se ejecute independientemente de la actividad de la app

Cuando alojas un jugador dentro de un Service, debes usar un MediaSessionService. Para ello, crea una clase que extienda MediaSessionService y crea tu sesión de contenido multimedia dentro de ella.

Usar MediaSessionService permite que los clientes externos, como Asistente de Google, los controles multimedia del sistema o los 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, pueden conectarse varias apps cliente al mismo MediaSessionService a la vez, 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 control está por conectarse y el se crea una instancia y se inicia. Es el mejor lugar para crear Player y MediaSession
  • 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 se ejecutan en primer plano. Si el reproductor está en pausa, el servicio no está en primer plano y se debe detener.
  • Se llama a onDestroy() cuando se detiene el servicio. Todos los recursos incluidos el reproductor 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 Detén el servicio en cualquier caso cuando el usuario descarte la app:

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 a otros clientes acceso a la sesión de media 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;
  }
}

Cómo declarar 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 a la 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 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 tu sesión multimedia con un MediaController. Tu IU usa el controlador multimedia para enviar comandos de la IU al reproductor dentro de la sesión. Consulta la guía Crea 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 Si inicializas un MediaSession, se crea una implementación predeterminada de MediaSession.Callback que controla automáticamente todos los comandos que un MediaController envía a tu reproductor.

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. El MediaNotification reconoce tu sesión y puede usarse para controlar la reproducción de cualquier otra app que están conectadas a la misma sesión.

Por ejemplo, una app de transmisión de música que usa un MediaSessionService crearía un MediaNotification que muestre el título, el artista y la portada del álbum del elemento multimedia actual que se está reproduciendo 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 declarar 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 una MediaNotification.Provider con DefaultMediaNotificationProvider.Builder o creando 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 de medios son botones de hardware que se encuentran en dispositivos Android y otros dispositivos periféricos, como el botón de reproducción o pausa en auriculares Bluetooth. Contenido multimedia3 se encarga de las entradas del botón multimedia cuando se ejecuta el servicio.

Declara el receptor de botones de medios Media3

Media3 incluye una API que permite a los usuarios reanudar la reproducción después de que se cierre una app e incluso después de que el dispositivo reiniciar. De forma predeterminada, la reanudación de la reproducción está desactivada. Esto significa que el usuario no puede reanudar la reproducción cuando tu servicio no se está ejecutando. Para habilitar esta función, primero declara 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 la devolución de llamada para reanudar la reproducción

Cuando un dispositivo Bluetooth o el Función de reanudación de la IU del sistema Android el onPlaybackResumption() de devolución de llamada.

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 lo prepare y comience a reproducir cuando se complete 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 automóviles. La app de demostración de sesiones de Media3 es un ejemplo de una app en la que se implementa esa situación.

Estos clientes externos pueden usar APIs como MediaControllerCompat de la heredada Biblioteca de AndroidX o android.media.session.MediaController de Android en un framework de nube. 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 PlaybackState.getActions() y PlaybackState.getCustomActions() del framework. 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 los medios de notificaciones a tu sesión, que usará el El onConnect() de tu devolución de llamada muestra ConnectionResult para configurarlo. y las 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();
}

Otorga permiso a Android Auto para enviar comandos personalizados

Cuando se usa un MediaLibraryService y se admite 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 controlador:

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 demo de sesión tiene un módulo automotriz, que demuestra la compatibilidad con Automotive OS, que requiere un APK independiente.