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 ExoPlayer clase, que implementa la Player interfaz. 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 un control remoto de auriculares o TV. También pueden provenir de apps cliente que tienen un controlador multimedia, como dar la instrucción "pausar" a Asistente de Google. La sesión multimedia delega estos comandos al reproductor de la app de contenido multimedia.

Cuándo elegir una sesión multimedia

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

  • 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.
  • Hablando con el Asistente de Google. Un patrón común es decir "OK Google, pausar" para pausar cualquier 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. 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 A/V se apagan o se cambia la entrada, la reproducción debería detenerse en la app).
  • A través de los controles multimedia de Android Auto. Esto permite un control de reproducción seguro mientras conduces.
  • 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 creando una app para TV.

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

  • Estás mostrando contenido de formato corto, en el que no se necesita control externo ni reproducción en segundo plano.
  • No hay un solo video activo, como cuando 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 sin necesidad de controles de reproducción externos.
  • Tu contenido es sensible a la privacidad y no deseas que los procesos externos accedan a los metadatos multimedia (por ejemplo, el modo Incógnito en un navegador).

Si tu caso de uso no se ajusta a ninguno de los mencionados anteriormente, considera si estás de acuerdo con que tu app continúe reproduciendo contenido cuando el usuario no interactúe de forma activa con él. Si la respuesta es sí, probablemente quieras elegir MediaSession. Si la respuesta es no, probablemente quieras usar Player en su lugar.

Crea una sesión multimedia

Una sesión multimedia existe junto al reproductor que administra. Puedes construir 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() 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 estados

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

Esto es diferente de la sesión multimedia de la plataforma, en la que debías crear y mantener un PlaybackState 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 ID de sesión. Esto es suficiente si una app solo pretende crear una 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 configurar cuando se compila la sesión con MediaSession.Builder.setId(String id).

Si ves que IllegalStateException falla en tu app con el error mensaje IllegalStateException: Session ID must be unique. ID= entonces es probable que se haya creado una sesión de forma inesperada antes de que se haya lanzado una instancia creada anteriormente con el mismo ID. Para evitar que las sesiones se filtren por un error de programación, se detectan y notifican estos casos mediante la generación 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 se encarga de reproducir tu contenido multimedia. Estas fuentes pueden ser botones físicos, como el botón de reproducción de un control remoto de auriculares o TV, o comandos indirectos, como dar la instrucción "pausar" a Asistente de Google. Del mismo modo, es posible que desees otorgar acceso al sistema Android para facilitar las notificaciones y los controles de la pantalla de bloqueo, o a un reloj Wear OS para que puedas controlar la reproducción desde la esfera del reloj. Los clientes externos pueden usar un controlador multimedia para enviar comandos de reproducción a tu app de contenido multimedia. 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 objeto MediaSession y un objeto 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 onConnect() método. Puedes usar el ControllerInfo para decidir si aceptas o rechazas la solicitud. Consulta un ejemplo de aceptación de una solicitud de conexión en la sección Declara comandos personalizados.

Después de conectarse, 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 de playlist definidos en la interfaz Player.

Otros métodos de devolución de llamada te permiten controlar, por ejemplo, solicitudes de comandos personalizados y modificar la playlist. Estas devoluciones de llamada también 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 MediaItem instancias 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 deseas personalizar las instancias de MediaItem agregadas al reproductor, puedes anular onAddMediaItems(). Este paso es necesario cuando deseas 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 esquema personalizado y que el reproductor no necesariamente puede reproducir directamente.
  • MediaItem.RequestMetadata.searchQuery: Es una consulta de búsqueda textual, por ejemplo, de Asistente de Google.
  • MediaItem.MediaMetadata: Son 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 dar instrucciones al reproductor para que comience en el índice del elemento solicitado originalmente. Una implementación de muestra de onSetMediaItems() con esta función se puede encontrar en la app de demostración de la sesión.

Administra las preferencias de los botones de medios

Cada controlador, por ejemplo, la IU del sistema, Android Auto o Wear OS, puede tomar sus propias decisiones sobre qué botones mostrar al usuario. Para indicar qué controles de reproducción deseas exponer al usuario, puedes especificar preferencias de botones de medios en MediaSession. Estas preferencias consisten en una lista ordenada de instancias de CommandButton, cada una de las cuales define una preferencia para un botón en la interfaz de usuario.

Define botones de comando

Las instancias de CommandButton se usan para definir las preferencias de los botones de medios. Cada botón define tres aspectos del elemento de la IU deseado:

  1. El ícono, que define la apariencia visual. El ícono debe establecerse en una de las constantes predefinidas cuando se crea un CommandButton.Builder. Ten en cuenta que no es un mapa de bits real ni un recurso de imagen. Una constante genérica ayuda a los controladores a elegir un recurso adecuado para una apariencia coherente dentro de su propia IU. Si ninguna de las constantes de íconos predefinidas se ajusta a tu caso de uso, puedes usar setCustomIconResId en su lugar.
  2. El comando, que define la acción que se activa cuando el usuario interactúa con el botón. Puedes usar setPlayerCommand para un Player.Command o setSessionCommand para un SessionCommand predefinido o personalizado.
  3. La ranura, que define dónde se debe colocar el botón en la IU del controlador. Este campo es opcional y se configura automáticamente en función del ícono y el comando. Por ejemplo, permite especificar que un botón se debe mostrar en el área de navegación "adelante" de la IU en lugar del área "ampliada" predeterminada.

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

Cuando se resuelven las preferencias de los botones de medios, se aplica el siguiente algoritmo:

  1. Para cada CommandButton en las preferencias de los botones de medios, coloca el botón en la primera ranura disponible y permitida.
  2. Si alguna de las ranuras centrales, hacia adelante y hacia atrás no se llena con un botón, agrega botones predeterminados para esta ranura.

Puedes usar CommandButton.DisplayConstraints para generar una vista previa de cómo se resolverán las preferencias de los botones de medios según las restricciones de visualización de la IU.

Configura las preferencias de los botones de medios

La forma más sencilla de configurar las preferencias de los botones de medios es definir la lista cuando se compila MediaSession. Como alternativa, puedes anular MediaSession.Callback.onConnect para personalizar las preferencias de los botones de medios para cada controlador conectado.

Kotlin

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Java

MediaSession mediaSession =
    new MediaSession.Builder(context, player)
        .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
        .build();

Actualiza las preferencias de los botones de medios después de una interacción del usuario

Después de controlar una interacción con tu reproductor, es posible que desees actualizar los botones que se muestran en la IU del controlador. Un ejemplo típico es un botón de alternancia que cambia su ícono y acción después de activar la acción asociada con este botón. Para actualizar las preferencias de los botones de medios, puedes usar MediaSession.setMediaButtonPreferences para actualizar las preferencias de todos los controladores o de un controlador específico:

Kotlin

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton));

Agrega comandos personalizados y personaliza el comportamiento predeterminado

Los comandos del reproductor disponibles se pueden extender con comandos personalizados, y también es posible interceptar los comandos del reproductor y los botones de medios entrantes para cambiar el comportamiento predeterminado.

Declara y controla comandos personalizados

Las aplicaciones de contenido multimedia pueden definir comandos personalizados que, por ejemplo, se pueden usar en las preferencias de los botones de medios. 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.

Para definir comandos personalizados, debes anular MediaSession.Callback.onConnect() para establecer los comandos personalizados disponibles para cada controlador conectado.

Kotlin

private class CustomMediaSessionCallback : MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  override fun onConnectAsync(
    session: MediaSession,
    controller: ControllerInfo,
  ): ListenableFuture<ConnectionResult> {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return Futures.immediateFuture(
      AcceptedResultBuilder(session).setAvailableSessionCommands(sessionCommands).build()
    )
  }
}

Java

private static class CustomMediaSessionCallback implements MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  @Override
  public ListenableFuture<ConnectionResult> onConnectAsync(
      MediaSession session, ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return Futures.immediateFuture(
        new AcceptedResultBuilder(session).setAvailableSessionCommands(sessionCommands).build());
  }
}

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

Kotlin

private class CustomCallback : MediaSession.Callback {
  // ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: 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))
    }
    // ...
    return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
  }
}

Java

private static class CustomCallback 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));
    }
    // ...
    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.

Personaliza los comandos predeterminados del reproductor

Todos los comandos predeterminados y el manejo de estados se delegan al Player que está en MediaSession. Para personalizar el comportamiento de un comando definido en la Player interfaz, como play() o seekToNext(), incluye tu Player en un ForwardingSimpleBasePlayer antes de pasarlo a MediaSession:

Kotlin

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

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

Java

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

private class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) {
  private lateinit var session: MediaSession

  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

private static final class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  private MediaSession session;

  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);
  }
}

Personaliza el manejo de los botones de medios

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 handles los eventos de los botones de medios cuando llegan a la sesión y llama al método Player adecuado en el reproductor de la sesión.

Se recomienda controlar todos los eventos de los botones de medios entrantes en el método Player correspondiente. Para casos de uso más avanzados, los eventos de los botones de medios se pueden interceptar en MediaSession.Callback.onMediaButtonEvent(Intent).

Manejo de errores y generación de informes

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

Errores de reproducción no recuperables

El reproductor informa un error de reproducción no recuperable a la sesión y, luego, se informa 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.

Cómo configurar un error personalizado del reproductor

Además de los errores no recuperables que informa el reproductor, una aplicación puede establecer un PlaybackException personalizado en el nivel de MediaSession con MediaSession.setPlaybackException(PlaybackException). Esto permite que la aplicación indique un estado de error a los controladores conectados. La excepción se puede configurar para todos los controladores conectados o para un ControllerInfo específico.

Cuando una app establece un PlaybackException con esta API, sucede lo siguiente:

  • Se notificarán las instancias de MediaController conectadas. Se invocarán las devoluciones de llamada Listener.onPlayerError(PlaybackException) y Listener.onPlayerErrorChanged(@Nullable PlaybackException) en el controlador con la excepción proporcionada.

  • El método MediaController.getPlayerError() mostrará el PlaybackException establecido por la aplicación.

  • El estado de reproducción de los controladores afectados cambiará a Player.STATE_IDLE.

  • Se quitarán los comandos disponibles y solo quedarán los comandos de lectura, como COMMAND_GET_TIMELINE, en caso de que ya se hayan otorgado. El estado de Timeline, por ejemplo, se congela al estado cuando se aplicó la excepción al controlador. Los comandos que intentan cambiar el estado del reproductor, como COMMAND_PLAY, se quitan hasta que la app quite la excepción de reproducción para el controlador determinado.

Para borrar un PlaybackException personalizado establecido anteriormente y restablecer la generación de informes de estado normal del reproductor, una app puede llamar a MediaSession.setPlaybackException(/* playbackException= */ null) o MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null).

Personalización de errores no recuperables

Para proporcionar información localizada y significativa al usuario, puedes personalizar el código de error, el mensaje de error y los extras de error de un error de reproducción no recuperable que proviene del reproductor real. Esto se puede lograr usando un ForwardingPlayer cuando se compila la sesión:

Kotlin

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

Java

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

El reproductor de reenvío puede usar ForwardingSimpleBasePlayer para interceptar el error y personalizar el código, el mensaje o los extras de error. Del mismo modo, también puedes generar errores nuevos que no existen en el reproductor original:

Kotlin

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

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon().setPlayerError(customizePlaybackException(state.playerError!!)).build()
    }
    return state
  }

  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)
      }
      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)
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon().setPlayerError(customizePlaybackException(state.playerError)).build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    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;
      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);
  }
}

Errores recuperables

Una app puede enviar errores recuperables que no provienen de 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),
  )

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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));

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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);
}

Cuando se envía un error recuperable al controlador de notificaciones de contenido multimedia, el código y el mensaje de error se replican en la sesión multimedia de la plataforma, mientras que PlaybackState.state no cambia a STATE_ERROR.

Recibe errores recuperables

Un MediaController recibe un error recuperable implementando 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.
              }
            });