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

Las sesiones multimedia proporcionan una forma universal de interactuar con un audio o video de fútbol favorito. 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 para anunciar la reproducción de contenido multimedia externamente y para recibir comandos de reproducción de fuentes externas.

Los comandos pueden provenir de botones físicos, como el botón Reproducir en una auriculares o el control remoto de la TV. También pueden provenir de apps cliente que tengan controlador multimedia, como indicar "pausar" al Asistente de Google. Los medios de comunicación del usuario, 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 el usuario puede reproducir o pausar contenido multimedia en sus auriculares, o ir al siguiente o una pista anterior.
  • Hablar con el Asistente de Google Un patrón común es decir "OK Google, pause" para pausar cualquier contenido multimedia que se esté reproduciendo en el dispositivo.
  • Por medio de su reloj Wear OS Esto facilita el acceso a la mayoría controles de reproducción comunes mientras juegan en su teléfono.
  • Mediante Controles de contenido multimedia En este carrusel, se muestran controles para cada uno mientras se ejecuta una sesión multimedia.
  • En la TV Permite acciones con botones de reproducción físicos, reproducción en la plataforma y administración de energía (por ejemplo, si la TV, la barra de sonido o el receptor se apaga o se cambia la entrada, la reproducción debería detenerse en la aplicación).
  • Y cualquier otro proceso externo que necesite influir en la reproducción.

Esto es ideal para muchos casos de uso. En particular, debes considerar seriamente con 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 música listas de reproducción.
  • Estás compilando una app para TV.

Sin embargo, no todos los casos de uso se adaptan bien a MediaSession. Es posible que quieras usa 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 es crucial.
  • No hay un solo video activo, por ejemplo, el usuario se desplaza por una lista. y se muestran varios videos en pantalla al mismo tiempo.
  • Estás reproduciendo un video de introducción o explicación único, que esperan que los usuarios miren activamente.
  • Tu contenido es sensible para la privacidad y no quieres que los procesos externos Acceder a los metadatos del contenido multimedia (por ejemplo, el modo Incógnito en un navegador)

Si tu caso de uso no se adapta a ninguno de los anteriores, considera si Aceptas que la app continúe la reproducción cuando el usuario no participa activamente. con el contenido. Si la respuesta es sí, quizás quieras elegir MediaSession Si la respuesta es no, te recomendamos que uses Player. en su lugar.

Cómo crear una sesión multimedia

Una sesión multimedia existe junto al reproductor que administra. Puedes construir un sesión multimedia con un objeto Context y un objeto Player. Deberías crear y inicializan una sesión multimedia cuando sea necesario, como onStart() o Método de ciclo de vida onResume() de Activity, Fragment o onCreate() de la 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 el estado del jugador. Por eso, no necesitas manejar manualmente la asignación desde de un jugador a una sesión.

Este es un punto de quiebre del enfoque heredado, en el que debías crear y mantener un objeto PlaybackStateCompat independientemente 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 tiene la intención de crear solo un de sesión abierta, que es el caso más común.

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

Si ves una IllegalStateException que hace que la app falle con el error mensaje IllegalStateException: Session ID must be unique. ID=, entonces probable que una sesión se haya creado inesperadamente antes de una sesión con el mismo ID. Para evitar que un objeto de escucha filtra las sesiones programación, estos casos se detectan y notifican arrojando una excepción.

Otórgales control a otros clientes

La sesión multimedia es la clave para controlar la reproducción. Te permite enrutar de fuentes externas al reproductor que hace el trabajo de reproducir medios de comunicación. Estas fuentes pueden ser botones físicos, como el botón de reproducción de una auriculares o el control remoto de la TV, o bien comandos indirectos, como instrucciones para pausar la reproducción, al Asistente de Google. Asimismo, te recomendamos que otorgues acceso al Centro de ayuda de Android, para facilitar los controles de notificaciones y de la pantalla de bloqueo, o a un dispositivo reloj para que puedas 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 Son recibidos por tu sesión multimedia 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 comandos de fuentes externas a la sesión multimedia.

Cuando un controlador esté a punto de conectarse a tu sesión multimedia, el onConnect() método. Puedes usar el ControllerInfo proporcionado para decidir si aceptar o rechazar la solicitud. Para ver un ejemplo de aceptación de una solicitud de conexión, consulta el artículo Cómo declarar de comandos disponibles.

Después de conectarse, un control puede enviar comandos de reproducción a la sesión. El y, luego, delega esos comandos al reproductor. Reproducción y playlist los comandos definidos en la interfaz de Player se controlan automáticamente sesión.

Otros métodos de devolución de llamada permiten controlar, por ejemplo, las solicitudes de comandos de reproducción personalizados y modificación de la playlist). De manera similar, estas devoluciones de llamada incluyen un objeto ControllerInfo para que puedas modificar y cómo responderá a cada solicitud en función del responsable del tratamiento de datos.

Modificar la playlist

Una sesión multimedia puede modificar directamente la playlist de su reproductor como se explica en el 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á disponible para el controlador.

Cuando se agregan elementos nuevos a la playlist, el reproductor suele requerir MediaItem instancias con un URI definido para que se puedan jugar. De forma predeterminada, los nuevos elementos 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 si quieres admitir controladores que solicitan contenido multimedia. sin un URI definido. En cambio, 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 valor personalizado. y no siempre el reproductor puede reproducirlo directamente.
  • MediaItem.RequestMetadata.searchQuery: Una búsqueda textual, por ejemplo. de Asistente de Google.
  • MediaItem.MediaMetadata: Metadatos estructurados, como “título” o "artist".

Para obtener más opciones de personalización para playlists completamente nuevas, puedes anular adicionalmente onSetMediaItems() que te permite definir el elemento de inicio y la posición en la playlist. Por ejemplo: puedes expandir un único elemento solicitado a toda una lista de reproducción e indicarle al jugador desde el índice del elemento solicitado originalmente. R implementación de muestra de onSetMediaItems() con esta función se pueden encontrar 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 de comandos a las apps cliente y autoriza a los controladores a enviar la con comandos de SQL sencillos.

Define un diseño personalizado de la sesión

Para indicar a las apps cliente qué controles de reproducción quieres mostrar al usuario, establece el diseño personalizado de la sesión. Cuando compiles 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();
}

Declara el reproductor disponible y los comandos personalizados

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

Puedes definir qué comandos de sesión personalizados están disponibles para un Es MediaController cuando se conecta a tu sesión multimedia. Lo logras al que anula MediaSession.Callback.onConnect(). Configuración y devolución el conjunto de comandos disponibles al aceptar 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 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 envía una solicitud mediante el la propiedad packageName del objeto MediaSession.ControllerInfo que se se pasan a los métodos Callback. Esto te permite adaptar comportamiento en respuesta a un comando determinado si se origina en el sistema, tu tu propia app u otras apps cliente.

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

Luego de controlar un comando personalizado o cualquier otra interacción con el jugador, puedes es posible que desees actualizar 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 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 lo siguiente: Personalización.

Identifica el controlador solicitante de un comando del jugador

Cuando un MediaController origina una llamada a un método Player, puedes identifica la fuente de origen con MediaSession.controllerForCurrentRequest y adquiere 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 periféricos. como el botón de reproducción/pausa en los auriculares Bluetooth. Controladores de Media3 eventos de botones multimedia cuando lleguen a la sesión y llamen a el 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 necesita manejar todos los detalles específicos de la API por su cuenta.

Informes y manejo de errores

Hay dos tipos de errores que emite una sesión y que informa a los controladores. Los errores fatales informan una falla técnica de reproducción de la sesión reproductor que interrumpe la reproducción. Se informan los errores fatales al controlador. automáticamente cuando ocurren. Los errores recuperables no son técnicos ni de política errores que no interrumpen la reproducción y que el de forma manual.

Errores fatales de reproducción

El reproductor informa un error fatal de reproducción a la sesión y, luego, informados a los controladores para llamar 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 PlayerException.errorCode para obtener información sobre el motivo del error.

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

Personalización de un error grave

Para proporcionar información localizada y significativa al usuario, el código de error, el mensaje de error y los extras de errores de un error de reproducción no recuperable se pueden personalizar con un ForwardingPlayer cuando compiles 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 para el jugador. e intercepta devoluciones de llamada que informan un error. Una Luego, se delega PlaybackException a los objetos de escucha que se registran en el reproductor de reenvío. Para que funcione, el reproductor de reenvío anula Player.addListener y Player.removeListener para tener acceso al objetos de escucha con los que se pueden enviar códigos de error, mensajes o extras 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

Se pueden enviar errores recuperables que no se originen en una excepción técnica por una app para todos o para un controlador 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 recuperable 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 establece en PlaybackStateCompat, según corresponda, mientras que PlaybackStateCompat.state no se cambió a STATE_ERROR.

Cómo recibir errores recuperables

Una MediaController recibe un error recuperable tras implementar 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.
              }
            });