Muitas vezes, é desejável reproduzir mídia enquanto um app não está em primeiro plano. Por exemplo, um player de música geralmente continua tocando música quando o usuário bloqueia o dispositivo ou está usando outro app. A biblioteca Media3 oferece uma série de interfaces que oferecem suporte à reprodução em segundo plano.
Usar um MediaSessionService
Para ativar a reprodução em segundo plano, é preciso conter Player
e
MediaSession
dentro de um Service separado.
Isso permite que o dispositivo continue exibindo mídia mesmo quando o app não estiver em
primeiro plano.
Ao hospedar um jogador 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
possibilita que clientes externos, como o Google
Assistente, controles de mídia do sistema ou dispositivos complementares, como o Wear OS, descubram
seu serviço, se conectem a ele e controlem a reprodução, tudo isso sem precisar 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
Você precisa implementar três 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 criarPlayer
eMediaSession
.- O
onTaskRemoved(Intent)
é chamado quando o usuário dispensa o app das tarefas recentes. Se a reprodução estiver em andamento, o app poderá optar por manter o serviço em execução em primeiro plano. Se o player estiver pausado, o serviço não está em primeiro plano e precisa ser interrompido. onDestroy()
é chamado quando o serviço está sendo interrompido. Todos os recursos, incluindo o jogador e a sessão, precisam ser liberados.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // Create your player and media session in the onCreate lifecycle event override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } // The user dismissed the app from the recent tasks override fun onTaskRemoved(rootIntent: Intent?) { val player = mediaSession?.player!! if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == Player.STATE_ENDED) { // Stop the service if not playing, continue playing in the background // otherwise. stopSelf() } } // Remember to release the player and media session in onDestroy override fun onDestroy() { mediaSession?.run { player.release() release() mediaSession = null } super.onDestroy() } }
Java
public 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(); } // The user dismissed the app from the recent tasks @Override public void onTaskRemoved(@Nullable Intent rootIntent) { Player player = mediaSession.getPlayer(); if (!player.getPlayWhenReady() || player.getMediaItemCount() == 0 || player.getPlaybackState() == Player.STATE_ENDED) { // Stop the service if not playing, continue playing in the background // otherwise. stopSelf(); } } // Remember to release the player and media session in onDestroy @Override public void onDestroy() { mediaSession.getPlayer().release(); mediaSession.release(); mediaSession = null; super.onDestroy(); } }
Como alternativa de manter a reprodução em andamento em segundo plano, o app pode interromper o serviço a qualquer momento quando o usuário dispensa o app:
Kotlin
override fun onTaskRemoved(rootIntent: Intent?) { val player = mediaSession.player if (player.playWhenReady) { // Make sure the service is not in foreground. player.pause() } stopSelf() }
Java
@Override public void onTaskRemoved(@Nullable Intent rootIntent) { Player player = mediaSession.getPlayer(); if (player.getPlayWhenReady()) { // Make sure the service is not in foreground. player.pause(); } stopSelf(); }
Conceder acesso à sessão de mídia
Substitua o método onGetSession()
para conceder a outros clientes acesso à sua sessão de mídia
criada quando o serviço foi criado.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // [...] lifecycle methods omitted override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // [...] lifecycle methods omitted @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } }
Declarar o serviço no manifesto
Um app precisa de permissão para executar um serviço em primeiro plano. Adicione a
permissão FOREGROUND_SERVICE
ao manifesto e, se você segmentar a API 34 e
versões mais recentes, também adicione FOREGROUND_SERVICE_MEDIA_PLAYBACK
:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
Também é necessário declarar a classe Service
no manifesto com um filtro de intent
de MediaSessionService
.
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
Você precisa definir um
foregroundServiceType
que inclua mediaPlayback
quando o app estiver sendo executado em um dispositivo com o Android
10 (nível 29 da API) e versões mais recentes.
Controlar a reprodução usando um MediaController
Na atividade ou no fragmento que contém a interface do player, você pode 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 ao player na
sessão. Consulte o guia
Criar um MediaController
para detalhes sobre como criar e usar um MediaController
.
Gerenciar comandos da interface
O MediaSession
recebe comandos do controlador pelo
MediaSession.Callback
. A inicialização de uma 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
, que
funciona na maioria dos casos. Por padrão, a notificação publicada é uma
notificação MediaStyle
que é atualizada com as informações mais recentes
da sua sessão de mídia e exibe controles de mídia. O MediaNotification
está ciente da sua sessão e pode ser usado para controlar a reprodução de outros apps
conectados à mesma sessão.
Por exemplo, um app de streaming de música usando um MediaSessionService
criaria uma
MediaNotification
que mostra o título, o artista e a arte do álbum para o
item de mídia que está sendo reproduzido com os controles de reprodução 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 abaixo:
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();
Os apps podem personalizar os botões de comando dos controles de mídia do Android. Leia mais sobre como personalizar os controles de mídia do Android.
Personalização de notificações
Para personalizar a notificação, crie uma
MediaNotification.Provider
com DefaultMediaNotificationProvider.Builder
ou criando uma implementação personalizada da interface do provedor. Adicione o
provedor ao MediaSessionService
com
setMediaNotificationProvider
.
Retomada da reprodução
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 ou pausa em um fone de ouvido Bluetooth. A Media3 processa entradas do botão de mídia para você quando o serviço está em execução.
Declarar o receptor do botão de mídia Media3
A Media3 inclui uma API para permitir que os usuários retomem
a reprodução após o encerramento de um app e mesmo após o dispositivo ser
reiniciado. Por padrão, a retomada da reprodução é desativada, ou seja, o usuário
não pode retomar a reprodução quando o serviço não estiver em execução. Para ativar, comece
declarando 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 de 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: ControllerInfo ): ListenableFuture<MediaItemsWithStartPosition> { val settable = SettableFuture.create<MediaItemsWithStartPosition>() scope.launch { // Your app is responsible for storing the playlist and the start position // to use here val resumptionPlaylist = restorePlaylist() settable.set(resumptionPlaylist) } return settable }
Java
@Override public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption( MediaSession mediaSession, ControllerInfo controller ) { SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create(); settableFuture.addListener(() -> { // Your app is responsible for storing the playlist and the start position // to use here MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist(); settableFuture.set(resumptionPlaylist); }, MoreExecutors.directExecutor()); return settableFuture; }
Se você tiver armazenado outros parâmetros, como velocidade de reprodução, 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.
Configuração avançada do controlador e compatibilidade com versões anteriores
Um cenário comum é usar uma MediaController
na interface do app para controlar
a reprodução e exibir a playlist. Ao mesmo tempo, a sessão é exposta
a clientes externos, como os controles de mídia do Android e o Google Assistente em dispositivos móveis ou TV,
Wear OS para relógios e 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 a MediaControllerCompat
da biblioteca
AndroidX legada ou o android.media.session.MediaController
do framework
do Android. A Media3 é totalmente compatível com versões anteriores da biblioteca legada e
oferece interoperabilidade com a API do framework do Android.
Usar o controlador de notificações de mídia
É importante entender que esses controladores legados ou de framework leem os
mesmos valores do framework PlaybackState.getActions()
e
PlaybackState.getCustomActions()
. Para determinar as ações e ações personalizadas da
sessão do framework, um app pode usar o controlador de notificações de mídia
e definir os comandos disponíveis e o layout personalizado. O serviço conecta o controlador
de notificação de mídia à sua sessão, e a sessão usa o
ConnectionResult
retornado pelo onConnect()
do callback para configurar
ações e ações personalizadas da sessão do framework.
Em um cenário exclusivo para dispositivos móveis, um app pode fornecer uma implementação de
MediaSession.Callback.onConnect()
para definir comandos disponíveis e
layout personalizado especificamente para a sessão do framework, desta forma:
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { if (session.isMediaNotificationController(controller)) { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() 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 layout and available commands to configure the legacy/framework session. return AcceptedResultBuilder(session) .setCustomLayout( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward)) ) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default custom layout for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { if (session.isMediaNotificationController(controller)) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); 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 layout and available commands to configure the legacy/framework session. return new AcceptedResultBuilder(session) .setCustomLayout( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward))) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands without default custom layout for all other controllers. return new AcceptedResultBuilder(session).build(); }
Autorizar o Android Auto a enviar comandos personalizados
Ao usar um MediaLibraryService
e oferecer suporte ao Android Auto no app para dispositivos móveis, o controlador do Android Auto
exige os comandos disponíveis apropriados. Caso contrário, o Media3 negaria
os comandos personalizados recebidos desse controle:
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { val sessionCommands = ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available session commands to accept incoming custom commands from Auto. return AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default custom layout for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available commands to accept incoming custom commands from Auto. return new AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands without default custom layout for all other controllers. return new AcceptedResultBuilder(session).build(); }
O app de demonstração da sessão tem um módulo automotivo que demonstra suporte ao Automotive OS que requer um APK separado.