Reprodução em segundo plano com uma MediaSessionService

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 oferecer suporte à 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 oferecendo mídia mesmo quando o app não está em primeiro plano.

O MediaSessionService permite que a sessão de mídia seja executada separadamente
  da atividade do app.
Figura 1: o MediaSessionService permite que a sessão de mídia seja executada separadamente da atividade do app

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, 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 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

É 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 criar Player e MediaSession.
  • 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 é mantido em execução se a reprodução estiver em andamento e será interrompido caso contrário.

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()
  }

  // 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();
  }

  // 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, você pode parar o serviço em qualquer caso quando o usuário dispensar o app:

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

Para qualquer outra implementação manual de onTaskRemoved, use isPlaybackOngoing() para verificar se a reprodução está em andamento e se o serviço em primeiro plano foi iniciado.

Conceder acesso à sessão de mídia

Modifique 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 das 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 a 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 ao 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 uma MediaSession cria uma implementação padrão de MediaSession.Callback que processa automaticamente todos os comandos que uma 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 um MediaSessionService cria um MediaNotification que mostra o título, o artista e a capa do álbum do item de mídia atual que está 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();

Ciclo de vida da notificação

A notificação é criada assim que o Player tem instâncias de MediaItem na playlist.

Todas as atualizações de notificação acontecem automaticamente com base no estado Player e MediaSession.

A notificação não pode ser removida enquanto o serviço em primeiro plano está 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 transferido para fora 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 personalizadas para os controles de mídia do Android. Saiba 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. Adicione o provedor ao MediaSessionService com setMediaNotificationProvider.

Retomada da reprodução

Depois que o MediaSessionService é encerrado e mesmo depois que o dispositivo é reinicializado, é possível oferecer a retomada da reprodução para permitir 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, você precisa declarar um receptor de botão de mídia e implementar o método onPlaybackResumption.

Declarar o receptor do botão de mídia 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 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ê 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, Wear OS para relógios e 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 da plataforma Android. O Media3 é totalmente compatível com a biblioteca legada e oferece interoperabilidade com a API da plataforma Android.

Usar o controle de notificação de mídia

É importante entender que esses controladores legados e de plataforma compartilham o mesmo estado, e a visibilidade não pode ser personalizada por controlador (por exemplo, o PlaybackState.getActions() e o 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 do botão de mídia especificamente para a sessão da plataforma 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 button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences 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 button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences 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 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 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.