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 ser originados de botões físicos, como o botão de reprodução em um fone de ouvido ou controle remoto de TV. Elas 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 MediaSession
, você permite que os usuários controlem a reprodução:
- Pelos fones de ouvido. Muitas vezes, há botões ou interações por toque que o usuário pode realizar nos fones de ouvido para reproduzir ou pausar mídia ou ir para a próxima ou a faixa anterior.
- Falando com o Google Assistente. Um padrão comum é dizer "Ok Google, pause" 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.
- Através dos controles de mídia. Esse carrossel mostra os controles de 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 áudio/vídeo desligar ou se a entrada for trocada, a reprodução será interrompida no app.
- 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ê 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 com o MediaSession
. Talvez seja melhor
usar apenas o Player
nos seguintes casos:
- Você está mostrando conteúdo de formato curto, em que o engajamento e a interação do usuário são essenciais.
- 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 de introdução ou explicação único, que você espera que o usuário assista ativamente.
- Seu conteúdo é sensível à privacidade e você não quer que processos externos acessem os metadados da 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ê
aceita 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", use o 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 detém 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();
Processamento 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 jogador para a sessão.
Isso é uma mudança em relação à abordagem legada, em que você precisava 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
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 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ê notar que um IllegalStateException
está 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
anteriormente criada 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 uma
exceção.
Conceder controle a outros clientes
A sessão de mídia é a chave para controlar a reprodução. Ele permite encaminhar comandos de fontes externas para o player que reproduz a 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 o comando "pausar" para o Google Assistente. Da mesma forma, você pode conceder acesso ao sistema Android para facilitar os controles de notificação e bloqueio da tela ou a um relógio Wear OS para controlar a reprodução pelo mostrador do relógio. Clientes externos podem usar um controle 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.
Quando um controlador está prestes a se conectar à sessão de mídia, o método
onConnect()
é chamado. Use o ControllerInfo
fornecido
para decidir se vai aceitar
ou rejeitar
a solicitação. Confira um exemplo de aceitação de uma solicitação de conexão na seção Declarar
comandos disponíveis.
Depois de se conectar, um controlador pode enviar comandos de reprodução para a sessão. A
sessão entã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
modificar a playlist.
Esses callbacks também incluem um objeto ControllerInfo
para que você possa modificar
como responde 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 controles também podem modificar a playlist se
COMMAND_SET_MEDIA_ITEM
ou COMMAND_CHANGE_MEDIA_ITEMS
estiver disponível para o controle.
Ao adicionar novos itens à playlist, o player normalmente 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 você quiser personalizar as instâncias de MediaItem
adicionadas ao player, é 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 diretamente pelo player.MediaItem.RequestMetadata.searchQuery
: uma consulta de pesquisa textual, por exemplo, do Google Assistente.MediaItem.MediaMetadata
: metadados estruturados, como "title" ou "artist".
Para mais opções de personalização para 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 exemplo de onSetMediaItems()
com esse recurso pode ser encontrada no app de demonstração de 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 cliente e autorizar 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 o 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 o player disponível
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 personalizados estão disponíveis para um
MediaController
quando ele se conecta à 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 comando personalizadas 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 acompanhar qual controlador de mídia está fazendo uma solicitação usando a
propriedade packageName
do objeto MediaSession.ControllerInfo
que é
transmitida para os métodos Callback
. Isso permite personalizar o comportamento
do app em resposta a um determinado comando, se ele for originado pelo sistema, pelo
seu próprio app ou por 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, é recomendável atualizar o layout exibido na interface do controle. Um exemplo típico
é um botão de alternância que muda o ícone depois de acionar a ação associada
a ele. Para atualizar o layout, use
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 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 do jogador
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); } }
Responder a 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
os eventos do botão de mídia para você quando eles chegam à sessão e chama o
método Player
apropriado no player da sessão.
Um app pode substituir o comportamento padrão substituindo
MediaSession.Callback.onMediaButtonEvent(Intent)
. Nesse caso, o app
pode/precisa processar todas as especificidades da API por conta própria.
Tratamento e relatórios de erros
Há dois tipos de erros que uma sessão emite e informa aos controladores. Erros fatais relatam uma falha técnica de reprodução do player de sessão que interrompe a reprodução. 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 pelo aplicativo aos controladores.
Erros fatais de reprodução
Um erro fatal de reprodução é informado ao player pela sessão e, em seguida,
informado 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 para o PlaybackStateCompat
da sessão da plataforma, fazendo a transição do estado para STATE_ERROR
e definindo
o código e a mensagem de erro de acordo com o 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 erro 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 registra um Player.Listener
para o player real
e intercepta callbacks que informam um erro. Um PlaybackException
personalizado é delegado aos listeners que
estão registrados no player de encaminhamento. Para que isso funcione, o player de encaminhamento
substitui Player.addListener
e Player.removeListener
para ter acesso aos
listeners com os quais enviar código de erro, mensagem ou extras personalizados:
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 têm origem em uma exceção técnica podem ser enviados por um app a todos ou a 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ção de mídia é replicado para o
PlaybackStateCompat
da sessão da plataforma. Assim, apenas o código de erro e
a mensagem de erro são definidos para PlaybackStateCompat
, enquanto
PlaybackStateCompat.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. } });