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. Na 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 avise a reprodução de mídia externamente e receba comandos 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. Eles também podem vir de apps clientes que têm um controlador de mídia, como ao instruir "pausar" ao Google Assistente. A sessão de mídia delega esses comandos ao player do app de música.

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 realizar nos fones de ouvido para reproduzir ou pausar mídia ou acessar a próxima faixa ou a 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 facilita o acesso aos controles de reprodução mais comuns durante a reprodução no smartphone.
  • Pelos Controles de mídia. Esse carrossel mostra os controles para 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, o soundbar ou o receptor A/V forem desligados ou a entrada for trocada, a reprodução precisará ser interrompida no app.
  • E qualquer outro processo externo que precise influenciar a reprodução.

Isso é ótimo para muitos casos de uso. Especificamente, 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 de formato longo, como podcasts ou playlists de música.
  • Você está criando um app de TV.

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

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

Caso seu caso de uso não se enquadre em nenhum dos listados acima, considere se o app pode continuar a reprodução quando o usuário não estiver interagindo ativamente com o conteúdo. Se a resposta for sim, talvez seja melhor 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. Você pode criar uma sessão de mídia com um Context e um objeto 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 é proprietário da sessão de mídia e do player associado.

Para criar uma sessão de mídia, inicialize um Player e forneça-o para 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. Assim, você não precisa processar manualmente o mapeamento do jogador para a sessão.

Isso é uma pausa da abordagem legada em que era necessário criar e manter uma PlaybackStateCompat independentemente do próprio 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 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 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ê vir um IllegalStateException fazendo uma falha no app com a mensagem de erro IllegalStateException: Session ID must be unique. ID=, é provável que uma sessão tenha sido criada inesperadamente antes de uma instância criada anteriormente com o mesmo ID ser liberada. Para evitar que as sessões vazem por causa de um erro 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 é a chave para controlar a reprodução. Ela permite encaminhar comandos de fontes externas para o player que executa o trabalho de 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 para instruir "pausar" ao Google Assistente. Da mesma forma, você pode conceder acesso ao sistema Android para facilitar os controles de notificação e da tela de bloqueio ou a um relógio Wear OS para que você possa controlar a reprodução no mostrador do relógio. Clientes externos podem usar um controlador de mídia para emitir comandos de reprodução para seu app de música. Eles são recebidos pela sessão de mídia, que delega comandos ao player de mídia.

Um diagrama demonstrando a interação entre a MediaSession e o 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 controle está prestes a se conectar à sua sessão de mídia, o método onConnect() é chamado. É possível usar a ControllerInfo fornecida para decidir se vai aceitar ou rejeitar a 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. Em seguida, a sessã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 modificação na playlist. Esses callbacks incluem um objeto ControllerInfo para que você possa modificar como 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 no guia do ExoPlayer para playlists (link em inglês). Os controladores também poderão 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 instâncias MediaItem com um URI definido para torná-las reproduzíveis. 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.

Caso queira personalizar as instâncias de MediaItem adicionadas ao player, substitua 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 jogador.
  • MediaItem.RequestMetadata.searchQuery: uma consulta de pesquisa textual, por exemplo, do Google Assistente.
  • MediaItem.MediaMetadata: metadados estruturados, como "título" ou "artista".

Para ter mais opções de personalização para playlists completamente novas, você também 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. Um exemplo de implementação de onSetMediaItems() com esse recurso pode ser encontrado no app de demonstração da 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 clientes e autorizar os 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 para o usuário, defina o layout personalizado da sessão ao criar 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 de player e personalizados disponíveis

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 personalizada estão disponíveis para um MediaController quando ele se conectar à 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 comandos personalizados 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 rastrear qual controlador de mídia está fazendo uma solicitação usando a propriedade packageName do objeto MediaSession.ControllerInfo que é transmitido aos métodos Callback. Isso permite que você personalize o comportamento do seu app em resposta a um determinado comando se ele for do sistema, do seu próprio app ou de 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, atualize o layout mostrado na interface do controle. Um exemplo típico é um botão de ativação que muda o ícone depois de acionar a ação associada a ele. 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 play() ou seekToNext(), envolva 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 ForwardingPlayer, consulte o guia do ExoPlayer sobre Personalização (link em inglês).

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 fonte de origem com MediaSession.controllerForCurrentRequest e adquirir o ControllerInfo para a 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

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 reproduzir/pausar em um fone de ouvido Bluetooth. A Media3 processa os eventos do botão de mídia quando eles chegam à sessão e chama o método Player apropriado no player de sessão.

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