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 ter origem em botões físicos, como o botão de reprodução em um fone de ouvido ou controle remoto da TV. Eles também podem vir de apps clientes que têm um controlador de mídia, como instruir "pausar" ao Google Assistente. 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. Geralmente, há botões ou interações de toque que um usuário pode realizar nos fones de ouvido para reproduzir ou pausar a mídia ou ir para a faixa seguinte ou anterior.
  • Falando com o Google Assistente. Um padrão comum é dizer "OK Google, pausar" para pausar qualquer mídia que esteja sendo reproduzida no dispositivo.
  • Pelo relógio Wear OS. Isso permite um acesso mais fácil aos controles de reprodução mais comuns durante a reprodução no smartphone.
  • Pelos controles de mídia. Esse carrossel mostra controles para cada sessão de mídia em execução.
  • Na TV. Permite ações com botões de reprodução físicos, controle de reprodução da plataforma e gerenciamento de energia (por exemplo, se a TV, a barra de som ou o receptor de áudio/vídeo forem desligados ou a entrada for trocada, a reprodução deverá ser interrompida no app).
  • Pelos controles de mídia do Android Auto. Isso permite um controle de reprodução seguro durante a direção.
  • 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ê estiver transmitindo conteúdo de vídeo longo, como filmes ou TV ao vivo.
  • Você estiver transmitindo conteúdo de áudio longo, como podcasts ou playlists de música.
  • Você estiver criando um app para TV.

No entanto, nem todos os casos de uso se encaixam bem no MediaSession. Talvez você queira usar apenas o Player nos seguintes casos:

  • Você está mostrando conteúdo curto, em que nenhum controle externo ou reprodução em segundo plano é necessário.
  • Não há um único vídeo ativo, como o usuário rolando uma lista e vários vídeos sendo exibidos na tela ao mesmo tempo.
  • Você está reproduzindo um vídeo de introdução ou explicação única, que você espera que o usuário assista ativamente sem precisar de controles de reprodução externos.
  • Seu conteúdo é sensível à privacidade e você não quer que processos externos acessem os metadados de 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ê concorda 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, provavelmente você vai querer usar o Player em vez disso.

Criar uma sessão de mídia

Uma sessão de mídia convive com o player que a gerencia. É possível construir uma sessão de mídia com um Context e um Player. Crie e inicialize uma sessão de mídia quando ela for necessária, como o onStart() ou onResume() método de ciclo de vida da Activity ou Fragment, ou onCreate() método do Service que possui 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 ao 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();

Tratamento 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 player para a sessão.

Isso é diferente da sessão de mídia da plataforma, em que era necessário criar e manter um PlaybackState independente do player, por exemplo, para indicar erros.

ID de sessão exclusivo

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 única 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 precisará 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ê encontrar um IllegalStateException que trava 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 criada anteriormente 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 gerando uma exceção.

Conceder controle a outros clientes

A sessão de mídia é a chave para controlar a reprodução. Ela permite rotear comandos de fontes externas para o player que faz o trabalho de reproduzir sua 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 instruir "pausar" ao Google Assistente. Da mesma forma, talvez você queira conceder acesso ao sistema Android para facilitar as notificações e os controles da tela de bloqueio ou a um relógio Wear OS para controlar a reprodução pelo mostrador. Clientes externos podem usar um controlador 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 um MediaController.
Figura 1: o controlador de mídia 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 onConnect() método é chamado. Você pode usar o ControllerInfo para decidir se aceita ou rejeita a solicitação. Consulte um exemplo de como aceitar uma solicitação de conexão na seção Declarar comandos personalizados.

Depois de se conectar, um controlador pode enviar comandos de reprodução para a sessão. A sessão delega esses comandos ao player. 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 personalizados e modificar a playlist. Esses callbacks também incluem um objeto ControllerInfo para que você possa modificar como você responde a cada solicitação por controlador.

Modificar a playlist

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

Ao adicionar novos itens à playlist, o player normalmente exige MediaItem instâncias com um URI definido para torná-los reproduzíveis. Por padrão, os itens recém-adicionados são encaminhados automaticamente para métodos de player como player.addMediaItem se tiverem um URI definido.

Se você quiser personalizar as instâncias MediaItem adicionadas ao player, você pode 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 reproduzível diretamente pelo player.
  • MediaItem.RequestMetadata.searchQuery: uma consulta de pesquisa textual, por exemplo do Google Assistente.
  • MediaItem.MediaMetadata: metadados estruturados, como "título" ou "artista".

Para mais opções de personalização de 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 amostra de onSetMediaItems() com esse recurso pode ser encontrada no app de demonstração da sessão.

Gerenciar preferências de botões de mídia

Cada controlador, por exemplo, a interface do sistema, o Android Auto ou o Wear OS, pode tomar as próprias decisões sobre quais botões mostrar ao usuário. Para indicar quais controles de reprodução você quer expor ao usuário, especifique preferências de botões de mídia no MediaSession. Essas preferências consistem em uma lista ordenada de CommandButton instâncias, cada uma definindo uma preferência para um botão na interface do usuário.

Definir botões de comando

CommandButton instâncias são usadas para definir preferências de botões de mídia. Cada botão define três aspectos do elemento de interface desejado:

  1. O ícone, que define a aparência visual. O ícone precisa ser definido como uma das constantes predefinidas ao criar um CommandButton.Builder. Observe que isso não é um bitmap ou recurso de imagem real. Uma constante genérica ajuda os controladores a escolher um recurso adequado para uma aparência consistente na própria interface. Se nenhuma das constantes de ícone predefinidas se ajustar ao seu caso de uso, use setCustomIconResId em vez disso.
  2. O comando, que define a ação acionada quando o usuário interage com o botão. Você pode usar setPlayerCommand para um Player.Command ou setSessionCommand para um SessionCommand predefinido ou personalizado.
  3. O slot, que define onde o botão deve ser colocado na interface do controlador. Esse campo é opcional e definido automaticamente com base no ícone e comando. Por exemplo, ele permite especificar que um botão seja mostrado na área de navegação "avançar" da interface em vez da área "flutuante" padrão.

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

Quando as preferências de botões de mídia são resolvidas, o seguinte algoritmo é aplicado:

  1. Para cada CommandButton nas preferências de botões de mídia, coloque o botão no primeiro slot disponível e permitido.
  2. Se algum dos slots central, avançar e voltar não estiver preenchido com um botão, adicione botões padrão para esse slot.

Você pode usar CommandButton.DisplayConstraints para gerar uma visualização de como as preferências de botões de mídia serão resolvidas, dependendo das restrições de exibição da interface.

Definir preferências de botões de mídia

A maneira mais fácil de definir as preferências de botões de mídia é definir a lista ao criar a MediaSession. Como alternativa, você pode substituir MediaSession.Callback.onConnect para personalizar as preferências de botões de mídia 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();

Atualizar as preferências de botões de mídia após uma interação do usuário

Depois de processar uma interação com o player, talvez você queira atualizar os botões mostrados na interface do controlador. Um exemplo típico é um botão de alternância que muda o ícone e a ação depois de acionar a ação associada a ele. Para atualizar as preferências de botões de mídia, use MediaSession.setMediaButtonPreferences para atualizar as preferências de todos os controladores ou de um 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));

Adicionar comandos personalizados e personalizar o comportamento padrão

Os comandos de player disponíveis podem ser estendidos por comandos personalizados, e também é possível interceptar comandos de player e botões de mídia recebidos para mudar o comportamento padrão.

Declarar e processar comandos personalizados

Os aplicativos de mídia podem definir comandos personalizados que, por exemplo, podem ser usados em preferências de botões de mídia. Por exemplo, talvez você queira implementar botões que permitam ao usuário salvar um item de mídia em uma lista de itens favoritos. O MediaController envia comandos personalizados, e o MediaSession.Callback os recebe.

Para definir comandos personalizados, substitua MediaSession.Callback.onConnect() para definir os comandos personalizados disponíveis para cada controlador conectado.

Kotlin

private class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): 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 comandos personalizados de um MediaController, substitua o onCustomCommand() método no Callback.

Kotlin

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

Você pode acompanhar qual controlador de mídia está fazendo uma solicitação usando a packageName propriedade do MediaSession.ControllerInfo objeto que é transmitido aos Callback métodos. Isso permite personalizar o comportamento do app em resposta a um determinado comando, caso ele tenha origem no sistema, no seu próprio app ou em outros apps clientes.

Personalizar comandos de player padrão

Todos os comandos padrão e o tratamento de estado são delegados ao Player que está em MediaSession. Para personalizar o comportamento de um comando definido na Player interface, como play() ou seekToNext(), encapsule 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 de player

Quando uma chamada para um método Player é originada por um MediaController, você pode 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);
  }
}

Personalizar o tratamento de 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 eventos de botões de mídia para você quando eles chegam à sessão e chama o método Player apropriado no player da sessão.

Recomendamos processar todos os eventos de botões de mídia recebidos no método Player correspondente. Para casos de uso mais avançados, os eventos de botões de mídia podem ser interceptados em MediaSession.Callback.onMediaButtonEvent(Intent).

Tratamento e relatório de erros

Há dois tipos de erros que uma sessão emite e informa aos controladores. Erros fatais informam uma falha técnica de reprodução do player da sessão que interrompe a reprodução. Os 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 aos controladores pelo aplicativo manualmente.

Erros fatais de reprodução

Um erro fatal de reprodução é informado à sessão pelo player e, em seguida, informado aos controladores para chamar através de 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 a PlaybackException que causou a transição. Um controlador pode inspecionar o PlayerException.errorCode para receber informações sobre o motivo do erro.

Definir um erro de player personalizado

Além dos erros fatais informados pelo player, um aplicativo pode definir um PlaybackException personalizado no nível da MediaSession usando MediaSession.setPlaybackException(PlaybackException). Isso permite que o aplicativo sinalize um estado de erro para controladores conectados. A exceção pode ser definida para todos os controladores conectados ou para um ControllerInfo específico.

Quando um app define um PlaybackException usando essa API:

  • As instâncias MediaController conectadas serão notificadas. Os Listener.onPlayerError(PlaybackException) e Listener.onPlayerErrorChanged(@Nullable PlaybackException) callbacks no controlador serão invocados com a exceção fornecida.

  • O método MediaController.getPlayerError() vai retornar o PlaybackException definido pelo aplicativo.

  • O estado de reprodução dos controladores afetados será alterado para Player.STATE_IDLE.

  • Os comandos disponíveis serão removidos, e apenas os comandos de leitura, como COMMAND_GET_TIMELINE serão deixados caso já tenham sido concedidos. O estado de o Timeline, por exemplo, é congelado para o estado quando a exceção foi aplicada ao controlador. Os comandos que tentam mudar o estado do player, como COMMAND_PLAY, são removidos até que a exceção de reprodução do controlador seja removida pelo app.

Para limpar um PlaybackException personalizado definido anteriormente e restaurar o relatório de estado normal do player, um app pode chamar MediaSession.setPlaybackException(/* playbackException= */ null) ou MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null).

Personalização de erros fatais

Para fornecer informações localizadas e significativas ao usuário, é possível personalizar o código de erro, a mensagem de erro e os extras de erro de um erro fatal de reprodução do player real. Isso pode ser feito 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 pode usar ForwardingSimpleBasePlayer para interceptar o erro e personalizar o código, a mensagem ou os extras. Da mesma forma, também é possível gerar novos erros que não existem no player original:

Kotlin

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
  }

  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

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

Erros não fatais

Erros não fatais que não se originam de uma exceção técnica podem ser enviados por um app para todos ou para um controlador 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);
}

Quando um erro não fatal é enviado ao controlador de notificação de mídia, o código e a mensagem de erro são replicados para a sessão de mídia da plataforma, enquanto PlaybackState.state não é alterado para STATE_ERROR.

Receber erros não fatais

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