Lecture en arrière-plan avec MediaSessionService

Il est souvent souhaitable de lire du contenu multimédia lorsqu'une application n'est pas au premier plan. Par exemple, un lecteur de musique continue généralement de lire de la musique lorsque l'utilisateur a verrouillé son appareil ou utilise une autre application. La bibliothèque Media3 fournit une série d'interfaces qui vous permettent de prendre en charge la lecture en arrière-plan.

Utiliser un MediaSessionService

Pour activer la lecture en arrière-plan, vous devez inclure Player et MediaSession dans un Service distinct. Cela permet à l'appareil de continuer à diffuser du contenu multimédia même lorsque votre application n'est pas au premier plan.

MediaSessionService permet à la session multimédia de s'exécuter séparément de l'activité de l'application.
Figure 1 : MediaSessionService permet à la session multimédia de s'exécuter séparément de l'activité de l'application

Lorsque vous hébergez un lecteur dans un service, vous devez utiliser un MediaSessionService. Pour ce faire, créez une classe qui étend MediaSessionService et créez votre session multimédia à l'intérieur.

L'utilisation de MediaSessionService permet aux clients externes tels que l'Assistant Google, les commandes multimédias système, les boutons multimédias sur les périphériques ou les appareils associés tels que Wear OS de découvrir votre service, de s'y connecter et de contrôler la lecture, le tout sans accéder à l'activité de l'UI de votre application. En fait, plusieurs applications clientes peuvent être connectées au même MediaSessionService en même temps, chacune avec son propre MediaController.

Implémenter le cycle de vie du service

Vous devez implémenter deux méthodes de cycle de vie de votre service :

  • onCreate() est appelé lorsque la première manette est sur le point de se connecter et que le service est instancié et démarré. C'est le meilleur endroit pour créer des Player et des MediaSession.
  • onDestroy() est appelé lorsque le service est arrêté. Toutes les ressources, y compris le lecteur et la session, doivent être libérées.

Vous pouvez éventuellement remplacer onTaskRemoved(Intent) pour personnaliser ce qui se passe lorsque l'utilisateur ferme l'application à partir des tâches récentes. Par défaut, le service reste en cours d'exécution si la lecture est en cours, et est arrêté dans le cas contraire.

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

Au lieu de maintenir la lecture en arrière-plan, vous pouvez arrêter le service dans tous les cas lorsque l'utilisateur ferme l'application :

Kotlin

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

Java

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

Pour toute autre implémentation manuelle de onTaskRemoved, vous pouvez utiliser isPlaybackOngoing() pour vérifier si la lecture est considérée comme en cours et si le service de premier plan est démarré.

Fournir l'accès à la session multimédia

Remplacez la méthode onGetSession() pour donner à d'autres clients l'accès à votre session multimédia créée lors de la création du service.

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;
  }
}

Déclarer le service dans le fichier manifeste

Une application nécessite les autorisations FOREGROUND_SERVICE et FOREGROUND_SERVICE_MEDIA_PLAYBACK pour exécuter un service de lecture au premier plan :

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

Vous devez également déclarer votre classe Service dans le fichier manifeste avec un filtre d'intent de MediaSessionService et un foregroundServiceType qui inclut 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>

Contrôler la lecture à l'aide d'un MediaController

Dans l'activité ou le fragment contenant l'UI de votre lecteur, vous pouvez établir un lien entre l'UI et votre session multimédia à l'aide d'un MediaController. Votre UI utilise le contrôleur multimédia pour envoyer des commandes de votre UI au lecteur dans la session. Pour savoir comment créer et utiliser un MediaController, consultez le guide Créer un MediaController.

Gérer les commandes MediaController

Le MediaSession reçoit les commandes du contrôleur via son MediaSession.Callback. L'initialisation d'un MediaSession crée une implémentation par défaut de MediaSession.Callback qui gère automatiquement toutes les commandes qu'un MediaController envoie à votre lecteur.

Notification

Un MediaSessionService crée automatiquement un MediaNotification pour vous, qui devrait fonctionner dans la plupart des cas. Par défaut, la notification publiée est une notification MediaStyle qui reste à jour avec les dernières informations de votre session multimédia et affiche les commandes de lecture. MediaNotification est conscient de votre session et peut être utilisé pour contrôler la lecture de toutes les autres applications connectées à la même session.

Par exemple, une application de streaming musical utilisant un MediaSessionService créerait un MediaNotification qui affiche le titre, l'artiste et la pochette de l'album de l'élément multimédia en cours de lecture, ainsi que des commandes de lecture basées sur votre configuration MediaSession.

Les métadonnées requises peuvent être fournies dans le contenu multimédia ou déclarées dans l'élément multimédia, comme dans l'extrait suivant :

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

Cycle de vie des notifications

La notification est créée dès que le Player comporte MediaItem instances dans sa playlist.

Toutes les mises à jour des notifications sont automatiques en fonction de l'état Player et MediaSession.

La notification ne peut pas être supprimée tant que le service de premier plan est en cours d'exécution. Pour supprimer immédiatement la notification, vous devez appeler Player.release() ou effacer la playlist à l'aide de Player.clearMediaItems().

Si le lecteur est mis en pause, arrêté ou en échec pendant plus de 10 minutes sans autre interaction de l'utilisateur, le service est automatiquement retiré de l'état de service de premier plan afin qu'il puisse être détruit par le système. Vous pouvez implémenter la reprise de la lecture pour permettre à un utilisateur de redémarrer le cycle de vie du service et de reprendre la lecture ultérieurement.

Personnalisation des notifications

Les métadonnées de l'élément en cours de lecture peuvent être personnalisées en modifiant MediaItem.MediaMetadata. Si vous souhaitez mettre à jour les métadonnées d'un élément existant, vous pouvez utiliser Player.replaceMediaItem pour les modifier sans interrompre la lecture.

Vous pouvez également personnaliser certains des boutons affichés dans la notification en définissant des préférences personnalisées pour les boutons multimédias dans les commandes multimédias Android. En savoir plus sur la personnalisation des commandes multimédias Android

Pour personnaliser davantage la notification elle-même, créez un MediaNotification.Provider avec DefaultMediaNotificationProvider.Builder ou en créant une implémentation personnalisée de l'interface du fournisseur. Ajoutez votre fournisseur à votre MediaSessionService avec setMediaNotificationProvider.

Reprise de la lecture

Une fois le MediaSessionService arrêté, et même après le redémarrage de l'appareil, il est possible de proposer la reprise de la lecture pour permettre aux utilisateurs de redémarrer le service et de reprendre la lecture là où ils l'avaient arrêtée. Par défaut, la reprise de la lecture est désactivée. Cela signifie que l'utilisateur ne peut pas reprendre la lecture lorsque votre service n'est pas en cours d'exécution. Pour activer cette fonctionnalité, vous devez déclarer un récepteur de bouton multimédia et implémenter la méthode onPlaybackResumption.

Déclarer le récepteur de bouton multimédia Media3

Commencez par déclarer MediaButtonReceiver dans votre fichier manifeste :

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

Implémenter le rappel de reprise de la lecture

Lorsque la reprise de la lecture est demandée par un appareil Bluetooth ou par la fonctionnalité de reprise de l'UI du système Android, la méthode de rappel onPlaybackResumption() est appelée.

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;
}

Si vous avez stocké d'autres paramètres tels que la vitesse de lecture, le mode Répéter ou le mode Lecture aléatoire, onPlaybackResumption() est un bon endroit pour configurer le lecteur avec ces paramètres avant que Media3 ne prépare le lecteur et ne lance la lecture lorsque le rappel est terminé.

Cette méthode est appelée au moment du démarrage pour créer la notification de reprise de l'UI du système Android après le redémarrage de l'appareil. Pour une notification enrichie, il est recommandé de renseigner les champs MediaMetadata tels que title et artworkData ou artworkUri de l'élément actuel avec des valeurs disponibles localement, car l'accès au réseau peut ne pas être encore disponible. Vous pouvez également ajouter MediaConstants.EXTRAS_KEY_COMPLETION_STATUS et MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE à MediaMetadata.extras pour indiquer la position de lecture de reprise.

Configuration avancée des manettes et rétrocompatibilité

Un scénario courant consiste à utiliser un MediaController dans l'UI de l'application pour contrôler la lecture et afficher la playlist. En même temps, la session est exposée aux clients externes tels que les commandes multimédias Android et l'Assistant sur mobile ou TV, Wear OS pour les montres et Android Auto dans les voitures. L'application de démonstration de session Media3 est un exemple d'application qui implémente un tel scénario.

Ces clients externes peuvent utiliser des API telles que MediaControllerCompat de l'ancienne bibliothèque AndroidX ou android.media.session.MediaController de la plate-forme Android. Media3 est entièrement rétrocompatible avec l'ancienne bibliothèque et offre une interopérabilité avec l'API de la plate-forme Android.

Utiliser le contrôleur de notifications multimédias

Il est important de comprendre que ces anciens contrôleurs et contrôleurs de plate-forme partagent le même état et que la visibilité ne peut pas être personnalisée par contrôleur (par exemple, les PlaybackState.getActions() et PlaybackState.getCustomActions() disponibles). Vous pouvez utiliser le contrôleur de notification multimédia pour configurer l'état défini dans la session multimédia de la plate-forme afin d'assurer la compatibilité avec ces anciens contrôleurs et contrôleurs de plate-forme.

Par exemple, une application peut fournir une implémentation de MediaSession.Callback.onConnect() pour définir les commandes disponibles et les préférences des boutons multimédias spécifiquement pour la session de plate-forme comme suit :

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

Autoriser Android Auto à envoyer des commandes personnalisées

Lorsque vous utilisez un MediaLibraryService et que vous souhaitez prendre en charge Android Auto avec l'application mobile, le contrôleur Android Auto nécessite des commandes disponibles appropriées. Autrement, Media3 refuserait les commandes personnalisées entrantes de ce contrôleur :

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

L'application de démonstration de session comporte un module automobile qui démontre la compatibilité avec Automotive OS, qui nécessite un APK distinct.