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. Conectar a sessão de mídia ao player permite que um app anuncia a reprodução de mídia externamente e receba comandos de fontes externas.

Os comandos podem se originar de botões físicos, como o botão "Assistir" em um fone de ouvido ou controle remoto de TV. Eles também podem ser de apps clientes que têm um controlador de mídia, como ao instruir o Google Assistente a "pausar". 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 tocar ou pausar mídia ou ir para a próxima faixa ou a 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 controles de mídia mais comuns durante a reprodução no smartphone.
  • Com os 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, a soundbar ou o receptor de A/V for desligado ou se a entrada for alterada, a reprodução será interrompida no app.
  • E quaisquer outros processos externos que precisem influenciar a reprodução.

Isso é ótimo para muitos casos de uso. Especificamente, considere o uso de 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ê estiver criando um app de TV.

No entanto, nem todos os casos de uso são adequados para MediaSession. 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 são cruciais.
  • Não há um único vídeo ativo, como o usuário está rolando por uma lista e vários vídeos são mostrados na tela ao mesmo tempo.
  • Você está reproduzindo um vídeo único de introdução ou explicação e espera que o usuário assista ativamente.
  • Seu conteúdo confide a privacidade, e você não quer que processos externos acessem os metadados de mídia (por exemplo, no 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 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, escolha MediaSession. Se a resposta for não, é recomendável usar 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 objeto Context e um Player. Crie e inicialize uma sessão de mídia quando ela for necessária. Por exemplo, o método de ciclo de vida onStart() ou onResume() do Activity ou Fragment ou o método 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 forneça-a 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. Dessa forma, não é necessário processar manualmente o mapeamento de jogador para sessão.

Essa é uma pausa da abordagem legada, em que era necessário 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 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 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ê notar uma falha IllegalStateException 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 o vazamento de sessões por um erro de programação, esses casos são detectados e notificados com a geração de uma exceção.

Conceder controle a outros clientes

A sessão de mídia é essencial para controlar a reprodução. Ele permite encaminhar comandos de fontes externas para o player que executa a mídia. Essas origens podem ser botões físicos, como o botão "Assistir" em um fone de ouvido ou controle remoto da TV, ou comandos indiretos, como instruir o Google Assistente a "pausar" . Da mesma forma, você pode conceder acesso ao sistema Android para facilitar os controles de notificação e tela de bloqueio, ou a um relógio Wear OS para 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, por fim, delega comandos ao 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 comandos de fontes externas para a sessão de mídia.

Quando um controle está prestes a se conectar à sessão de mídia, o método onConnect() é chamado. É possível usar o ControllerInfo fornecido para decidir se quer 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 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 modificação da playlist. Da mesma forma, esses callbacks incluem um objeto ControllerInfo para que você possa modificar a forma de responder 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 controladores também poderão modificar a playlist se COMMAND_SET_MEDIA_ITEM ou COMMAND_CHANGE_MEDIA_ITEMS estiverem disponíveis.

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

Se você quiser personalizar as instâncias de MediaItem adicionadas ao jogador, é 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 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, você pode expandir um único item solicitado para uma playlist inteira e instruir o player a iniciar 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 comandos 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 ao usuário, defina o layout personalizado da sessão ao criar a 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 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, implemente botões que permitam 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.

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. Para fazer isso, substitua 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 personalizado de um MediaController, modifique 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 é transmitida aos métodos Callback. Isso permite que você personalize o comportamento do app em resposta a um determinado comando, caso ele seja originado 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, recomendamos atualizar o layout mostrado na interface do controle. Um exemplo típico é um botão ativar que muda de í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(), una o 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 em 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 origem 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

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 para você quando eles chegam à sessão e chama o método Player adequado no player de sessão.

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