Hintergrundwiedergabe mit MediaSessionService

Oft ist es wünschenswert, Medien abzuspielen, während eine App nicht im Vordergrund ist. Beispielsweise wird bei einem Musikplayer in der Regel Musik abgespielt, 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 du die Hintergrundwiedergabe aktivieren möchtest, solltest du Player und MediaSession in einem separaten Dienst einfügen. So kann das Gerät auch dann Medien bereitstellen, wenn Ihre App nicht im Vordergrund ist.

Mit dem MediaSessionService kann die Mediensitzung unabhängig von der Aktivität der App ausgeführt werden.
Abbildung 1: MediaSessionService ermöglicht, dass die Mediensitzung getrennt von der App-Aktivität ausgeführt wird

Wenn du einen Player in einem Dienst hostest, solltest du einen MediaSessionService verwenden. Erstelle dazu eine Klasse, die MediaSessionService erweitert, und erstelle darin deine Mediensitzung.

Mit MediaSessionService können externe Clients wie Google Assistant, Systemmediensteuerungen oder Companion-Geräte wie Wear OS deinen Dienst finden, eine Verbindung dazu herstellen und die Wiedergabe steuern, ohne auf die UI-Aktivitäten deiner App zuzugreifen. Tatsächlich können mehrere Client-Apps gleichzeitig mit demselben MediaSessionService verbunden sein, wobei jede App einen eigenen MediaController hat.

Dienstlebenszyklus implementieren

Sie müssen drei Lebenszyklusmethoden für Ihren Dienst implementieren:

  • onCreate() wird aufgerufen, wenn der erste Controller eine Verbindung herstellen soll und der Dienst instanziiert und gestartet wird. Das ist der beste Ort, um Player und MediaSession zu entwickeln.
  • onTaskRemoved(Intent) wird aufgerufen, wenn der Nutzer die App aus den letzten Aufgaben schließt. Wenn die Wiedergabe läuft, kann die App festlegen, dass der Dienst im Vordergrund ausgeführt wird. Wenn der Player pausiert ist, ist der Dienst nicht im Vordergrund und muss beendet werden.
  • onDestroy() wird aufgerufen, wenn der Dienst beendet wird. Alle Ressourcen, einschließlich Spieler und Sitzung, müssen freigegeben werden.

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

Anstatt die Wiedergabe im Hintergrund fortzusetzen, kann eine App den Dienst in jedem Fall beenden, wenn der Nutzer die App schließt:

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

Zugriff auf die Mediensitzung gewähren

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

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

Dienst im Manifest deklarieren

Eine App benötigt eine Berechtigung, um einen Dienst im Vordergrund auszuführen. Fügen Sie dem Manifest die Berechtigung FOREGROUND_SERVICE hinzu. Wenn Sie die API 34 oder höher verwenden, fügen Sie auch FOREGROUND_SERVICE_MEDIA_PLAYBACK hinzu:

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

Außerdem müssen Sie Ihre Service-Klasse im Manifest mit einem Intent-Filter von MediaSessionService deklarieren.

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

Sie müssen eine foregroundServiceType definieren, die mediaPlayback enthält, wenn Ihre App auf einem Gerät mit Android 10 (API-Level 29) oder höher ausgeführt wird.

Wiedergabe mit MediaController steuern

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

UI-Befehle verarbeiten

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

Benachrichtigung

Mit einer MediaSessionService wird automatisch eine MediaNotification für Sie erstellt, die 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. Das MediaNotification ist sich deiner Sitzung bewusst und kann verwendet werden, um die Wiedergabe aller anderen Apps zu steuern, die mit derselben Sitzung verbunden sind.

In einer Musik-Streaming-App mit einer MediaSessionService wird beispielsweise ein MediaNotification erstellt, in dem der Titel, der Künstler und das Albumcover des aktuell wiedergegebenen Medienelements sowie die Wiedergabesteuerung basierend auf der MediaSession-Konfiguration angezeigt werden.

Die erforderlichen Metadaten können in den Medien bereitgestellt oder als Teil des Medienelements wie im folgenden Snippet 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();

Apps können die Befehlsschaltflächen der Android-Mediensteuerung anpassen. Weitere Informationen zum Anpassen der Mediensteuerung unter Android

Benachrichtigungsanpassung

Wenn du die Benachrichtigung anpassen möchtest, erstelle eine MediaNotification.Provider mit DefaultMediaNotificationProvider.Builder oder erstelle eine benutzerdefinierte Implementierung der Anbieterschnittstelle. Fügen Sie Ihren Anbieter mit setMediaNotificationProvider zu Ihrem MediaSessionService hinzu.

Wiedergabe fortsetzen

Medientasten sind Hardwaretasten auf Android-Geräten und anderen Peripheriegeräten, z. B. die Wiedergabe- oder Pausetaste auf einem Bluetooth-Headset. Media3 verarbeitet die Eingaben der Medienschaltflächen für Sie, wenn der Dienst ausgeführt wird.

Empfänger für Medienschaltflächen vom Typ „Media3“ deklarieren

Media3 enthält eine API, mit der Nutzer die Wiedergabe fortsetzen können, nachdem eine App beendet wurde und sogar nachdem das Gerät neu gestartet wurde. Die Wiederaufnahme der Wiedergabe ist standardmäßig deaktiviert. Das bedeutet, dass der Nutzer die Wiedergabe nicht fortsetzen kann, wenn dein Dienst nicht ausgeführt wird. Wenn Sie die Funktion aktivieren möchten, deklarieren Sie zuerst die MediaButtonReceiver in Ihrem 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 Wiedergabefortsetzung implementieren

Wenn die Wiedergabe entweder von einem Bluetooth-Gerät oder über die Wiedergabefunktion der Android-System-UI fortgesetzt werden soll, wird die Rückrufmethode onPlaybackResumption() aufgerufen.

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

Wenn du andere Parameter wie Wiedergabegeschwindigkeit, Wiederholungsmodus oder Zufallsmix gespeichert hast, eignet sich onPlaybackResumption() gut, um den Player mit diesen Parametern zu konfigurieren, bevor Media3 den Player vorbereitet und die Wiedergabe nach Abschluss des Callbacks startet.

Erweiterte Controllerkonfiguration und Abwärtskompatibilität

Ein gängiges Szenario ist die Verwendung einer MediaController in der App-Benutzeroberfläche zur Steuerung der Wiedergabe und zum Anzeigen der Playlist. Gleichzeitig ist die Sitzung für externe Clients wie die Android-Mediensteuerung und Assistant auf Mobilgeräten oder Fernsehern, Wear OS für Smartwatches und Android Auto in Autos verfügbar. Die Media3-Demoanwendung für Sitzungen ist ein Beispiel für eine App, die ein solches Szenario implementiert.

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

Controller für Medienbenachrichtigungen verwenden

Wichtig ist, dass diese Legacy- oder Framework-Controller dieselben Werte aus dem Framework PlaybackState.getActions() und PlaybackState.getCustomActions() lesen. Um Aktionen und benutzerdefinierte Aktionen der Framework-Sitzung zu bestimmen, kann eine App den Media Notification Controller verwenden und die verfügbaren Befehle und das benutzerdefinierte Layout festlegen. Der Dienst verbindet den Media-Benachrichtigungscontroller mit deiner Sitzung. Die Sitzung verwendet den von onConnect() deines Callbacks zurückgegebenen Wert ConnectionResult, um Aktionen und benutzerdefinierte Aktionen der Framework-Sitzung zu konfigurieren.

Bei einem reinen Mobilgeräteszenario kann eine App eine Implementierung von MediaSession.Callback.onConnect() bereitstellen, um verfügbare Befehle und ein benutzerdefiniertes Layout speziell für die Framework-Sitzung festzulegen. Dazu ist Folgendes erforderlich:

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

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 lehnt Media3 eingehende benutzerdefinierte Befehle von diesem Controller ab:

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

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