Lecture en arrière-plan avec MediaSessionService

Il est souvent souhaitable de lire des contenus multimédias 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 MediaSessionService

Pour activer la lecture en arrière-plan, vous devez contenir Player et MediaSession dans un service distinct. Cela permet à l'appareil de continuer à diffuser des contenus multimédias 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 de celle-ci.

L'utilisation de MediaSessionService permet aux clients externes tels que l'Assistant Google, les commandes multimédias du système 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'interface utilisateur de votre application. En fait, plusieurs applications clientes peuvent être connectées au même MediaSessionService en même temps, chaque application ayant son propre MediaController.

Implémenter le cycle de vie du service

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

  • onCreate() est appelé lorsque le premier contrôleur est sur le point de se connecter et que le service est instancié et démarré. C'est le meilleur endroit pour créer Player et MediaSession.
  • onTaskRemoved(Intent) est appelé lorsque l'utilisateur ferme l'application des tâches récentes. Si la lecture est en cours, l'application peut choisir de laisser le service s'exécuter au premier plan. Si le lecteur est mis en pause, le service n'est pas au premier plan et doit être arrêté.
  • onDestroy() est appelé lorsque le service est en cours d'arrêt. Toutes les ressources, y compris le lecteur et la session, doivent être libérées.

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

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

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

Donner accès à la session multimédia

Ignorez la méthode onGetSession() pour permettre à d'autres clients d'accéder à la 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 l'autorisation d'exécuter un service de premier plan. Ajoutez l'autorisation FOREGROUND_SERVICE au fichier manifeste et, si vous ciblez l'API 34 ou une version ultérieure, FOREGROUND_SERVICE_MEDIA_PLAYBACK:

<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 MediaSessionService.

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
    </intent-filter>
</service>

Vous devez définir un foregroundServiceType qui inclut mediaPlayback lorsque votre application s'exécute sur un appareil équipé d'Android 10 (niveau d'API 29) ou version ultérieure.

Contrôler la lecture avec un MediaController

Dans l'activité ou le fragment contenant l'interface utilisateur de votre lecteur, vous pouvez établir un lien entre l'interface utilisateur et votre session multimédia à l'aide d'un MediaController. Votre interface utilisateur utilise le contrôleur multimédia pour envoyer des commandes depuis l'interface utilisateur au lecteur de la session. Pour en savoir plus sur la création et l'utilisation d'un MediaController, consultez le guide Créer une MediaController.

Gérer les commandes d'interface utilisateur

MediaSession reçoit les commandes de la manette 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 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. Le MediaNotification connaît votre session et peut être utilisé pour contrôler la lecture de toute autre application connectée à la même session.

Par exemple, une application de streaming musical utilisant un MediaSessionService crée un élément MediaNotification qui affiche le titre, l'artiste et la pochette de l'album pour l'élément multimédia en cours de lecture, ainsi que les commandes de lecture en fonction de 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();

Les applications peuvent personnaliser les boutons de commande des commandes multimédias Android. En savoir plus sur la personnalisation des commandes multimédias Android

Personnalisation des notifications

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

Reprise de la lecture

Les boutons multimédias sont des boutons physiques que l'on trouve sur les appareils Android et d'autres périphériques, tels que le bouton lecture ou pause d'un casque Bluetooth. Media3 gère les entrées des boutons multimédias à votre place lorsque le service est en cours d'exécution.

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

Media3 inclut une API permettant aux utilisateurs de reprendre la lecture après l'arrêt d'une application et même après le redémarrage de l'appareil. 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 option, 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 un rappel de reprise de lecture

Lorsque la reprise de la lecture est demandée par un appareil Bluetooth ou la fonctionnalité de reprise de l'interface utilisateur 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 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;
}

Si vous avez stocké d'autres paramètres tels que la vitesse de lecture, le mode de répétition ou le mode de brassage, onPlaybackResumption() est idéal pour configurer le lecteur avec ces paramètres avant que Media3 ne le prépare et ne démarre la lecture une fois le rappel terminé.

Configuration avancée de la manette et rétrocompatibilité

Un scénario courant consiste à utiliser un MediaController dans l'interface utilisateur de l'application pour contrôler la lecture et afficher la playlist. En même temps, la session est exposée à des clients externes tels que les commandes multimédias Android et l'Assistant sur mobile ou téléviseur, Wear OS pour les montres et Android Auto dans les voitures. L'application de démonstration de session Media3 est un exemple d'application mettant en œuvre 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 du framework Android. Media3 est entièrement rétrocompatible avec l'ancienne bibliothèque et offre une interopérabilité avec l'API du framework Android.

Utiliser le contrôleur de notifications multimédias

Il est important de comprendre que ces anciens contrôleurs ou contrôleurs de framework lisent les mêmes valeurs que dans les champs PlaybackState.getActions() et PlaybackState.getCustomActions() du framework. Pour déterminer les actions et les actions personnalisées de la session de framework, une application peut utiliser le contrôleur des notifications multimédias, et définir ses commandes disponibles et sa mise en page personnalisée. Le service connecte le contrôleur de notifications multimédias à votre session, qui utilise le ConnectionResult renvoyé par le onConnect() de votre rappel pour configurer les actions et les actions personnalisées de la session de framework.

Dans un scénario exclusivement mobile, une application peut fournir une implémentation de MediaSession.Callback.onConnect() pour définir les commandes disponibles et une mise en page personnalisée spécifiquement pour la session de framework, 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 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();
}

Autoriser Android Auto à envoyer des commandes personnalisées

Lorsque vous utilisez un MediaLibraryService et pour assurer la compatibilité avec Android Auto avec l'application mobile, la manette Android Auto nécessite les commandes disponibles appropriées, sinon Media3 refuserait les commandes personnalisées entrantes de cette manette:

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

L'application de démonstration de session comporte un module automobile qui démontre la prise en charge d'Automotive OS qui nécessite un APK distinct.