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. Conectar la sesión multimedia al reproductor permite que una app anuncie la reproducción de contenido multimedia de forma externa 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 unos auriculares o el control remoto de una TV. También pueden provenir de apps cliente que tienen un controlador de música, como cuando se le indica "pausar" al 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 bien ir a la pista siguiente o anterior.
  • Hablando con Asistente de Google. Un patrón común es decir "Hey Google, pausa" para pausar el contenido multimedia que se está reproduciendo en el dispositivo.
  • A través de su reloj Wear OS. Esto permite un acceso más fácil a los controles de reproducción más comunes mientras se reproduce contenido en el teléfono.
  • A través de los Controles multimedia. En este carrusel, se muestran los controles de cada sesión multimedia en ejecución.
  • En 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 debería detenerse en la app).
  • Y cualquier otro proceso externo que deba influir en la reproducción.

Esto es excelente para muchos casos de uso. En particular, te recomendamos que uses MediaSession en los siguientes casos:

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

Sin embargo, no todos los casos de uso se ajustan bien a MediaSession. Te recomendamos que solo uses Player en los siguientes casos:

  • Muestras contenido breve, en el que la participación y la interacción de los usuarios es fundamental.
  • 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, que esperas que el usuario mire de forma activa.
  • Tu contenido es sensible a la privacidad y no quieres que los procesos externos accedan a los metadatos de contenido multimedia (por ejemplo, el modo Incógnito en un navegador).

Si tu caso de uso no se ajusta a ninguno de los anteriores, considera si estás de acuerdo con que tu app continúe la reproducción cuando el usuario no esté interactuando de forma activa con el contenido. Si la respuesta es sí, es probable que quieras elegir MediaSession. Si la respuesta es no, es probable que quieras usar Player en su lugar.

Crea una sesión multimedia

Una sesión multimedia existe junto al reproductor que administra. Puedes crear una sesión de media 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() de 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();

Manejo automático de estado

La biblioteca Media3 actualiza automáticamente la sesión multimedia con el estado del reproductor. Por lo tanto, no es necesario que controles manualmente la asignación del reproductor a la sesión.

Esto es una ruptura con el enfoque heredado en el que debías 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 cadena vacía como el ID de sesión. Esto es suficiente si una app solo tiene la intención de crear una sola instancia de sesión, que es el caso más común.

Si una app quiere administrar varias instancias de sesión al mismo tiempo, debe asegurarse de que el ID de sesión 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 que un IllegalStateException hace que tu 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 publicado una instancia creada con el mismo ID. Para evitar que un error de programación divulgue las sesiones, estos casos se detectan y se notifican mediante el lanzamiento de una excepción.

Otorga 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 reproduce tu contenido multimedia. Estas fuentes pueden ser botones físicos, como el botón de reproducción en auriculares o el control remoto de la TV, o bien comandos indirectos, como indicar "pausar" a Asistente de Google. Del mismo modo, te recomendamos que otorgues acceso al sistema Android para facilitar los controles de notificaciones y pantalla de bloqueo, o a un reloj Wear OS para que puedas controlar la reproducción desde la cara de reloj. Los clientes externos pueden usar un controlador multimedia para enviar comandos de reproducción a tu app de música. Estos los recibe tu sesión multimedia, que, en última instancia, delega los comandos al reproductor multimedia.

Un diagrama que muestra la interacción entre un MediaSession y un MediaController.
Figura 1: El controlador de contenido 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 aceptas o rechazas la solicitud. Consulta un ejemplo de cómo aceptar una solicitud de conexión en la sección Cómo declarar comandos disponibles.

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

Otros métodos de devolución de llamada te permiten controlar, por ejemplo, solicitudes de comandos de reproducción personalizados y modificar la playlist. 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.

Modifica 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 controlador.

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 agregados recientemente se reenvían automáticamente a métodos de reproductores, como player.addMediaItem, si tienen un URI definido.

Si deseas personalizar las instancias de MediaItem que se agregaron al reproductor, puedes reemplazar onAddMediaItems(). Este paso es necesario cuando deseas admitir controladores que soliciten contenido multimedia sin un URI definido. En cambio, el 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 reproducir directamente.
  • MediaItem.RequestMetadata.searchQuery: Es una búsqueda textual, por ejemplo, de Asistente de Google.
  • MediaItem.MediaMetadata: Metadatos estructurados, como "título" o "artista".

Para obtener más opciones de personalización para playlists completamente nuevas, 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 ordenarle 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.

Administra el diseño personalizado y los comandos personalizados

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

Define el diseño personalizado de la sesión

Para indicar a las apps cliente qué controles de reproducción deseas mostrarle al usuario, configura el diseño personalizado de la sesión cuando compilas el 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();
}

Cómo declarar los comandos personalizados y del reproductor disponibles

Las aplicaciones multimedia pueden definir comandos personalizados que, por ejemplo, se pueden usar en un diseño personalizado. Por ejemplo, puedes implementar botones que le 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 comandos personalizados de un MediaController, anula el método onCustomCommand() en 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 hacer un seguimiento de qué controlador multimedia realiza una solicitud con 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 proviene del sistema, de tu propia app o de 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 que actualices el diseño que se muestra en la IU del controlador. 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 un ForwardingSimpleBasePlayer antes de pasarlo a MediaSession.

Kotlin

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

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

Java

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

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

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

Identifica el controlador solicitante de un comando del reproductor

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 CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Java

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

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 auriculares Bluetooth. Media3 controla los eventos de botones multimedia por ti cuando llegan a la sesión y llama al método Player apropiado 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 debe controlar todos los detalles de la API por su cuenta.

Manejo y generación de informes de errores

Existen dos tipos de errores que una sesión emite y, luego, informa a los controladores. Los errores fatales informan una falla técnica de reproducción del reproductor de sesión que interrumpe la reproducción. Los errores fatales se informan al controlador automáticamente cuando ocurren. Los errores recuperables son errores no técnicos o de políticas que no interrumpen la reproducción y que la aplicación envía a los controladores de forma manual.

Errores graves de reproducción

El reproductor informa un error irrecuperable de reproducción a la sesión y, luego, a los controladores para que llamen a través de Player.Listener.onPlayerError(PlaybackException) y Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

En ese caso, el estado de reproducción pasa a STATE_IDLE y MediaController.getPlaybackError() muestra el PlaybackException que causó la transición. Un controlador puede inspeccionar el PlayerException.errorCode para obtener información sobre el motivo del error.

Para la interoperabilidad, se replica un error irrecuperable en el PlaybackStateCompat de la sesión de la plataforma transfiriendo su estado a STATE_ERROR y configurando el código y el mensaje de error según PlaybackException.

Personalización de un error fatal

Para proporcionar información localizada y significativa al usuario, el código de error, el mensaje de error y los elementos adicionales de un error de reproducción irrecuperable se pueden personalizar con un ForwardingPlayer cuando se compila la sesión:

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Java

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

El reproductor de reenvío registra un Player.Listener en el reproductor real y intercepta las devoluciones de llamada que informan un error. Luego, se delega un PlaybackException personalizado a los objetos de escucha que están registrados en el reproductor de reenvío. Para que esto funcione, el reproductor de reenvío anula Player.addListener y Player.removeListener para tener acceso a los objetos de escucha con los que enviar un código de error, un mensaje o elementos adicionales personalizados:

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Errores recuperables

Una app puede enviar errores no fatales que no se originan en una excepción técnica a todos los controladores o a uno específico:

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Java

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Un error no fatal que se envía al controlador de notificaciones multimedia se replica en el PlaybackStateCompat de la sesión de la plataforma. Por lo tanto, solo el código de error y el mensaje de error se establecen en PlaybackStateCompat según corresponda, mientras que PlaybackStateCompat.state no se cambia a STATE_ERROR.

Cómo recibir errores recuperables

Un MediaController recibe un error recuperable cuando implementa MediaController.Listener.onError:

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });