Controlar e anunciar a reprodução usando uma MediaSession

As sessões de mídia oferecem uma maneira universal de interagir com um áudio ou vídeo de futebol. Na Media3, o player padrão é a classe ExoPlayer, que implementa a interface Player. Conectar a sessão de mídia ao player permite que um app para anunciar a reprodução de mídia externamente e receber comandos de reprodução de fontes externas.

Os comandos podem se originar de 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 com um controlador de mídia, como a instrução de "pausar" para o Google Assistente. A mídia sessão delega esses comandos para o 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 de toque que o usuário pode tocar nos fones de ouvido para reproduzir ou pausar mídia ou ir para a próxima ou à faixa anterior.
  • Falando com o Google Assistente. Um padrão comum é dizer "Ok, Google, pause" para pausar qualquer mídia em reprodução no dispositivo.
  • No relógio Wear OS. Isso facilita o acesso aos recursos controles de reprodução comuns durante a reprodução no telefone.
  • Com os Controles de mídia. Este carrossel mostra os controles de cada sessão de mídia em execução.
  • Na TV. Permite ações com botões de reprodução físicos ou reprodução na plataforma controle de energia e gerenciamento de energia (por exemplo, se a TV, a soundbar ou o receptor A/V for desligada ou a entrada for alterada, a reprodução deverá ser interrompida no aplicativo).
  • E quaisquer outros processos externos que precisem influenciar a reprodução.

Isso é ótimo para muitos casos de uso. Especificamente, considere usando 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 de formato longo, como podcasts ou músicas listas de reprodução.
  • Você estiver criando um app de TV.

No entanto, nem todos os casos de uso são adequados para MediaSession. Talvez você queira use apenas Player nos seguintes casos:

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

Caso seu caso de uso não se enquadre em nenhum dos listados acima, considere se você está Tudo bem se o app continuar a reprodução quando o usuário não estiver interagindo ativamente com o conteúdo. Se a resposta for sim, você provavelmente MediaSession: Se a resposta for não, é recomendável usar Player. como alternativa.

Criar uma sessão de mídia

Uma sessão de mídia convive com o player que a gerencia. Você pode criar um sessão de mídia com um Context e um objeto Player. Você deve criar e inicializar uma sessão de mídia quando necessário, como a onStart() ou Método de ciclo de vida onResume() do Activity, Fragment ou onCreate() do Service que tem a sessão de mídia e o player associado.

Para criar uma sessão de mídia, inicialize uma Player e a forneça 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 método do player de vídeo. Dessa forma, não é necessário manipular manualmente o mapeamento do jogador para sessão.

Isso é uma quebra da abordagem legada, em que você precisava criar e manter um PlaybackStateCompat independentemente 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 aplicativo quiser gerenciar várias instâncias de sessão ao mesmo tempo, precisa garantir que o ID de cada sessão seja único. O ID da sessão pode ser definido ao criar a sessão com MediaSession.Builder.setId(String id).

Se você vir uma IllegalStateException fazendo com que o app falhe com o erro enviar uma mensagem para IllegalStateException: Session ID must be unique. ID=, ele será a probabilidade de uma sessão ter sido criada inesperadamente com o mesmo ID for lançada. Para evitar o vazamento de sessões por um de programação, esses casos são detectados e notificados com o lançamento de uma exceção.

Conceder controle a outros clientes

A sessão de mídia é essencial para controlar a reprodução. Ele permite rotear de fontes externas para o player que reproduz 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 pedir para pausar para o Google Assistente. Da mesma forma, você pode querer conceder acesso ao Android para facilitar os controles de notificações e de bloqueio de tela, ou para um sistema relógio para que você possa controlar a reprodução a partir do mostrador do relógio. Os clientes externos podem usar um controlador de mídia para emitir comandos de reprodução para seu app de música. São recebidos pela sessão de mídia, que delega comandos para o player de mídia.

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

Quando um controlador está prestes a se conectar à sua sessão de mídia, o onConnect() é chamado. Você pode usar o ControllerInfo fornecido para decidir se você quer aceitar ou rejeitar da solicitação. Veja um exemplo de como aceitar uma solicitação de conexão na seção Declarar comandos disponíveis.

Após a conexão, um controlador pode enviar comandos de reprodução para a sessão. A sessão e delega esses comandos para o player. Reprodução e playlist Os comandos definidos na interface Player são processados automaticamente pelo sessão.

Outros métodos de callback permitem que você lide, por exemplo, com solicitações de comandos de reprodução personalizados e modificando a playlist). Da mesma forma, esses callbacks 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 lista de reprodução do player, conforme explicado em as Guia do ExoPlayer para playlists (link em inglês). Os controladores também podem modificar a playlist se COMMAND_SET_MEDIA_ITEM ou COMMAND_CHANGE_MEDIA_ITEMS está disponível para o controlador.

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

Se você quiser personalizar as instâncias de MediaItem adicionadas ao player, poderá sobrepor-se onAddMediaItems(). Esta 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 endereço que não pode ser reproduzido diretamente pelo jogador.
  • MediaItem.RequestMetadata.searchQuery: uma consulta de pesquisa textual, por exemplo. usando o Google Assistente.
  • MediaItem.MediaMetadata: metadados estruturados, como "título" ou "artista".

Para mais opções de personalização para playlists completamente novas, modificar adicionalmente 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 para começar no índice do item solicitado originalmente. Um exemplo de implementação de onSetMediaItems() com esse recurso pode ser encontrada no aplicativo de demonstração da sessão.

Gerenciar layout e comandos personalizados

As seções a seguir descrevem como anunciar um layout personalizado de para aplicativos clientes e autorize os controladores a enviar os comandos comandos

Definir o layout personalizado da sessão

Para indicar aos apps clientes quais controles de mídia você quer exibir ao usuário, defina o layout personalizado da sessão ao criar a MediaSession no método onCreate() da 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 de player disponíveis

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

Você pode definir quais comandos de sessão personalizada estão disponíveis para um MediaController quando ele se conecta à sessão de mídia. Você consegue isso substituindo MediaSession.Callback.onConnect(). Configurar e retornar 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 personalizado de um MediaController, modifique o Método onCustomCommand() na 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)
      );
    }
    ...
  }
}

Você pode rastrear qual controlador de mídia está fazendo uma solicitação usando o packageName do objeto MediaSession.ControllerInfo que é transmitidos para os métodos Callback. Isso permite que você personalize o comportamento do usuário em resposta a um comando, caso ele seja originado no sistema, próprio app ou de outros apps clientes.

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

Depois de lidar com um comando personalizado ou qualquer outra interação com seu player, você atualizar o layout exibido na interface do controlador. Um exemplo típico é um botão ativar que muda de ícone após acionar a ação associada com este botão. Para atualizar o layout, você pode usar MediaSession.setCustomLayout:

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

Personalizar o comportamento do comando de reprodução

Para personalizar o comportamento de um comando definido na interface Player, como como play() ou seekToNext(), envolva a Player em um ForwardingPlayer.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

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

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

Para saber mais sobre o ForwardingPlayer, 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 identifique a fonte de origem com MediaSession.controllerForCurrentRequest e receba o ControllerInfo da solicitação atual:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

Responder aos botões de mídia

Botões de mídia são botões de hardware encontrados em dispositivos Android e outros periféricos dispositivos, como o botão reproduzir/pausar em um fone de ouvido Bluetooth. Identificadores da Media3 eventos do botão de mídia quando eles chegarem à sessão e chamarem o método Player apropriado no player de sessão.

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

Tratamento de erros e geração de relatórios

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

Erros fatais de reprodução

Um erro fatal de reprodução é relatado à sessão pelo jogador e, em seguida, informados aos controladores para fazer chamadas por meio 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 de erro e a mensagem de acordo com 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 um erro fatal de reprodução podem ser personalizados por 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 no player real. e intercepta callbacks que informam um erro. Um PlaybackException é delegada aos listeners que estejam registrados no player de encaminhamento. Para que isso funcione, o player de encaminhamento substitui Player.addListener e Player.removeListener para ter acesso ao listeners com os quais enviar códigos de erro personalizados, mensagens ou extras:

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

// 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ções de mídia é replicado para o PlaybackStateCompat da sessão da plataforma. Assim, apenas o código de erro e a mensagem de erro será definida como PlaybackStateCompat, enquanto PlaybackStateCompat.state não muda 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.
              }
            });