Muitas vezes, é interessante reproduzir mídia enquanto um app não está em primeiro plano. Por exemplo, um player de música geralmente continua tocando quando o usuário bloqueia o dispositivo ou usa outro app. A biblioteca Media3 oferece uma série de interfaces que permitem a reprodução em segundo plano.
Usar um MediaSessionService
Para ativar a reprodução em segundo plano, inclua o Player e o
MediaSession em um Service separado.
Isso permite que o dispositivo continue veiculando mídia mesmo quando o app não está em
primeiro plano.
MediaSessionService permite que a sessão de mídia
seja executada separadamente da atividade do appAo hospedar um player em um serviço, use um MediaSessionService.
Para fazer isso, crie uma classe que estenda MediaSessionService e crie sua
sessão de mídia dentro dela.
O uso de MediaSessionService permite que clientes externos, como o Google
Assistente, controles de mídia do sistema, botões de mídia em dispositivos periféricos ou
dispositivos complementares, como o Wear OS, descubram seu serviço, se conectem a ele e
controlem a reprodução, tudo sem acessar a atividade da interface do app. Na verdade,
pode haver vários apps clientes conectados ao mesmo MediaSessionService ao
mesmo tempo, cada um com o próprio MediaController.
Implementar o ciclo de vida do serviço
É necessário implementar dois métodos de ciclo de vida do serviço:
onCreate()é chamado quando o primeiro controlador está prestes a se conectar e o serviço é instanciado e iniciado. É o melhor lugar para criarPlayereMediaSession.onDestroy()é chamado quando o serviço está sendo interrompido. Todos os recursos, incluindo o player e a sessão, precisam ser liberados.
Você pode substituir onTaskRemoved(Intent) para personalizar o que acontece
quando o usuário dispensa o app das tarefas recentes. Por padrão, o serviço
permanece em execução se a reprodução estiver em andamento e é interrompido caso contrário.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // Create your Player and MediaSession in the onCreate lifecycle event override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } // Remember to release the player and media session in onDestroy override fun onDestroy() { mediaSession?.run { player.release() release() mediaSession = null } super.onDestroy() } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession }
Java
class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // Create your Player and MediaSession in the onCreate lifecycle event @Override public void onCreate() { super.onCreate(); ExoPlayer player = new ExoPlayer.Builder(this).build(); mediaSession = new MediaSession.Builder(this, player).build(); } // Remember to release the player and media session in onDestroy @Override public void onDestroy() { mediaSession.getPlayer().release(); mediaSession.release(); mediaSession = null; super.onDestroy(); } @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } }
Como alternativa para manter a reprodução em segundo plano, você pode interromper o serviço em qualquer caso quando o usuário dispensar o app:
Kotlin
@OptIn(UnstableApi::class) override fun onTaskRemoved(rootIntent: Intent?) { pauseAllPlayersAndStopSelf() }
Java
@OptIn(markerClass = UnstableApi.class) @Override public void onTaskRemoved(@Nullable Intent rootIntent) { pauseAllPlayersAndStopSelf(); }
Para qualquer outra implementação manual de onTaskRemoved, use
isPlaybackOngoing() para verificar se a reprodução é considerada em andamento e se o
serviço em primeiro plano foi iniciado.
Fornecer acesso à sessão de mídia
Substitua o método onGetSession() para dar a outros clientes acesso à sessão de mídia
criada quando o serviço foi criado.
Kotlin
class PlaybackService : MediaSessionService() { // [...] lifecycle methods omitted override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession }
Java
class PlaybackService extends MediaSessionService { // [...] lifecycle methods omitted @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } }
Declarar o serviço no manifesto
Um app requer as permissões FOREGROUND_SERVICE e FOREGROUND_SERVICE_MEDIA_PLAYBACK
para executar um serviço em primeiro plano de reprodução:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
Você também precisa declarar sua classe Service no manifesto com um filtro de intent
de MediaSessionService e um foregroundServiceType que inclua
mediaPlayback.
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
Controlar a reprodução usando uma MediaController
Na atividade ou no fragmento que contém a interface do player, é possível estabelecer um link
entre a interface e a sessão de mídia usando um MediaController. A interface usa
o controlador de mídia para enviar comandos da interface para o player na
sessão. Consulte o guia
Criar um MediaController
para saber como criar e usar um MediaController.
Processar comandos MediaController
O MediaSession recebe comandos do controlador pelo
MediaSession.Callback. A inicialização de um MediaSession cria uma implementação
padrão de MediaSession.Callback que processa automaticamente todos os
comandos que um MediaController envia ao player.
Notificação
Um MediaSessionService cria automaticamente um MediaNotification para você que
deve funcionar na maioria dos casos. Por padrão, a notificação publicada é uma
notificação do MediaStyle que se mantém atualizada com as informações
mais recentes da sessão de mídia e mostra os controles de mídia. O
MediaNotification reconhece sua sessão e pode ser usado para controlar a reprodução
de qualquer outro app conectado à mesma sessão.
Por exemplo, um app de streaming de música que usa um MediaSessionService criaria uma
MediaNotification que mostra o título, o artista e a arte do álbum do
item de mídia que está sendo reproduzido, além de controles de mídia com base na sua
configuração de MediaSession.
Os metadados necessários podem ser fornecidos na mídia ou declarados como parte do item de mídia, como no snippet a seguir:
Kotlin
val mediaItem = MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build() ) .build() mediaController.setMediaItem(mediaItem) mediaController.prepare() mediaController.play()
Java
MediaItem mediaItem = new MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( new MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build()) .build(); mediaController.setMediaItem(mediaItem); mediaController.prepare(); mediaController.play();
Ciclo de vida da notificação
A notificação é criada assim que o Player tem instâncias MediaItem
na playlist.
Todas as atualizações de notificação acontecem automaticamente com base no estado de Player e
MediaSession.
A notificação não pode ser removida enquanto o serviço em primeiro plano estiver em execução. Para
remover a notificação imediatamente, chame Player.release() ou limpe
a playlist usando Player.clearMediaItems().
Se o player for pausado, interrompido ou falhar por mais de 10 minutos sem outras interações do usuário, o serviço será automaticamente removido do estado de serviço em primeiro plano para que possa ser destruído pelo sistema. É possível implementar a retomada da reprodução para permitir que um usuário reinicie o ciclo de vida do serviço e retome a reprodução mais tarde.
Personalização de notificações
Os metadados sobre o item em reprodução podem ser personalizados modificando
o MediaItem.MediaMetadata. Se você quiser atualizar os metadados de um item
existente, use Player.replaceMediaItem para atualizar os metadados sem
interromper a reprodução.
Também é possível personalizar alguns dos botões mostrados na notificação definindo preferências de botão de mídia personalizadas para os controles de mídia do Android. Leia mais sobre como personalizar os controles de mídia do Android.
Para personalizar ainda mais a notificação, crie um
MediaNotification.Provider
com DefaultMediaNotificationProvider.Builder
ou criando uma implementação personalizada da interface do provedor. Sua vez
do provedor ao MediaSessionService com
setMediaNotificationProvider.
Retomada da reprodução
Depois que o MediaSessionService for encerrado, e mesmo após a reinicialização do dispositivo, é possível oferecer a retomada da reprodução para que os usuários
reiniciem o serviço e retomem a reprodução de onde pararam. Por padrão,
a retomada da reprodução está desativada. Isso significa que o usuário não pode retomar a reprodução
quando o serviço não está em execução. Para ativar esse recurso, declare
um receptor de botão de mídia e implemente o método onPlaybackResumption.
Declarar o receptor de botão de mídia do Media3
Comece declarando o MediaButtonReceiver no manifesto:
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
Implementar o callback de retomada da reprodução
Quando a retomada da reprodução é solicitada por um dispositivo Bluetooth ou pelo
recurso de retomada da interface do sistema Android,
o método de callback onPlaybackResumption() é
chamado.
Kotlin
override fun onPlaybackResumption( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, isForPlayback: Boolean, ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> { val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>() settableFuture.addListener( { // Your app is responsible for storing the playlist, metadata (like title // and artwork) of the current item and the start position to use here. val resumptionPlaylist = restorePlaylist() settableFuture.set(resumptionPlaylist) }, MoreExecutors.directExecutor(), ) return settableFuture }
Java
@Override public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption( MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) { SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create(); settableFuture.addListener( () -> { // Your app is responsible for storing the playlist, metadata (like title // and artwork) of the current item and the start position to use here. MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist(); settableFuture.set(resumptionPlaylist); }, MoreExecutors.directExecutor()); return settableFuture; }
Se você armazenou outros parâmetros, como velocidade do vídeo, modo de repetição ou
modo de ordem aleatória, onPlaybackResumption() é um bom lugar para configurar o player
com esses parâmetros antes que o Media3 prepare o player e inicie a reprodução quando
o callback for concluído.
Esse método é chamado durante a inicialização para criar a notificação de retomada da interface do sistema Android após uma reinicialização do dispositivo com isForPlayback definido como false. Para uma notificação avançada, é recomendável preencher os campos MediaMetadata,
como title e artworkData ou artworkUri do item atual, com
valores disponíveis localmente, já que o acesso à rede pode ainda não estar disponível. Também é possível
adicionar MediaConstants.EXTRAS_KEY_COMPLETION_STATUS e
MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE ao MediaMetadata.extras
para indicar a posição de retomada da reprodução.
Configuração avançada do controlador e compatibilidade com versões anteriores
Um cenário comum é usar um MediaController na interface do app para controlar
a reprodução e mostrar a playlist. Ao mesmo tempo, a sessão é exposta
a clientes externos, como os controles de mídia do Android e o Assistente em dispositivos móveis ou TVs,
o Wear OS para relógios e o Android Auto em carros. O app de demonstração de sessão
da Media3 é um exemplo de app que implementa esse cenário.
Esses clientes externos podem usar APIs como MediaControllerCompat da biblioteca legada
do AndroidX ou android.media.session.MediaController da plataforma
Android. A Media3 é totalmente compatível com versões anteriores da biblioteca legada e
oferece interoperabilidade com a API da plataforma Android.
Identificar controladores confiáveis
Qualquer app pode tentar se conectar à sua sessão ou biblioteca de mídia. Se você quiser
restringir o acesso a controladores do sistema, controladores com permissão de controle de conteúdo de mídia
e seu próprio app, use ControllerInfo.isTrusted() para uma
verificação de acesso básica. Como alternativa, é possível identificar controladores mais específicos,
como o controlador de notificação de mídia ou os controladores do Android Auto, conforme descrito
nas seções a seguir.
Usar o controlador de notificações de mídia
É importante entender que esses controladores legados e de plataforma compartilham
o mesmo estado, e a visibilidade não pode ser personalizada pelo controlador (por exemplo, os
PlaybackState.getActions() e PlaybackState.getCustomActions() disponíveis).
Você pode usar o controlador de notificação de mídia para
configurar o estado definido na sessão de mídia da plataforma para compatibilidade com esses
controladores legados e de plataforma.
Por exemplo, um app pode fornecer uma implementação de
MediaSession.Callback.onConnect() para definir comandos disponíveis e
preferências de botão de mídia especificamente para a sessão da plataforma, da seguinte maneira:
Kotlin
override fun onConnectAsync( session: MediaSession, controller: MediaSession.ControllerInfo, ): ListenableFuture<ConnectionResult> { if (session.isMediaNotificationController(controller)) { val playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build() // Custom button preferences and commands to configure the platform session. return immediateFuture( AcceptedResultBuilder(session, controller) .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton)) .setAvailablePlayerCommands(playerCommands) .build() ) } // Default commands with default button preferences for all other controllers. return immediateFuture(AcceptedResultBuilder(session, controller).build()) }
Java
@Override public ListenableFuture<ConnectionResult> onConnectAsync( MediaSession session, MediaSession.ControllerInfo controller) { if (session.isMediaNotificationController(controller)) { Player.Commands playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS .buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build(); // Custom button preferences and commands to configure the platform session. return immediateFuture( new AcceptedResultBuilder(session, controller) .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton)) .setAvailablePlayerCommands(playerCommands) .build()); } // Default commands with default button preferences for all other controllers. return immediateFuture(new AcceptedResultBuilder(session, controller).build()); }
Autorizar o Android Auto a enviar comandos personalizados
Ao usar um MediaLibraryService
e para oferecer suporte ao Android Auto com o app para dispositivos móveis, o controlador do Android Auto
requer comandos disponíveis adequados. Caso contrário, o Media3 vai negar
comandos personalizados recebidos desse controlador:
Kotlin
override fun onConnectAsync( session: MediaSession, controller: MediaSession.ControllerInfo, ): ListenableFuture<ConnectionResult> { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build() if (session.isMediaNotificationController(controller)) { // ... See above. } else if (session.isAutoCompanionController(controller)) { // Available commands to accept incoming custom commands from Auto. return immediateFuture( AcceptedResultBuilder(session, controller) .setAvailableSessionCommands(sessionCommands) .build() ) } // Default commands for all other controllers. return immediateFuture(AcceptedResultBuilder(session, controller).build()) }
Java
@Override public ListenableFuture<ConnectionResult> onConnectAsync( MediaSession session, MediaSession.ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build(); if (session.isMediaNotificationController(controller)) { // ... See above. } else if (session.isAutoCompanionController(controller)) { // Available commands to accept incoming custom commands from Auto. return immediateFuture( new AcceptedResultBuilder(session, controller) .setAvailableSessionCommands(sessionCommands) .build()); } // Default commands for all other controllers. return immediateFuture(new AcceptedResultBuilder(session, controller).build()); }
O app de demonstração da sessão tem um módulo automotivo, que demonstra o suporte ao Automotive OS, que requer um APK separado.