Hintergrundwiedergabe mit MediaSessionService

Oft ist es wünschenswert, Medien abzuspielen, während eine App nicht im Vordergrund ist. Ein Musikplayer spielt beispielsweise in der Regel weiter Musik ab, wenn der Nutzer sein Gerät gesperrt hat oder eine andere App verwendet. Die Media3-Bibliothek bietet eine Reihe von Schnittstellen, mit denen Sie die Hintergrundwiedergabe unterstützen können.

MediaSessionService verwenden

Wenn Sie die Hintergrundwiedergabe aktivieren möchten, sollten Sie Player und MediaSession in einem separaten Dienst unterbringen. So kann das Gerät weiterhin Medien bereitstellen, auch wenn sich Ihre App nicht im Vordergrund befindet.

Mit dem MediaSessionService kann die Mediensitzung unabhängig von der Aktivität der App ausgeführt werden.
Abbildung 1: Mit der MediaSessionService kann die Mediensitzung unabhängig von der Aktivität der App ausgeführt werden.

Wenn du einen Player in einem Dienst hostest, solltest du einen MediaSessionService verwenden. Erstellen Sie dazu eine Klasse, die MediaSessionService erweitert, und erstellen Sie darin Ihre Media Session.

Mit MediaSessionService können externe Clients wie Google Assistant, die Mediensteuerung des Systems, Medientasten auf Peripheriegeräten oder Companion-Geräte wie Wear OS deinen Dienst finden, sich mit ihm verbinden und die Wiedergabe steuern, ohne auf die UI-Aktivität deiner App zuzugreifen. Es können sogar mehrere Client-Apps gleichzeitig mit demselben MediaSessionService verbunden sein, wobei jede App einen eigenen MediaController hat.

Lebenszyklus des Dienstes implementieren

Sie müssen zwei Lebenszyklusmethoden Ihres Dienstes implementieren:

  • onCreate() wird aufgerufen, wenn der erste Controller eine Verbindung herstellt und der Dienst instanziiert und gestartet wird. Es ist der beste Ort, um Player und MediaSession zu erstellen.
  • onDestroy() wird aufgerufen, wenn der Dienst beendet wird. Alle Ressourcen, einschließlich Player und Sitzung, müssen freigegeben werden.

Optional können Sie onTaskRemoved(Intent) überschreiben, um anzupassen, was passiert, wenn der Nutzer die App aus den letzten Aufgaben entfernt. Standardmäßig wird der Dienst weiter ausgeführt, wenn die Wiedergabe läuft, und andernfalls beendet.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession 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()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

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

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Anstatt die Wiedergabe im Hintergrund fortzusetzen, kannst du den Dienst auch beenden, wenn der Nutzer die App schließt:

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

Bei jeder anderen manuellen Implementierung von onTaskRemoved kannst du mit isPlaybackOngoing() prüfen, ob die Wiedergabe als laufend gilt und der Dienst im Vordergrund gestartet wurde.

Zugriff auf die Mediensitzung gewähren

Überschreibe die Methode onGetSession(), um anderen Clients Zugriff auf deine Mediensitzung zu gewähren, die beim Erstellen des Dienstes erstellt wurde.

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Dienst im Manifest deklarieren

Eine App benötigt die Berechtigungen FOREGROUND_SERVICE und FOREGROUND_SERVICE_MEDIA_PLAYBACK, um einen Dienst im Vordergrund für die Wiedergabe auszuführen:

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

Außerdem musst du deine Service-Klasse im Manifest mit einem Intent-Filter von MediaSessionService und einer foregroundServiceType deklarieren, die mediaPlayback enthält.

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

Wiedergabe über MediaController steuern

In der Aktivität oder dem Fragment, das die Player-UI enthält, kannst du mit einem MediaController eine Verknüpfung zwischen der UI und deiner Mediensitzung herstellen. Ihre Benutzeroberfläche verwendet den Media Controller, um Befehle von Ihrer Benutzeroberfläche an den Player innerhalb der Sitzung zu senden. Weitere Informationen zum Erstellen und Verwenden eines MediaController finden Sie im Leitfaden MediaController erstellen.

MediaController-Befehle verarbeiten

MediaSession empfängt Befehle vom Controller über MediaSession.Callback. Durch die Initialisierung eines MediaSession wird eine Standardimplementierung von MediaSession.Callback erstellt, die automatisch alle Befehle verarbeitet, die ein MediaController an deinen Player sendet.

Benachrichtigung

Ein MediaSessionService erstellt automatisch ein MediaNotification für Sie, das in den meisten Fällen funktionieren sollte. Standardmäßig ist die veröffentlichte Benachrichtigung eine MediaStyle-Benachrichtigung, die mit den neuesten Informationen aus Ihrer Mediensitzung aktualisiert wird und die Wiedergabesteuerung anzeigt. Der MediaNotification kennt deine Sitzung und kann verwendet werden, um die Wiedergabe für alle anderen Apps zu steuern, die mit derselben Sitzung verbunden sind.

Eine Musik-Streaming-App, die eine MediaSessionService verwendet, würde beispielsweise eine MediaNotification erstellen, die den Titel, den Künstler und das Albumcover für die aktuelle Mediendatei zusammen mit der Wiedergabesteuerung basierend auf deiner MediaSession-Konfiguration anzeigt.

Die erforderlichen Metadaten können in den Medien bereitgestellt oder wie im folgenden Snippet als Teil der Mediendatei deklariert werden:

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

Lebenszyklus von Benachrichtigungen

Die Benachrichtigung wird erstellt, sobald die Player MediaItem Instanzen in ihrer Playlist hat.

Alle Benachrichtigungsaktualisierungen erfolgen automatisch basierend auf dem Status von Player und MediaSession.

Die Benachrichtigung kann nicht entfernt werden, während der Dienst im Vordergrund ausgeführt wird. Wenn du die Benachrichtigung sofort entfernen möchtest, musst du Player.release() aufrufen oder die Playlist mit Player.clearMediaItems() löschen.

Wenn der Player länger als 10 Minuten ohne weitere Nutzerinteraktionen pausiert, gestoppt oder fehlerhaft ist, wird der Dienst automatisch aus dem Status „Dienst im Vordergrund“ entfernt, damit er vom System beendet werden kann. Du kannst die Wiedergabefortsetzung implementieren, damit ein Nutzer den Dienstlebenszyklus neu starten und die Wiedergabe zu einem späteren Zeitpunkt fortsetzen kann.

Anpassung von Benachrichtigungen

Die Metadaten zum aktuell wiedergegebenen Element können durch Ändern von MediaItem.MediaMetadata angepasst werden. Wenn du die Metadaten eines vorhandenen Elements aktualisieren möchtest, kannst du Player.replaceMediaItem verwenden, um die Metadaten zu aktualisieren, ohne die Wiedergabe zu unterbrechen.

Du kannst auch einige der in der Benachrichtigung angezeigten Schaltflächen anpassen, indem du benutzerdefinierte Einstellungen für die Medientasten der Android-Mediensteuerung festlegst. Weitere Informationen zum Anpassen der Android-Mediensteuerung

Wenn Sie die Benachrichtigung selbst weiter anpassen möchten, erstellen Sie einen MediaNotification.Provider mit DefaultMediaNotificationProvider.Builder oder erstellen Sie eine benutzerdefinierte Implementierung der Anbieterschnittstelle. Fügen Sie Ihren Anbieter mit setMediaNotificationProvider zu Ihrem MediaSessionService hinzu.

Wiedergabe fortsetzen

Nachdem der MediaSessionService beendet wurde und auch nach einem Neustart des Geräts ist es möglich, die Wiedergabe fortzusetzen. Nutzer können den Dienst dann neu starten und die Wiedergabe an der Stelle fortsetzen, an der sie aufgehört haben. Standardmäßig ist die Fortsetzung der Wiedergabe deaktiviert. Das bedeutet, dass der Nutzer die Wiedergabe nicht fortsetzen kann, wenn dein Dienst nicht ausgeführt wird. Wenn du diese Funktion aktivieren möchtest, musst du einen MediaButtonReceiver deklarieren und die Methode onPlaybackResumption implementieren.

Media3-Medientastenempfänger deklarieren

Deklariere zuerst die MediaButtonReceiver in deinem Manifest:

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

Callback für die Fortsetzung der Wiedergabe implementieren

Wenn die Fortsetzung der Wiedergabe entweder von einem Bluetooth-Gerät oder der Fortsetzungsfunktion der Android-System-UI angefordert wird, wird die Callback-Methode onPlaybackResumption() aufgerufen.

Kotlin

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  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.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  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;
}

Wenn Sie andere Parameter wie Wiedergabegeschwindigkeit, Wiederholungsmodus oder Zufallsmodus gespeichert haben, ist onPlaybackResumption() ein guter Ort, um den Player mit diesen Parametern zu konfigurieren, bevor Media3 den Player vorbereitet und die Wiedergabe startet, wenn der Callback abgeschlossen ist.

Diese Methode wird während des Bootvorgangs aufgerufen, um die Benachrichtigung zur Wiederaufnahme der Android-System-UI nach einem Neustart des Geräts zu erstellen, wobei isForPlayback auf false gesetzt ist. Bei einer Rich-Benachrichtigung wird empfohlen, MediaMetadata-Felder wie title und artworkData oder artworkUri des aktuellen Elements mit lokal verfügbaren Werten zu füllen, da der Netzwerkzugriff möglicherweise noch nicht verfügbar ist. Sie können auch MediaConstants.EXTRAS_KEY_COMPLETION_STATUS und MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE zum MediaMetadata.extras hinzufügen, um die Wiedergabeposition für die Fortsetzung anzugeben.

Erweiterte Controller-Konfiguration und Abwärtskompatibilität

Ein gängiges Szenario ist die Verwendung eines MediaController in der App-UI, um die Wiedergabe zu steuern und die Playlist anzuzeigen. Gleichzeitig wird die Sitzung externen Clients wie der Android-Mediensteuerung und Assistant auf Mobilgeräten oder Fernsehern, Wear OS für Smartwatches und Android Auto in Autos zugänglich gemacht. Die Session-Demo-App von Media3 ist ein Beispiel für eine App, die ein solches Szenario implementiert.

Diese externen Clients können APIs wie MediaControllerCompat der Legacy-AndroidX-Bibliothek oder android.media.session.MediaController der Android-Plattform verwenden. Media3 ist vollständig abwärtskompatibel mit der Legacy-Bibliothek und bietet Interoperabilität mit der Android-Plattform-API.

Vertrauenswürdige Controller identifizieren

Jede App kann versuchen, eine Verbindung zu Ihrer Mediensitzung oder -bibliothek herzustellen. Wenn du den Zugriff auf System-Controller, Controller mit Berechtigung zur Steuerung von Medieninhalten und deine eigene App einschränken möchtest, kannst du ControllerInfo.isTrusted() für eine einfache Zugriffsprüfung verwenden. Alternativ können Sie spezifischere Controller wie den Controller für Medienbenachrichtigungen oder Android Auto-Controller identifizieren, wie in den folgenden Abschnitten beschrieben.

Medienbenachrichtigungs-Controller verwenden

Diese Legacy- und Plattform-Controller haben denselben Status und die Sichtbarkeit kann nicht pro Controller angepasst werden (z. B. die verfügbaren PlaybackState.getActions() und PlaybackState.getCustomActions()). Mit dem Controller für Medienbenachrichtigungen kannst du den Status konfigurieren, der in der Mediensitzung der Plattform festgelegt ist, um die Kompatibilität mit diesen Legacy- und Plattform-Controllern zu gewährleisten.

Eine App kann beispielsweise eine Implementierung von MediaSession.Callback.onConnect() bereitstellen, um verfügbare Befehle und Einstellungen für Medientasten speziell für die Plattformsitzung festzulegen:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

Android Auto zum Senden benutzerdefinierter Befehle autorisieren

Wenn du einen MediaLibraryService verwendest und Android Auto mit der mobilen App unterstützen möchtest, benötigt der Android Auto-Controller entsprechende verfügbare Befehle. Andernfalls würde Media3 eingehende benutzerdefinierte Befehle von diesem Controller ablehnen:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

Die Demo-App für die Sitzung hat ein Automotive-Modul, das die Unterstützung für Automotive OS demonstriert, für das ein separates APK erforderlich ist.