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
. Ao conectar a sessão de mídia ao player, um app pode anunciar a reprodução de mídia externamente e receber comandos de reprodução de fontes externas.
Os comandos podem vir 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 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 o MediaSession
, você permite que os usuários controlem a reprodução:
- Usando fones de ouvido. Geralmente, há botões ou interações de toque que um usuário pode fazer nos fones de ouvido para tocar ou pausar mídia ou ir para a faixa seguinte ou anterior.
- Ao falar com o Google Assistente. Um padrão comum é dizer "Ok Google, pause" para pausar qualquer mídia que esteja sendo reproduzida no dispositivo.
- No relógio Wear OS. Isso facilita o acesso aos controles de reprodução mais comuns durante a reprodução no smartphone.
- Usando os 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 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 A/V for desligado ou a entrada for trocada, a reprodução será interrompida no app.
- Usando os controles de mídia do Android Auto. Isso permite o controle seguro da reprodução ao dirigir.
- e outros processos externos que precisam influenciar a reprodução.
Isso é ótimo para muitos casos de uso. Em especial, 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 no MediaSession
. Convém usar apenas o Player
nos seguintes casos:
- Você está mostrando conteúdo de formato curto, em que não é necessário controle externo ou reprodução em segundo plano.
- 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 único de introdução ou explicação, 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 incógnito em um navegador).
Se o seu caso de uso não se encaixa em nenhum dos listados acima, pense 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 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 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 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();
Tratamento automático de estados
A biblioteca Media3 atualiza automaticamente a sessão de mídia usando o estado do player. Assim, não é necessário processar manualmente o mapeamento do jogador 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 exclusivo da sessão
Por padrão, MediaSession.Builder
cria uma sessão com uma string vazia como ID. 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ê vir um IllegalStateException
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
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 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. Ele 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 o Google Assistente a "pausar". Da mesma forma, talvez você queira conceder acesso ao sistema Android para facilitar os controles de notificação e tela de bloqueio ou a um relógio Wear OS para controlar a reprodução no mostrador do relógio. Clientes externos podem usar um controle de mídia para emitir comandos de reprodução ao seu app de mídia. Esses comandos são recebidos pela sessão de mídia, que delega os comandos ao player de mídia.

Quando um controlador está prestes a se conectar à sua sessão de mídia, o método
onConnect()
é chamado. Use o ControllerInfo
fornecido para decidir se você vai aceitar ou rejeitar o pedido. Confira um exemplo de como aceitar uma solicitação de conexão na seção Declarar comandos personalizados.
Depois de conectado, 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
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.
Os controladores também podem modificar a playlist se COMMAND_SET_MEDIA_ITEM
ou COMMAND_CHANGE_MEDIA_ITEMS
estiver disponível para eles.
Ao adicionar novos itens à playlist, o player geralmente 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 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
geralmente 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 totalmente novas, você 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 começar no índice do item originalmente solicitado. Um
exemplo de implementação de onSetMediaItems()
com esse recurso pode ser encontrado no app de demonstração de sessão.
Gerenciar preferências de botão 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 as preferências de botão de mídia no MediaSession
. Essas preferências consistem em uma lista ordenada de instâncias CommandButton
, cada uma definindo uma preferência por um botão na interface do usuário.
Definir botões de comando
As instâncias CommandButton
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:
- O ícone, que define a aparência visual. O ícone precisa ser definido como uma das
constantes predefinidas ao criar um
CommandButton.Builder
. Essa não é uma imagem bitmap ou um 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 adequar ao seu caso de uso, usesetCustomIconResId
. - O comando, que define a ação acionada quando o usuário interage com
o botão. Você pode usar
setPlayerCommand
para umPlayer.Command
ousetSessionCommand
para umSessionCommand
predefinido ou personalizado. - 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 no Comando. Por exemplo, é possível especificar que um botão seja mostrado na área de navegação "para frente" da interface em vez da área "de estouro" padrão.
Kotlin
val button = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build()
Java
CommandButton button = new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build();
Quando as preferências de botões de mídia são resolvidas, o seguinte algoritmo é aplicado:
- Para cada
CommandButton
nas preferências de botão de mídia, coloque o botão no primeiro slot disponível e permitido. - Se algum dos slots central, de encaminhamento e de retorno não estiver preenchido com um botão, adicione botões padrão para esse slot.
Você pode usar CommandButton.DisplayConstraints
para gerar uma prévia de como
as preferências do botão de mídia serão resolvidas dependendo das restrições de exibição
da interface.
Definir preferências de botão de mídia
A maneira mais fácil de definir as preferências do botão de mídia é definir a lista ao
criar o MediaSession
. Como alternativa, você pode substituir
MediaSession.Callback.onConnect
para personalizar as preferências do botão de mídia de
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 seja necessário 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
esse botão. Para atualizar as preferências do botão 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 disponíveis do player podem ser estendidos por comandos personalizados. Também é possível interceptar comandos 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 nas
preferências de botão de mídia. Por exemplo, você pode
implementar botões que permitem 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, é necessário substituir
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 ): 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 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 para os métodos Callback
. Isso permite adaptar o comportamento do app em resposta a um comando, seja ele originado do sistema, do seu próprio app ou de outros apps clientes.
Personalizar comandos padrão do player
Todos os comandos padrão e o processamento de estado são delegados ao Player
que está no
MediaSession
. Para personalizar o comportamento de um comando definido na interface
Player
, como play()
ou seekToNext()
, encapsule seu 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 em
Personalização.
Identificar o controlador solicitante de um comando do player
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); } }
Personalizar o processamento 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 reproduzir/pausar em um fone de ouvido Bluetooth. O Media3 processa
eventos de botão 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ão de mídia recebidos no método Player
correspondente. Para casos de uso mais avançados, os eventos do botão de mídia
podem ser interceptados em MediaSession.Callback.onMediaButtonEvent(Intent)
.
Tratamento e relatórios de erros
Há dois tipos de erros que uma sessão emite e informa aos controladores. Os 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 manualmente aos controladores pelo aplicativo.
Erros fatais de reprodução
Um erro fatal de reprodução é informado à sessão pelo player e, em seguida,
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 na sessão da plataforma
ao fazer a transição do estado para STATE_ERROR
e definir o código e a mensagem de erro
de acordo com o PlaybackException
.
Personalização de erros fatais
Para fornecer informações localizadas e significativas ao usuário, o código, a mensagem e os extras 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 pode usar ForwardingSimpleBasePlayer
para interceptar o
erro e personalizar o código, a mensagem ou os extras. Da mesma forma, você também pode 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 têm origem em 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ções 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 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. } });