Hintergrundwiedergabe mit MediaSessionService

Häufig ist es sinnvoll, Medien abzuspielen, während sich eine App nicht im Vordergrund befindet. Ein Musikplayer spielt beispielsweise die Musik in der Regel weiter ab, wenn der Nutzer sein Gerät gesperrt hat oder eine andere App verwendet. Die Media3-Bibliothek bietet verschiedene Oberflächen, mit denen die Hintergrundwiedergabe unterstützt werden kann.

MediaSessionService verwenden

Zur Aktivierung der Hintergrundwiedergabe solltest du Player und MediaSession in einen separaten Service aufnehmen. So kann das Gerät weiterhin Medien bereitstellen, auch wenn deine App nicht im Vordergrund ausgeführt wird.

Durch den MediaSessionService kann die Mediensitzung getrennt von der App-Aktivität ausgeführt werden.
Abbildung 1: MediaSessionService ermöglicht es der Mediensitzung, getrennt von der Anwendungsaktivität auszuführen.

Wenn Sie einen Player in einem Dienst hosten, sollten Sie einen MediaSessionService verwenden. Erstellen Sie dazu eine Klasse, die MediaSessionService erweitert, und erstellen Sie darin Ihre Mediensitzung.

Mit MediaSessionService können externe Clients wie Google Assistant, die Mediensteuerung des Systems 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 zugreifen zu müssen. Tatsächlich können mehrere Clientanwendungen gleichzeitig mit demselben MediaSessionService verbunden sein, wobei jede Anwendung eine eigene MediaController hat.

Dienstlebenszyklus implementieren

Sie müssen drei Lebenszyklusmethoden Ihres Dienstes implementieren:

  • onCreate() wird aufgerufen, wenn der erste Controller eine Verbindung herstellt und der Dienst instanziiert und gestartet wird. Dies ist der beste Ort, um Player und MediaSession zu erstellen.
  • onTaskRemoved(Intent) wird aufgerufen, wenn der Nutzer die App aus den letzten Aufgaben schließt. Wenn die Wiedergabe läuft, kann die App den Dienst auch im Vordergrund ausführen. Wenn der Player pausiert ist, befindet sich der Dienst nicht im Vordergrund und muss beendet werden.
  • onDestroy() wird aufgerufen, wenn der Dienst angehalten 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 Ihre 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 zum Ausführen eines Diensts im Vordergrund. Fügen Sie dem Manifest die Berechtigung FOREGROUND_SERVICE hinzu. Wenn Sie auf API 34 und höher abzielen, 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 im Manifest die Klasse Service mit dem Intent-Filter MediaSessionService deklarieren.

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

Wenn deine App auf einem Gerät mit Android 10 (API-Level 29) oder höher ausgeführt wird, musst du eine foregroundServiceType definieren, die mediaPlayback enthält.

Wiedergabe mit einem MediaController steuern

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

UI-Befehle verarbeiten

MediaSession empfängt Befehle vom Controller über seinen MediaSession.Callback. Durch das Initialisieren 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 eine MediaNotification, die in den meisten Fällen funktionieren sollte. Die veröffentlichte Benachrichtigung ist standardmäßig eine MediaStyle-Benachrichtigung, die mit den neuesten Informationen aus Ihrer Mediensitzung aktualisiert wird und die Wiedergabesteuerung enthält. Das MediaNotification erkennt Ihre Sitzung und kann verwendet werden, um die Wiedergabe für alle anderen Apps zu steuern, die mit derselben Sitzung verbunden sind.

Eine Musikstreaming-App mit MediaSessionService würde beispielsweise ein MediaNotification erstellen, das den Titel, den Künstler und das Albumcover des aktuellen Medienelements anzeigt, das zusammen mit der Wiedergabesteuerung basierend auf Ihrer MediaSession-Konfiguration wiedergegeben wird.

Die erforderlichen Metadaten können im Medium 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 Media-Steuerelemente anpassen. Weitere Informationen zum Anpassen der Android Media-Steuerelemente

Benachrichtigungsanpassung

Wenn Sie die Benachrichtigung 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 MediaSessionService hinzu.

Wiedergabe wird fortgesetzt

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

Media3-Medienschaltflächen-Empfänger deklarieren

Media3 enthält eine API, mit der Nutzer die Wiedergabe nach dem Beenden einer App und sogar nach dem Neustart des Geräts fortsetzen können. Standardmäßig ist die Fortsetzung der Wiedergabe deaktiviert. Das bedeutet, dass der Nutzer die Wiedergabe nicht fortsetzen kann, wenn Ihr Dienst nicht ausgeführt wird. Zur Aktivierung musst du zuerst MediaButtonReceiver in deinem Manifest deklarieren:

<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 Wiederaufnahme der Wiedergabe implementieren

Wenn die Fortsetzung der Wiedergabe entweder von einem Bluetooth-Gerät oder von der Wiederaufnahmefunktion der Android System-UI angefordert wird, wird die Callback-Methode 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() am besten, um den Player mit diesen Parametern zu konfigurieren, bevor Media3 den Player vorbereitet und die Wiedergabe startet, wenn der Callback abgeschlossen ist.

Erweiterte Controller-Konfiguration und Abwärtskompatibilität

In einem häufig verwendeten Szenario wird ein MediaController in der App-UI verwendet, 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 Uhren und Android Auto in Autos präsentiert. Die Media3-Sitzungsdemo-App ist ein Beispiel für eine App, in der ein solches Szenario implementiert wird.

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

Es ist wichtig zu verstehen, 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 ermitteln, kann eine App den Media Notification Controller verwenden und die verfügbaren Befehle und das benutzerdefinierte Layout festlegen. Der Dienst verbindet den Media Notification-Controller mit Ihrer Sitzung. Die Sitzung verwendet das vom onConnect() des Callbacks zurückgegebene ConnectionResult, um Aktionen und benutzerdefinierte Aktionen der Framework-Sitzung zu konfigurieren.

In einem rein mobilen Szenario 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:

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 ein MediaLibraryService verwendet wird und Android Auto mit der mobilen App unterstützt wird, benötigt der Android Auto-Controller entsprechende verfügbare Befehle. Andernfalls würde Media3 eingehende benutzerdefinierte Befehle von diesem Controller ablehnen:

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 Sitzungsdemo-App hat ein Automotive-Modul, das die Unterstützung für Automotive OS veranschaulicht, für die ein separates APK erforderlich ist.