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 bloqueou o dispositivo ou está usando 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 Player
e
MediaSession
em um serviço separado.
Isso permite que o dispositivo continue a veicular mídia mesmo quando o app não está 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 nela.
O uso de MediaSessionService
permite 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 sem acessar a
atividade da IU 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á manter o serviço em execução em primeiro plano. Se o player estiver pausado, o serviço não estará em primeiro plano e precisará ser interrompido. onDestroy()
é chamado quando o serviço está sendo interrompido. Todos os recursos, incluindo o player 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 para manter a reprodução em segundo plano, um app pode parar o serviço em qualquer caso quando o usuário dispensar 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 dar 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 FOREGROUND_SERVICE_MEDIA_PLAYBACK
:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
Você também precisa 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>
É necessário definir uma
foregroundServiceType
que inclua mediaPlayback
quando o app estiver em execução em um dispositivo com o Android
10 (nível 29 da API) ou mais recente.
Controlar a reprodução usando um 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 ao player na
sessão. Consulte o guia
Criar um MediaController
para saber como criar e usar um MediaController
.
Processar comandos da interface
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 para o player.
Notificação
Um MediaSessionService
cria automaticamente um MediaNotification
para você, que
funciona na maioria dos casos. Por padrão, a notificação publicada é uma
notificação MediaStyle
que fica atualizada com as informações mais recentes
da sua sessão de mídia e mostra os controles de reprodução. 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 que usa uma MediaSessionService
cria uma
MediaNotification
que mostra o título, o artista e a arte do álbum do
item de mídia atual sendo reproduzido, além dos controles de reprodução com base na
configuração da 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. Saiba 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. O Media3 processa as entradas de 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
O Media3 inclui uma API para permitir que os usuários retomem
a reprodução depois que um app é encerrado e mesmo depois que o dispositivo é
reiniciado. 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, 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 IU 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ê armazenou 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 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 controles de mídia do Android e o Google 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 do Media3
é um exemplo de app que implementa esse cenário.
Esses clientes externos podem usar APIs como MediaControllerCompat
da biblioteca
AndroidX legada ou android.media.session.MediaController
do framework
Android. O Media3 é totalmente compatível com a biblioteca legada e
oferece interoperabilidade com a API do framework do Android.
Usar o controle de notificação de mídia
É importante entender que esses controladores legados ou de framework leem os
mesmos valores de PlaybackState.getActions()
e
PlaybackState.getCustomActions()
do framework. Para determinar ações e ações personalizadas da
sessão do framework, um app pode usar o controlador de notificação de mídia
e definir os comandos e o layout personalizado disponíveis. 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 da seguinte maneira:
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 com o app para dispositivos móveis, o controlador do Android Auto
precisa de comandos disponíveis adequados. Caso contrário, o Media3 negará
os comandos personalizados recebidos desse controlador:
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.