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 de 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 o 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 reproduz a 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 bloqueio da tela 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 para o 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 origens 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 aceitação de 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 para o 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.
              }
            });