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 quando o usuário bloqueia o dispositivo ou usa 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, coloque 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.

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 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 sem acessar a atividade da interface 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

É 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 player e 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 continua 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 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 a 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 é considerada contínua 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 à 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" />

Também é necessário 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 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 detalhes sobre 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 MediaStyle que fica atualizada com as informações mais recentes da sua sessão de mídia e mostra controles de reprodução. 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 um MediaNotification que mostra o título, o artista e a arte do álbum do item de mídia atual sendo reproduzido junto com os controles de reprodução com base na sua configuração MediaSession.

Os metadados obrigató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 das notificações

A notificação é criada assim que o Player tem MediaItem instâncias 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 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 mais interações do usuário, o serviço será automaticamente transferido do estado de serviço em primeiro plano para que possa ser destruído pelo sistema. Você pode 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 em um momento posterior.

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.

Você também pode personalizar alguns dos botões mostrados na notificação definindo preferências personalizadas de botões de mídia 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 seu 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 fica 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 do 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 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, metadata (like title
    // and artwork) of the current item 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, 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 de reprodução, modo de repetição ou modo aleatório, 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. Para uma notificação avançada, é recomendável preencher campos MediaMetadata, como title e artworkData ou artworkUri do item atual, com valores disponíveis localmente, já que o acesso à rede ainda pode 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 reprodução da retomada.

Configuração avançada de 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 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. A Media3 é totalmente compatível com versões anteriores da biblioteca legada e oferece interoperabilidade com a API da plataforma Android.

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 por controlador (por exemplo, os PlaybackState.getActions() e PlaybackState.getCustomActions() disponíveis). Você pode usar o controlador de notificações de mídia para configurar o conjunto de estados 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ões 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 para oferecer suporte ao Android Auto com o app para dispositivos móveis, o controlador do Android Auto precisa dos comandos disponíveis adequados. Caso contrário, a Media3 negaria 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 de sessão tem um módulo automotivo, que demonstra a compatibilidade com o SO do Android Automotive, que exige um APK separado.