Wiedergabe mit einer MediaSession steuern und bewerben

Mediensitzungen bieten eine universelle Art der Interaktion mit einem Audio- oder Videoplayer. In Media3 ist der Standardplayer die ExoPlayer-Klasse, die die Player-Schnittstelle implementiert. Durch die Verbindung der Mediensitzung mit dem Player kann eine App die Medienwiedergabe extern bewerben und Wiedergabebefehle von externen Quellen empfangen.

Die Befehle können von physischen Tasten wie der Wiedergabetaste auf einem Headset oder einer TV-Fernbedienung stammen. Sie können auch von Client-Apps mit einem Mediencontroller stammen, z. B. mit Anweisungen zum „Pause“ an Google Assistant. Die Mediensitzung delegiert diese Befehle an den Player der Medien-App.

Wann sollte eine Mediensitzung ausgewählt werden?

Wenn du MediaSession implementierst, können Nutzer die Wiedergabe steuern:

  • Über die Kopfhörer. Häufig gibt es Schaltflächen oder Touchbedienung, die Nutzer auf ihren Kopfhörern ausführen können, um Medien abzuspielen 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 die Wiedergabe von Medien anzuhalten.
  • über die Wear OS-Smartwatch Dies ermöglicht einen einfacheren Zugriff auf die gebräuchlichsten Wiedergabesteuerungen während der Wiedergabe auf dem Smartphone.
  • Über die Mediensteuerung. Dieses Karussell zeigt Steuerelemente für jede laufende Mediensitzung.
  • Auf dem Fernseher. Ermöglicht Aktionen mit physischen Wiedergabeschaltflächen, Steuerung der Plattformwiedergabe und Energiesparmodus, z. B. wenn der Fernseher, die Soundbar oder der AV-Receiver ausgeschaltet oder die Eingangsquelle gewechselt wird, wird die Wiedergabe in der App beendet.
  • sowie alle anderen externen Prozesse, die die Wiedergabe beeinflussen müssen.

Das ist für viele Anwendungsfälle gut geeignet. Besonders in folgenden Fällen wird die Verwendung von MediaSession empfohlen:

  • Sie streamen Videos im Langformat wie Filme oder Live-TV.
  • Sie streamen langformatige Audioinhalte wie Podcasts oder Musikplaylists.
  • Sie erstellen eine TV-App.

Allerdings passen nicht alle Anwendungsfälle gut zum MediaSession. In den folgenden Fällen kann es sinnvoll sein, nur Player zu verwenden:

  • Sie präsentieren Kurzinhalte, bei denen Nutzerinteraktion und ‐interaktion entscheidend sind.
  • Es gibt kein einzelnes aktives Video, z. B. wenn der Nutzer durch eine Liste scrollt, und mehrere Videos werden gleichzeitig auf dem Bildschirm angezeigt.
  • Sie spielen ein einmaliges Einführungs- oder Erklärungsvideo ab, das sich Ihre Nutzer aktiv ansehen werden.
  • Ihre Inhalte sind datenschutzfreundlich und Sie möchten nicht, dass externe Prozesse auf die Medienmetadaten zugreifen (z. B. im Inkognitomodus in einem Browser).

Wenn dein Anwendungsfall keinem der oben genannten Punkte entspricht, solltest du prüfen, ob du damit einverstanden bist, dass die App fortgesetzt wird, wenn der Nutzer nicht aktiv mit den Inhalten interagiert. Wenn die Antwort „Ja“ lautet, sollten Sie MediaSession auswählen. Wenn dies nicht der Fall ist, sollten Sie stattdessen Player verwenden.

Mediensitzung erstellen

Eine Mediensitzung befindet sich neben dem von ihr verwalteten Player. Sie können eine Mediensitzung mit einem Context- und einem Player-Objekt erstellen. Sie sollten eine Mediensitzung erstellen und initialisieren, wenn sie benötigt wird. Das kann z. B. die Lebenszyklusmethode onStart() oder onResume() von Activity oder Fragment oder die Methode onCreate() des Service sein, zu dem die Mediensitzung und der zugehörige Player gehören.

Initialisieren Sie zum Erstellen einer Mediensitzung ein Player und geben Sie es wie folgt 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 Statusbehandlung

Die Media3-Mediathek 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 Abkehr vom Legacy-Ansatz, bei dem Sie einen PlaybackStateCompat unabhängig vom Player selbst erstellen und verwalten müssen, 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 möchte, was der häufigste Fall ist.

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

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

Anderen Clients 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 für die Wiedergabe Ihrer Medien zuständig ist. Dies können physische Tasten wie die Wiedergabetaste auf einem Headset oder die Fernbedienung eines Fernsehers oder indirekte Befehle wie die Anweisung „Pause“ an Google Assistant sein. Vielleicht möchten Sie auch Zugriff auf das Android-System gewähren, um die Steuerung von Benachrichtigungen und den Sperrbildschirm zu ermöglichen, oder auf eine Wear OS-Smartwatch, um die Wiedergabe über das Zifferblatt zu steuern. Externe Clients können einen Mediencontroller verwenden, um Wiedergabebefehle an Ihre Medien-App zu senden. Diese werden von Ihrer Mediensitzung empfangen, die schließlich Befehle an den Medienplayer delegiert.

Diagramm zur Veranschaulichung der Interaktion zwischen MediaSession und MediaController
Abbildung 1: Der Mediencontroller ermöglicht die Übergabe von Befehlen von externen Quellen an die Mediensitzung.

Wenn ein Controller eine Verbindung zu Ihrer Mediensitzung herstellt, wird die Methode onConnect() aufgerufen. Mit dem angegebenen ControllerInfo können Sie entscheiden, ob Sie die Anfrage annehmen oder ablehnen möchten. Ein Beispiel für das Akzeptieren 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 Spieler. Die auf der Player-Oberfläche definierten Wiedergabe- und Playlist-Befehle werden von der Sitzung automatisch ausgeführt.

Mit anderen Callback-Methoden können Sie beispielsweise Anfragen für benutzerdefinierte Wiedergabebefehle verarbeiten oder die Playlist ändern. Diese Callbacks enthalten ebenfalls ein ControllerInfo-Objekt, sodass Sie ändern können, wie Sie auf jede Anfrage für einzelne Controller reagieren.

Playlist ändern

Bei einer Mediensitzung kann die Playlist ihres Players direkt geändert werden. Dies wird im ExoPlayer-Leitfaden für Playlists erläutert. Controller können die Playlist auch ändern, wenn COMMAND_SET_MEDIA_ITEM oder COMMAND_CHANGE_MEDIA_ITEMS für den Controller verfügbar ist.

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

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

  • MediaItem.id: Eine generische ID zur Identifizierung des Mediums.
  • MediaItem.RequestMetadata.mediaUri: Ein Anfrage-URI, der möglicherweise ein benutzerdefiniertes Schema verwendet und nicht unbedingt direkt vom Player wiedergegeben werden kann.
  • MediaItem.RequestMetadata.searchQuery: Eine Suchanfrage in Textform, z. B. von Google Assistant.
  • MediaItem.MediaMetadata: Strukturierte Metadaten wie „title“ oder „artist“.

Für weitere Anpassungsoptionen für komplett neue Playlists kannst du zusätzlich onSetMediaItems() überschreiben, um das Startelement und die Position in der Playlist zu definieren. Du kannst beispielsweise ein einzelnes angefordertes Element auf eine ganze Playlist erweitern und den Player 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 von benutzerdefinierten Befehlsschaltflächen anbieten und Controller zum Senden der benutzerdefinierten Befehle autorisieren.

Benutzerdefiniertes Layout der Sitzung definieren

Um Client-Apps anzugeben, welche Wiedergabesteuerungen dem Nutzer angezeigt werden sollen, legen Sie das benutzerdefinierte Layout der Sitzung fest, wenn Sie die 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 angeben

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

Sie können definieren, 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 einem 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)
      );
    }
    ...
  }
}

Mit dem Attribut packageName des Objekts MediaSession.ControllerInfo, das an die Methoden Callback übergeben wird, können Sie verfolgen, welcher Mediencontroller eine Anfrage sendet. So können Sie das Verhalten Ihrer Anwendung als Reaktion auf einen bestimmten Befehl anpassen, wenn dieser vom 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 Layout in der Controller-Benutzeroberfläche aktualisieren. Ein typisches Beispiel ist eine Ein/Aus-Schaltfläche, die ihr Symbol ändert, nachdem die mit dieser Schaltfläche verknüpfte 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 Befehls anpassen möchten, der in der Player-Schnittstelle definiert ist, z. B. play() oder seekToNext(), verpacken Sie Ihren Player in eine ForwardingPlayer.

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 zur Anpassung.

Den anfordernden Controller eines Spielerbefehls identifizieren

Wenn ein Aufruf einer Player-Methode von einer MediaController stammt, können Sie die Ursprungsquelle 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 in einem Bluetooth-Headset. Media3 verarbeitet die Medienschaltflächenereignisse für Sie, wenn sie bei der Sitzung ankommen, und ruft die entsprechende Player-Methode im Sitzungsplayer auf.

Eine Anwendung kann das Standardverhalten durch Überschreiben von MediaSession.Callback.onMediaButtonEvent(Intent) überschreiben. In einem solchen Fall kann/muss die App alle API-Details selbst verarbeiten.