Cómo controlar y anunciar la reproducción con una MediaSession

Las sesiones multimedia proporcionan una forma universal de interactuar con un reproductor de audio o video. En Media3, el reproductor predeterminado es la clase ExoPlayer, que implementa la interfaz Player. La conexión de la sesión multimedia con el reproductor permite que una app anuncie la reproducción de contenido multimedia externamente y reciba comandos de reproducción de fuentes externas.

Los comandos pueden provenir de botones físicos, como el botón de reproducción de auriculares o el control remoto de la TV. También pueden provenir de apps cliente que tienen un controlador multimedia, como indicar "pausar" a Asistente de Google. La sesión multimedia delega estos comandos al reproductor de la app de música.

Cuándo elegir una sesión multimedia

Cuando implementas MediaSession, permites que los usuarios controlen la reproducción:

  • A través de sus auriculares A menudo, hay botones o interacciones táctiles que un usuario puede realizar en sus auriculares para reproducir o pausar contenido multimedia, o ir a la pista siguiente o anterior.
  • Hablar con el Asistente de Google Un patrón común es decir "Hey Google, pause" para pausar cualquier contenido multimedia que se esté reproduciendo en el dispositivo.
  • Por medio de su reloj Wear OS De esta manera, podrás acceder con mayor facilidad a los controles de reproducción más comunes mientras juegas en el teléfono.
  • Mediante Controles de contenido multimedia Este carrusel muestra controles para cada sesión multimedia en ejecución.
  • En la TV Permite acciones con botones de reproducción físicos, control de reproducción de la plataforma y administración de energía (por ejemplo, si la TV, la barra de sonido o el receptor de A/V se apagan o se cambia la entrada, la reproducción debe detenerse en la app).
  • Y cualquier otro proceso externo que necesite influir en la reproducción.

Esto es ideal para muchos casos de uso. En particular, debes considerar usar MediaSession en los siguientes casos:

  • Estás transmitiendo contenido de video de formato largo, como películas o TV en vivo.
  • Estás transmitiendo contenido de audio de formato largo, como podcasts o playlists de música.
  • Estás compilando una app para TV.

Sin embargo, no todos los casos de uso se adaptan bien a MediaSession. Es posible que quieras usar solo Player en los siguientes casos:

  • Estás mostrando contenido de formato corto, en el que la participación y la interacción del usuario son fundamentales.
  • No hay un solo video activo, por ejemplo, el usuario se desplaza por una lista y se muestran varios videos en la pantalla al mismo tiempo.
  • Estás reproduciendo un video de introducción o explicación único y esperas que el usuario lo mire activamente.
  • Tu contenido es sensible para la privacidad y no quieres que procesos externos accedan a los metadatos multimedia (por ejemplo, el modo Incógnito en un navegador).

Si tu caso de uso no se adapta a ninguno de los mencionados anteriormente, considera si estás de acuerdo con que tu app continúe con la reproducción cuando el usuario no interactúa de manera activa con el contenido. Si la respuesta es sí, te recomendamos elegir MediaSession. Si la respuesta es no, probablemente quieras usar Player en su lugar.

Cómo crear una sesión multimedia

Una sesión multimedia existe junto al reproductor que administra. Puedes crear una sesión multimedia con un objeto Context y un objeto Player. Debes crear e inicializar una sesión multimedia cuando sea necesario, como el método de ciclo de vida onStart() o onResume() del Activity o Fragment, o el método onCreate() de Service que posee la sesión multimedia y su reproductor asociado.

Para crear una sesión multimedia, inicializa un Player y proporciónalo a MediaSession.Builder de la siguiente manera:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Control automático del estado

La biblioteca Media3 actualiza automáticamente la sesión multimedia con el estado del reproductor. Por lo tanto, no necesitas controlar de forma manual la asignación del jugador a la sesión.

Este es un punto de quiebre del enfoque heredado, en el que necesitabas crear y mantener un PlaybackStateCompat de forma independiente del reproductor, por ejemplo, para indicar cualquier error.

ID de sesión único

De forma predeterminada, MediaSession.Builder crea una sesión con una string vacía como ID de sesión. Esto es suficiente si una app tiene la intención de crear una sola instancia de sesión, que es el caso más común.

Si una app desea administrar varias instancias de sesión al mismo tiempo, debe asegurarse de que el ID de cada sesión sea único. El ID de sesión se puede establecer cuando se compila la sesión con MediaSession.Builder.setId(String id).

Si ves una IllegalStateException que hace que la app falle con el mensaje de error IllegalStateException: Session ID must be unique. ID=, es probable que se haya creado una sesión de forma inesperada antes de que se haya liberado una instancia creada anteriormente con el mismo ID. Para evitar que las sesiones se filtren debido a un error de programación, esos casos se detectan y se notifican con una excepción.

Otórgales control a otros clientes

La sesión multimedia es la clave para controlar la reproducción. Te permite enrutar comandos de fuentes externas al reproductor que se encarga de reproducir el contenido multimedia. Estas fuentes pueden ser botones físicos, como el botón de reproducción de auriculares o el control remoto de la TV, o comandos indirectos, como instrucciones para pausar la reproducción, a Asistente de Google. Del mismo modo, es posible que quieras otorgar acceso al sistema Android para facilitar los controles de notificación y en la pantalla de bloqueo, o a un reloj Wear OS para controlar la reproducción desde la cara de reloj. Los clientes externos pueden usar un controlador multimedia para emitir comandos de reproducción a tu app de música. La sesión multimedia los recibe y, en última instancia, delega comandos al reproductor multimedia.

Diagrama que muestra la interacción entre un MediaSession y un MediaController.
Figura 1: El controlador multimedia facilita el paso de comandos de fuentes externas a la sesión multimedia.

Cuando un controlador está a punto de conectarse a tu sesión multimedia, se llama al método onConnect(). Puedes usar el ControllerInfo proporcionado para decidir si aceptar o rechaza la solicitud. Consulta un ejemplo de cómo aceptar una solicitud de conexión en la sección Cómo declarar los comandos disponibles.

Después de conectarse, un control puede enviar comandos de reproducción a la sesión. Luego, la sesión delega esos comandos al jugador. La sesión controla automáticamente los comandos de reproducción y playlist definidos en la interfaz de Player.

Otros métodos de devolución de llamada te permiten controlar, por ejemplo, las solicitudes de comandos de reproducción personalizados y la modificación de listas de reproducción. De manera similar, estas devoluciones de llamada incluyen un objeto ControllerInfo para que puedas modificar la forma en que respondes a cada solicitud por controlador.

Modificar la playlist

Una sesión multimedia puede modificar directamente la playlist de su reproductor, como se explica en la guía de ExoPlayer para playlists. Los controladores también pueden modificar la playlist si COMMAND_SET_MEDIA_ITEM o COMMAND_CHANGE_MEDIA_ITEMS están disponibles para el control.

Cuando se agregan elementos nuevos a la playlist, el reproductor suele requerir instancias de MediaItem con un URI definido para que se puedan reproducir. De forma predeterminada, los elementos recién agregados se reenvían automáticamente a los métodos del reproductor, como player.addMediaItem, si tienen un URI definido.

Si quieres personalizar las instancias de MediaItem que se agregaron al reproductor, puedes anular onAddMediaItems(). Este paso es necesario cuando deseas admitir controladores que solicitan contenido multimedia sin un URI definido. En su lugar, MediaItem suele tener uno o más de los siguientes campos configurados para describir el contenido multimedia solicitado:

  • MediaItem.id: Es un ID genérico que identifica el contenido multimedia.
  • MediaItem.RequestMetadata.mediaUri: Es un URI de solicitud que puede usar un esquema personalizado y que el reproductor no necesariamente puede reproducirlo directamente.
  • MediaItem.RequestMetadata.searchQuery: Es una búsqueda de texto, por ejemplo, desde Asistente de Google.
  • MediaItem.MediaMetadata: Metadatos estructurados, como “título” o “artista”.

Para obtener más opciones de personalización para playlists completamente nuevas, también puedes anular onSetMediaItems(), que te permite definir el elemento de inicio y la posición en la playlist. Por ejemplo, puedes expandir un solo elemento solicitado a una playlist completa y, luego, indicarle al reproductor que comience en el índice del elemento solicitado originalmente. Puedes encontrar una implementación de ejemplo de onSetMediaItems() con esta función en la app de demostración de la sesión.

Cómo administrar diseños y comandos personalizados

En las siguientes secciones, se describe cómo anunciar un diseño personalizado de botones de comando personalizados a las apps cliente y cómo autorizar a los controladores para que envíen los comandos personalizados.

Define un diseño personalizado de la sesión

Para indicarles a las apps cliente qué controles de reproducción quieres mostrarles al usuario, configura el diseño personalizado de la sesión cuando compiles MediaSession en el método onCreate() de tu servicio.

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

Declara el reproductor disponible y los comandos personalizados

Las aplicaciones multimedia pueden definir comandos personalizados que, por ejemplo, se pueden usar en un diseño personalizado. Por ejemplo, es posible que desees implementar botones que permitan al usuario guardar un elemento multimedia en una lista de elementos favoritos. MediaController envía comandos personalizados y MediaSession.Callback los recibe.

Puedes definir qué comandos de sesión personalizados están disponibles para un MediaController cuando se conecta a tu sesión multimedia. Para lograrlo, anula MediaSession.Callback.onConnect(). Configura y muestra el conjunto de comandos disponibles cuando aceptes una solicitud de conexión de un MediaController en el método de devolución de llamada onConnect:

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Para recibir solicitudes de comando personalizadas de un MediaController, anula el método onCustomCommand() en el Callback.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Puedes realizar un seguimiento de qué controlador multimedia realiza una solicitud mediante la propiedad packageName del objeto MediaSession.ControllerInfo que se pasa a los métodos Callback. Esto te permite adaptar el comportamiento de tu app en respuesta a un comando determinado si se origina en el sistema, tu propia app o en otras apps cliente.

Cómo actualizar el diseño personalizado después de una interacción del usuario

Después de controlar un comando personalizado o cualquier otra interacción con el reproductor, te recomendamos actualizar el diseño que se muestra en la IU del control. Un ejemplo típico es un botón de activación que cambia su ícono después de activar la acción asociada con este botón. Para actualizar el diseño, puedes usar MediaSession.setCustomLayout:

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

Personaliza el comportamiento de los comandos de reproducción

Para personalizar el comportamiento de un comando definido en la interfaz Player, como play() o seekToNext(), une tu Player en una ForwardingPlayer.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession = 
  new MediaSession.Builder(context, forwardingPlayer).build();

Para obtener más información sobre ForwardingPlayer, consulta la guía de ExoPlayer sobre personalización.

Identifica el controlador solicitante de un comando del jugador

Cuando un MediaController origina una llamada a un método Player, puedes identificar la fuente de origen con MediaSession.controllerForCurrentRequest y adquirir el ControllerInfo para la solicitud actual:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

Cómo responder a los botones multimedia

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/pausa en los auriculares Bluetooth. Media3 controla los eventos de botones de contenido multimedia por ti cuando llegan a la sesión y llama al método Player adecuado en el reproductor de sesión.

Una app puede anular el comportamiento predeterminado anulando MediaSession.Callback.onMediaButtonEvent(Intent). En ese caso, la app puede o necesita controlar todos los detalles de la API por su cuenta.