Reprodução em segundo plano com uma MediaSessionService

Muitas vezes, é recomendá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, você precisa conter os atributos 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.

O MediaSessionService permite que a sessão de mídia seja executada separadamente
  da atividade do app.
Figura 1: o MediaSessionService permite que a 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 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 app 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 criar Player e MediaSession.
  • O onTaskRemoved(Intent) é chamado quando o usuário dispensa o app do tarefas recentes. Se a reprodução estiver em andamento, o app poderá optar por manter o serviç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 player e 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 o permissão FOREGROUND_SERVICE para o manifesto e, se você direcionar ao nível 34 da API e acima 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 Fragmento que contém a interface do seu player, você pode estabelecer um link entre a interface e a sessão de mídia usando um MediaController. Sua interface usa o controlador de mídia para enviar comandos da interface de usuário para o player dentro do 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 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 criaria uma MediaNotification que mostra o título, o artista e a capa do álbum do item de mídia atual sendo reproduzido junto com os controles de mídia com base no seu 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 seu provedor ao seu MediaSessionService com setMediaNotificationProvider.

Retomada da reprodução

Botões de mídia são botões de hardware encontrados em dispositivos Android e outros periféricos dispositivos, como o botão de reproduzir ou pausar 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 poderá 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, 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, o onPlaybackResumption() é um bom lugar para configurar o player com esses parâmetros antes que a Media3 prepare o player e inicie a reprodução quando o retorno de chamada é 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 reproduzir e exibir a lista de reprodução. Ao mesmo tempo, a sessão é exposta para clientes externos, como os 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 (link em inglês) Media3 da sessão. é um exemplo de app que implementa esse tipo de cenário.

Esses clientes externos podem usar APIs como a MediaControllerCompat das Biblioteca AndroidX ou android.media.session.MediaController do Android de análise de dados em nuvem. A Media3 é totalmente compatível com versões anteriores da biblioteca legada e Fornece interoperabilidade com a API de 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 as ações personalizadas a sessão do framework, um app poderá usar o controlador de notificação de mídia e definir os comandos disponíveis e o layout personalizado. O serviço conecta a mídia à sua sessão, e a sessão usará o ConnectionResult retornado pelo onConnect() do callback para configurar 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.