Wiedergabe mit einer MediaSession steuern und bewerben

Mediensitzungen bieten eine universelle Möglichkeit, mit einem Audio- oder Videoplayer zu interagieren. In Media3 ist der Standardplayer die Klasse ExoPlayer, die die Player-Schnittstelle implementiert. Durch das Verbinden der Mediensitzung mit dem Player kann eine App die Medienwiedergabe extern bewerben und Wiedergabebefehle von externen Quellen empfangen.

Befehle können über physische Tasten wie die Wiedergabetaste auf einem Headset oder die Fernbedienung des Fernsehers ausgeführt werden. Sie können auch von Client-Apps mit einem Mediencontroller stammen, z. B. mit Anweisungen zum Pausieren an Google Assistant. In der Mediensitzung werden diese Befehle an den Player der Medien-App delegiert.

Wann sollte eine Mediensitzung ausgewählt werden?

Wenn du MediaSession implementierst, erlaubst du Nutzern, die Wiedergabe zu steuern:

  • Über ihre Kopfhörer Häufig gibt es Schaltflächen oder Touch-Interaktionen, die Nutzer über ihre Kopfhörer ausführen können, um Medien wiederzugeben oder zu pausieren oder zum nächsten oder vorherigen Titel zu wechseln.
  • Durch Sprechen mit Google Assistant Ein gängiges Muster ist "Ok Google, Pause", um Medien anzuhalten, die gerade auf dem Gerät abgespielt werden.
  • Über die Wear OS-Smartwatch. So können Sie einfacher auf die gängigsten Wiedergabesteuerungen zugreifen, während Sie auf dem Smartphone spielen.
  • Über die Mediensteuerung Dieses Karussell zeigt Steuerelemente für jede laufende Mediensitzung.
  • Auf Fernsehern. Ermöglicht Aktionen mit physischen Wiedergabeschaltflächen, der Wiedergabesteuerung über die Plattform und der Energieverwaltung. Wenn beispielsweise der Fernseher, die Soundbar oder der AV-Receiver ausgeschaltet oder der Eingang gewechselt wird, sollte die Wiedergabe in der App beendet werden.
  • Und alle anderen externen Prozesse, die die Wiedergabe beeinflussen müssen.

Dies eignet sich für viele Anwendungsfälle. Insbesondere in folgenden Fällen solltest du MediaSession verwenden:

  • Sie streamen Videos im Langformat, z. B. Filme oder Live-TV.
  • Sie streamen längere Audioinhalte wie Podcasts oder Musikplaylists.
  • Sie erstellen eine TV-App.

Allerdings eignen sich nicht alle Anwendungsfälle für MediaSession. In den folgenden Fällen kann es sinnvoll sein, nur Player zu verwenden:

  • Du präsentierst Kurzinhalte, bei denen die Nutzerinteraktion und -interaktion entscheidend sind.
  • Es gibt nicht nur ein aktives Video, z. B. wenn der Nutzer durch eine Liste scrollt und mehrere Videos gleichzeitig auf dem Bildschirm angezeigt werden.
  • Du spielst ein einmaliges Einführungs- oder Erklärungsvideo ab, das sich dein Nutzer wahrscheinlich aktiv ansieht.
  • Bei Ihren Inhalten muss Datenschutz beachtet werden und Sie möchten nicht, dass externe Prozesse auf die Medienmetadaten zugreifen (z. B. der Inkognitomodus in einem Browser).

Wenn dein Anwendungsfall zu keinem der oben genannten Punkte passt, solltest du überlegen, ob du damit einverstanden bist, dass deine App die Wiedergabe fortsetzt, wenn der Nutzer nicht aktiv mit den Inhalten interagiert. Wenn die Antwort „Ja“ lautet, sollten Sie MediaSession auswählen. Wenn die Antwort „Nein“ lautet, sollten Sie stattdessen Player verwenden.

Mediensitzung erstellen

Neben dem von ihm verwalteten Player befindet sich eine Mediensitzung. Sie können eine Mediensitzung mit einem Context- und einem Player-Objekt erstellen. Sie sollten eine Mediensitzung bei Bedarf erstellen und initialisieren, z. B. die Lebenszyklusmethode onStart() oder onResume() der Activity oder Fragment oder die onCreate()-Methode der Service, der die Mediensitzung und der zugehörige Player gehören.

Initialisieren Sie zum Erstellen einer Mediensitzung ein Player und geben Sie es so an MediaSession.Builder an:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Automatische Statusverarbeitung

Die Media3-Bibliothek aktualisiert die Mediensitzung automatisch anhand des Status des Players. Daher müssen Sie die Zuordnung von Spieler zu Sitzung nicht manuell vornehmen.

Dies ist eine Abweichung vom Legacy-Ansatz, bei dem ein PlaybackStateCompat unabhängig vom Player selbst erstellt und verwaltet werden muss, um beispielsweise Fehler anzuzeigen.

Eindeutige Sitzungs-ID

Standardmäßig erstellt MediaSession.Builder eine Sitzung mit einem leeren String als Sitzungs-ID. Das ist ausreichend, wenn eine Anwendung nur eine einzige Sitzungsinstanz erstellen soll, was in der Regel der Fall ist.

Wenn eine Anwendung mehrere Sitzungsinstanzen gleichzeitig verwalten möchte, muss sie dafür sorgen, dass die Sitzungs-ID jeder Sitzung eindeutig ist. Die Sitzungs-ID kann beim Erstellen der Sitzung mit MediaSession.Builder.setId(String id) festgelegt werden.

Wenn ein IllegalStateException Ihre Anwendung abstürzt und die Fehlermeldung IllegalStateException: Session ID must be unique. ID= anzeigt, wurde wahrscheinlich unerwartet eine Sitzung erstellt, bevor eine zuvor erstellte Instanz mit derselben ID freigegeben wurde. Um zu verhindern, dass Sitzungen durch einen Programmierfehler gehackt werden, werden solche Fälle erkannt und durch das Auslösen einer Ausnahme benachrichtigt.

Anderen Kunden Kontrolle gewähren

Die Mediensitzung ist der Schlüssel zur Steuerung der Wiedergabe. Damit können Sie Befehle von externen Quellen an den Player weiterleiten, der die Wiedergabe Ihrer Medien übernimmt. Bei diesen Quellen kann es sich um physische Tasten wie die Wiedergabetaste auf einem Headset oder die Fernbedienung des Fernsehers oder indirekte Befehle handeln, z. B. um Anweisungen zum Pausieren von Google Assistant. Ebenso kannst du Zugriff auf das Android-System gewähren, um die Steuerung von Benachrichtigungen und Sperrbildschirmen zu erleichtern, oder auf eine Wear OS-Smartwatch, damit du die Wiedergabe über das Zifferblatt steuern kannst. Externe Clients können einen Mediencontroller verwenden, um Wiedergabebefehle an Ihre Medien-App auszugeben. Diese werden von Ihrer Mediensitzung empfangen, die schließlich Befehle an den Mediaplayer delegiert.

Ein Diagramm, das die Interaktion zwischen MediaSession und MediaController veranschaulicht.
Abbildung 1: Der Media Controller ermöglicht die Übergabe von Befehlen aus externen Quellen an die Mediensitzung.

Wenn ein Controller eine Verbindung zu Ihrer Mediensitzung herstellen möchte, wird die Methode onConnect() aufgerufen. Anhand des bereitgestellten ControllerInfo kannst du entscheiden, ob du die Anfrage akzeptieren oder ablehnen möchtest. Ein Beispiel für die Annahme einer Verbindungsanfrage finden Sie im Abschnitt Verfügbare Befehle deklarieren.

Nach dem Verbinden kann ein Controller Wiedergabebefehle an die Sitzung senden. Die Sitzung delegiert diese Befehle dann an den Player. Wiedergabe- und Wiedergabelistenbefehle, die in der Player-Oberfläche definiert sind, werden von der Sitzung automatisch verarbeitet.

Mit anderen Callback-Methoden kannst du beispielsweise Anfragen für benutzerdefinierte Wiedergabebefehle oder die Änderung der Playlist verarbeiten. Diese Callbacks enthalten in ähnlicher Weise ein ControllerInfo-Objekt, sodass du für jeden Controller modifizieren kannst, wie du auf die einzelnen Anfragen antwortest.

Playlist ändern

Eine Mediasitzung kann die Playlist ihres Players direkt ändern, wie im ExoPlayer-Leitfaden für Playlists erläutert. Controller können die Playlist auch ändern, wenn entweder COMMAND_SET_MEDIA_ITEM oder COMMAND_CHANGE_MEDIA_ITEMS für den Controller verfügbar ist.

Wenn der Playlist neue Elemente hinzugefügt werden, benötigt der Player normalerweise MediaItem-Instanzen mit einem definierten URI, damit sie abgespielt werden können. Standardmäßig werden neu hinzugefügte Elemente automatisch an Spielermethoden wie player.addMediaItem weitergeleitet, wenn für sie ein URI definiert ist.

Wenn du die dem Player hinzugefügten MediaItem-Instanzen anpassen möchtest, kannst du onAddMediaItems() überschreiben. Dieser Schritt ist erforderlich, wenn Sie Controller unterstützen möchten, die Medien ohne definierten URI anfordern. Stattdessen ist für MediaItem normalerweise eines oder mehrere der folgenden Felder festgelegt, um das angeforderte Medium zu beschreiben:

  • MediaItem.id: Eine generische ID zur Identifizierung des Mediums.
  • MediaItem.RequestMetadata.mediaUri: Ein Anfrage-URI, der ein benutzerdefiniertes Schema verwenden kann und vom Spieler nicht unbedingt direkt wiedergegeben werden kann.
  • MediaItem.RequestMetadata.searchQuery: Eine Suchanfrage in Textform, zum Beispiel von Google Assistant.
  • MediaItem.MediaMetadata: Strukturierte Metadaten wie „Titel“ oder „Interpret“.

Für weitere Anpassungsoptionen für völlig neue Playlists kannst du zusätzlich onSetMediaItems() überschreiben, mit dem du den Startelement und die Position in der Playlist definieren kannst. Sie können beispielsweise ein einzelnes angefordertes Element auf eine ganze Playlist erweitern und den Spieler anweisen, beim Index des ursprünglich angeforderten Elements zu beginnen. Eine Beispielimplementierung von onSetMediaItems() mit dieser Funktion finden Sie in der Sitzungsdemo-App.

Benutzerdefiniertes Layout und benutzerdefinierte Befehle verwalten

In den folgenden Abschnitten wird beschrieben, wie Sie Clientanwendungen ein benutzerdefiniertes Layout benutzerdefinierter Befehlsschaltflächen anbieten und Controller autorisieren, die benutzerdefinierten Befehle zu senden.

Benutzerdefiniertes Layout der Sitzung definieren

Um Client-Apps anzugeben, welche Wiedergabesteuerungen für den Nutzer eingeblendet werden sollen, legen Sie das benutzerdefinierte Layout der Sitzung fest, wenn Sie MediaSession in der Methode onCreate() Ihres Dienstes erstellen.

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

Verfügbare Player- und benutzerdefinierte Befehle deklarieren

Medienanwendungen können benutzerdefinierte Befehle definieren, die beispielsweise in einem benutzerdefinierten Layout verwendet werden können. Beispielsweise können Sie Schaltflächen implementieren, mit denen Nutzer ein Medienelement in einer Liste von Lieblingselementen speichern können. MediaController sendet benutzerdefinierte Befehle und MediaSession.Callback empfängt sie.

Sie können festlegen, welche benutzerdefinierten Sitzungsbefehle für ein MediaController verfügbar sind, wenn es eine Verbindung zu Ihrer Mediensitzung herstellt. Dazu überschreiben Sie MediaSession.Callback.onConnect(). Konfigurieren Sie die verfügbaren Befehle und geben Sie sie zurück, wenn Sie eine Verbindungsanfrage von einer MediaController in der Callback-Methode onConnect akzeptieren:

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Wenn Sie benutzerdefinierte Befehlsanfragen von einem MediaController erhalten möchten, überschreiben Sie die Methode onCustomCommand() in Callback.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Mithilfe der packageName-Eigenschaft des MediaSession.ControllerInfo-Objekts, das an Callback-Methoden übergeben wird, können Sie verfolgen, welcher Mediacontroller eine Anfrage stellt. Auf diese Weise können Sie das Verhalten Ihrer Anwendung als Reaktion auf einen bestimmten Befehl anpassen, wenn dieser aus dem System, Ihrer eigenen Anwendung oder anderen Clientanwendungen stammt.

Benutzerdefiniertes Layout nach einer Nutzerinteraktion aktualisieren

Nachdem du einen benutzerdefinierten Befehl oder eine andere Interaktion mit deinem Player ausgeführt hast, kannst du das auf der Controller-Benutzeroberfläche angezeigte Layout aktualisieren. Ein typisches Beispiel ist eine Ein/Aus-Schaltfläche, deren Symbol sich ändert, nachdem die mit dieser Schaltfläche verbundene Aktion ausgelöst wurde. Mit MediaSession.setCustomLayout können Sie das Layout aktualisieren:

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

Verhalten von Wiedergabebefehlen anpassen

Wenn Sie das Verhalten eines in der Player-Schnittstelle definierten Befehls anpassen möchten, z. B. play() oder seekToNext(), müssen Sie Player mit einem ForwardingPlayer zusammenfassen.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession = 
  new MediaSession.Builder(context, forwardingPlayer).build();

Weitere Informationen zu ForwardingPlayer findest du im ExoPlayer-Leitfaden unter Anpassung.

Anfordernden Controller eines Spielerbefehls identifizieren

Wenn ein Aufruf einer Player-Methode von einem MediaController ausgeht, können Sie die Herkunftsquelle mit MediaSession.controllerForCurrentRequest identifizieren und die ControllerInfo für die aktuelle Anfrage abrufen:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

Auf Medienschaltflächen reagieren

Medientasten sind Hardwaretasten auf Android-Geräten und anderen Peripheriegeräten, z. B. die Wiedergabe-/Pause-Taste an einem Bluetooth-Headset. Media3 verarbeitet Medienschaltflächenereignisse für Sie, wenn sie in der Sitzung ankommen, und ruft die entsprechende Player-Methode im Sitzungsplayer auf.

Eine App kann das Standardverhalten durch Überschreiben von MediaSession.Callback.onMediaButtonEvent(Intent) überschreiben. In diesem Fall kann/muss die Anwendung alle API-Spezifikationen selbstständig verarbeiten.