Controlar e anunciar a reprodução usando uma MediaSession

As sessões de mídia oferecem uma maneira universal de interagir com um player de áudio ou vídeo. No Media3, o player padrão é a classe ExoPlayer, que implementa a interface Player. A conexão da sessão de mídia ao player permite que um app anuncie a reprodução de mídia externamente e receba comandos de reprodução de fontes externas.

Os comandos podem ser originados de botões físicos, como o botão de reprodução em um fone de ouvido ou controle remoto da TV. Elas também podem vir de apps clientes que têm um controlador de mídia, como instruir o Google Assistente a "pausar". A sessão de mídia delega esses comandos ao player do app de mídia.

Quando escolher uma sessão de mídia

Ao implementar MediaSession, você permite que os usuários controlem a reprodução:

  • Pelos fones de ouvido. Muitas vezes, há botões ou interações por toque que o usuário pode realizar nos fones de ouvido para reproduzir ou pausar mídia ou ir para a próxima ou a faixa anterior.
  • Falando com o Google Assistente. Um padrão comum é dizer "Ok Google, pause" para pausar qualquer mídia que esteja sendo reproduzida no dispositivo.
  • Pelo relógio Wear OS. Isso facilita o acesso aos controles de reprodução mais comuns durante a reprodução no smartphone.
  • Através dos controles de mídia. Esse carrossel mostra os controles de cada sessão de mídia em execução.
  • Na TV. Permite ações com botões físicos de reprodução, controle de reprodução da plataforma e gerenciamento de energia. Por exemplo, se a TV, a soundbar ou o receptor de áudio/vídeo desligar ou se a entrada for trocada, a reprodução será interrompida no app.
  • E qualquer outro processo externo que precise influenciar a reprodução.

Isso é ótimo para muitos casos de uso. Em particular, considere usar MediaSession quando:

  • Você está transmitindo conteúdo de vídeo mais longo, como filmes ou TV ao vivo.
  • Você está transmitindo conteúdo de áudio mais longo, como podcasts ou playlists de música.
  • Você está criando um app para TV.

No entanto, nem todos os casos de uso se encaixam bem com o MediaSession. Talvez seja melhor usar apenas o Player nos seguintes casos:

  • Você está mostrando conteúdo de formato curto, em que o engajamento e a interação do usuário são essenciais.
  • Não há um único vídeo ativo, como quando o usuário rola uma lista e vários vídeos são exibidos na tela ao mesmo tempo.
  • Você está reproduzindo um vídeo de introdução ou explicação único, que você espera que o usuário assista ativamente.
  • Seu conteúdo é sensível à privacidade e você não quer que processos externos acessem os metadados da mídia (por exemplo, o modo de navegação anônima em um navegador).

Se o caso de uso não se encaixar em nenhum dos listados acima, considere se você aceita que o app continue a reprodução quando o usuário não estiver interagindo ativamente com o conteúdo. Se a resposta for sim, provavelmente você vai querer escolher MediaSession. Se a resposta for "não", use Player.

Criar uma sessão de mídia

Uma sessão de mídia convive com o player que a gerencia. É possível criar uma sessão de mídia com um objeto Context e um Player. Crie e inicialize uma sessão de mídia quando necessário, como o método de ciclo de vida onStart() ou onResume() do Activity ou Fragment, ou o método onCreate() do Service que detém a sessão de mídia e o player associado.

Para criar uma sessão de mídia, inicialize um Player e forneça-o a MediaSession.Builder desta forma:

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

Processamento automático de estado

A biblioteca Media3 atualiza automaticamente a sessão de mídia usando o estado do player. Portanto, não é necessário processar manualmente o mapeamento do jogador para a sessão.

Isso é uma mudança em relação à abordagem legada, em que você precisava criar e manter um PlaybackStateCompat independente do próprio player, por exemplo, para indicar erros.

ID exclusivo da sessão

Por padrão, MediaSession.Builder cria uma sessão com uma string vazia como o ID da sessão. Isso é suficiente se um app pretende criar apenas uma instância de sessão, que é o caso mais comum.

Se um app quiser gerenciar várias instâncias de sessão ao mesmo tempo, ele precisa garantir que o ID de cada sessão seja exclusivo. O ID da sessão pode ser definido ao criar a sessão com MediaSession.Builder.setId(String id).

Se você notar que um IllegalStateException está travando seu app com a mensagem de erro IllegalStateException: Session ID must be unique. ID=, é provável que uma sessão tenha sido criada inesperadamente antes que uma instância anteriormente criada com o mesmo ID tenha sido liberada. Para evitar que as sessões sejam vazadas por um erro de programação, esses casos são detectados e notificados com uma exceção.

Conceder controle a outros clientes

A sessão de mídia é a chave para controlar a reprodução. Ele permite encaminhar comandos de fontes externas para o player que faz a reprodução da mídia. Essas fontes podem ser botões físicos, como o botão de reprodução em um fone de ouvido ou controle remoto da TV, ou comandos indiretos, como o comando "pausar" para o Google Assistente. Da mesma forma, você pode conceder acesso ao sistema Android para facilitar os controles de notificação e tela de bloqueio ou a um relógio Wear OS para controlar a reprodução pelo mostrador do relógio. Clientes externos podem usar um controle de mídia para emitir comandos de reprodução ao app de mídia. Eles são recebidos pela sessão de mídia, que delega comandos ao player de mídia.

Um diagrama que demonstra a interação entre uma MediaSession e uma MediaController.
Figura 1: o media controller facilita a transmissão de comandos de fontes externas para a sessão de mídia.

Quando um controlador está prestes a se conectar à sessão de mídia, o método onConnect() é chamado. Use o ControllerInfo fornecido para decidir se vai aceitar ou rejeitar a solicitação. Confira um exemplo de como aceitar uma solicitação de conexão na seção Declarar comandos disponíveis.

Depois de se conectar, um controlador pode enviar comandos de reprodução para a sessão. A sessão então delega esses comandos ao jogador. Os comandos de reprodução e playlist definidos na interface Player são processados automaticamente pela sessão.

Outros métodos de callback permitem processar, por exemplo, solicitações de comandos de reprodução personalizados e modificar a playlist. Esses callbacks também incluem um objeto ControllerInfo para que você possa modificar como responde a cada solicitação em cada controlador.

Modificar a playlist

Uma sessão de mídia pode modificar diretamente a playlist do player, conforme explicado no guia do ExoPlayer para playlists. Os controles também podem modificar a playlist se COMMAND_SET_MEDIA_ITEM ou COMMAND_CHANGE_MEDIA_ITEMS estiver disponível para o controle.

Ao adicionar novos itens à playlist, o player normalmente exige instâncias MediaItem com um URI definido para que eles possam ser reproduzidos. Por padrão, os itens recém-adicionados são encaminhados automaticamente para métodos do player, como player.addMediaItem, se tiverem um URI definido.

Se você quiser personalizar as instâncias de MediaItem adicionadas ao player, é possível substituir onAddMediaItems(). Essa etapa é necessária quando você quer oferecer suporte a controladores que solicitam mídia sem um URI definido. Em vez disso, o MediaItem normalmente tem um ou mais dos seguintes campos definidos para descrever a mídia solicitada:

  • MediaItem.id: um ID genérico que identifica a mídia.
  • MediaItem.RequestMetadata.mediaUri: um URI de solicitação que pode usar um esquema personalizado e não é necessariamente reproduzido diretamente pelo player.
  • MediaItem.RequestMetadata.searchQuery: uma consulta de pesquisa textual, por exemplo, do Google Assistente.
  • MediaItem.MediaMetadata: metadados estruturados, como "title" ou "artist".

Para mais opções de personalização para playlists completamente novas, você pode substituir onSetMediaItems(), que permite definir o item inicial e a posição na playlist. Por exemplo, é possível expandir um único item solicitado para uma playlist inteira e instruir o player a começar no índice do item solicitado originalmente. Uma implementação de exemplo de onSetMediaItems() com esse recurso pode ser encontrada no app de demonstração de sessão.

Gerenciar layout e comandos personalizados

As seções a seguir descrevem como anunciar um layout personalizado de botões de comando personalizados para apps cliente e autorizar controladores a enviar os comandos personalizados.

Definir o layout personalizado da sessão

Para indicar aos apps clientes quais controles de reprodução você quer mostrar ao usuário, defina o layout personalizado da sessão ao criar o MediaSession no método onCreate() do serviço.

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

Declarar comandos personalizados e o player disponível

Os aplicativos de mídia podem definir comandos personalizados que, por exemplo, podem ser usados em um layout personalizado. Por exemplo, você pode implementar botões que permitem que o usuário salve um item de mídia em uma lista de itens favoritos. O MediaController envia comandos personalizados, e o MediaSession.Callback os recebe.

É possível definir quais comandos de sessão personalizados estão disponíveis para um MediaController quando ele se conecta à sua sessão de mídia. Para isso, modifique MediaSession.Callback.onConnect(). Configure e retorne o conjunto de comandos disponíveis ao aceitar uma solicitação de conexão de um MediaController no método de callback 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 receber solicitações de comando personalizadas de um MediaController, substitua o método onCustomCommand() no 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)
      );
    }
    ...
  }
}

É possível acompanhar qual controlador de mídia está fazendo uma solicitação usando a propriedade packageName do objeto MediaSession.ControllerInfo que é transmitida para os métodos Callback. Isso permite personalizar o comportamento do app em resposta a um determinado comando, se ele for originado pelo sistema, pelo seu próprio app ou por outros apps clientes.

Atualizar o layout personalizado após uma interação do usuário

Depois de processar um comando personalizado ou qualquer outra interação com o player, é recomendável atualizar o layout exibido na interface do controle. Um exemplo típico é um botão de alternância que muda o ícone depois de acionar a ação associada a ele. Para atualizar o layout, use 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));

Personalizar o comportamento do comando de reprodução

Para personalizar o comportamento de um comando definido na interface Player, como play() ou seekToNext(), envolva o Player em um ForwardingSimpleBasePlayer antes de transmiti-lo para 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 mais informações sobre ForwardingSimpleBasePlayer, consulte o guia do ExoPlayer sobre Personalização.

Identificar o controlador solicitante de um comando do jogador

Quando uma chamada para um método Player é originada por um MediaController, é possível identificar a origem com MediaSession.controllerForCurrentRequest e adquirir o ControllerInfo para a solicitação atual:

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

Responder a botões de mídia

Os botões de mídia são botões de hardware encontrados em dispositivos Android e outros dispositivos periféricos, como o botão de reprodução/pausa em um fone de ouvido Bluetooth. O Media3 processa os eventos do botão de mídia para você quando eles chegam à sessão e chama o método Player apropriado no player da sessão.

Um app pode substituir o comportamento padrão substituindo MediaSession.Callback.onMediaButtonEvent(Intent). Nesse caso, o app pode/precisa processar todas as especificidades da API por conta própria.

Tratamento e relatórios de erros

Há dois tipos de erros que uma sessão emite e informa aos controladores. Erros fatais relatam uma falha técnica de reprodução do player de sessão que interrompe a reprodução. Erros fatais são informados ao controlador automaticamente quando ocorrem. Erros não fatais são erros não técnicos ou de política que não interrompem a reprodução e são enviados manualmente pelo aplicativo aos controladores.

Erros fatais de reprodução

Um erro fatal de reprodução é informado ao player pela sessão e, em seguida, informado aos controladores para chamar Player.Listener.onPlayerError(PlaybackException) e Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

Nesse caso, o estado de reprodução é transferido para STATE_IDLE, e MediaController.getPlaybackError() retorna o PlaybackException que causou a transição. Um controlador pode inspecionar o PlayerException.errorCode para receber informações sobre o motivo do erro.

Para interoperabilidade, um erro fatal é replicado para o PlaybackStateCompat da sessão da plataforma, fazendo a transição do estado para STATE_ERROR e definindo o código e a mensagem de erro de acordo com o PlaybackException.

Personalização de um erro fatal

Para fornecer informações localizadas e significativas ao usuário, o código de erro, a mensagem de erro e os extras de erro de um erro fatal de reprodução podem ser personalizados usando um ForwardingPlayer ao criar a sessão:

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

O player de encaminhamento registra um Player.Listener para o player real e intercepta callbacks que informam um erro. Um PlaybackException personalizado é delegado aos listeners que estão registrados no player de encaminhamento. Para que isso funcione, o player de encaminhamento substitui Player.addListener e Player.removeListener para ter acesso aos listeners com os quais enviar código de erro, mensagem ou 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.
  }
}

Erros não fatais

Erros não fatais que não têm origem em uma exceção técnica podem ser enviados por um app a todos ou a um 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);
}

Um erro não fatal enviado ao controlador de notificação de mídia é replicado para o PlaybackStateCompat da sessão da plataforma. Assim, apenas o código de erro e a mensagem de erro são definidos para PlaybackStateCompat, enquanto PlaybackStateCompat.state não é alterado para STATE_ERROR.

Receber erros não fatais

Um MediaController recebe um erro não fatal ao 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.
              }
            });