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 Schnittstelle Player implementiert. Wenn die Mediensitzung mit dem Player verbunden ist, kann eine App die Medienwiedergabe extern ankündigen und Wiedergabebefehle von externen Quellen empfangen.

Befehle können von physischen Tasten wie der Wiedergabetaste auf einem Headset oder der Fernbedienung eines Fernsehers stammen. Sie können auch von Client-Apps mit einem Mediacontroller stammen, z. B. wenn Sie Google Assistant bitten, die Wiedergabe zu pausieren. Die Mediensitzung delegiert diese Befehle an den Player der Medien-App.

Wann sollte ich eine Mediensitzung auswählen?

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

  • Über die Kopfhörer Häufig gibt es Tasten oder Touch-Interaktionen, mit denen Nutzer Medien abspielen oder pausieren oder zum nächsten oder vorherigen Titel springen können.
  • Sie können mit Google Assistant sprechen. Ein gängiges Muster ist „Hey Google, pausiere“, um alle Medien zu pausieren, die gerade auf dem Gerät wiedergegeben werden.
  • Über ihre Wear OS-Smartwatch So können Nutzer beim Abspielen auf ihrem Smartphone einfacher auf die gängigsten Wiedergabesteuerungen zugreifen.
  • Über die Mediensteuerung In diesem Karussell werden die Steuerelemente für jede laufende Mediensitzung angezeigt.
  • Unter TV Ermöglicht Aktionen mit physischen Wiedergabeschaltflächen, Plattformwiedergabesteuerung und Energieverwaltung. Wenn sich beispielsweise der Fernseher, die Soundbar oder der A/V-Receiver ausschaltet oder der Eingang gewechselt wird, sollte die Wiedergabe in der App beendet werden.
  • und alle anderen externen Prozesse, die die Wiedergabe beeinflussen müssen.

Das ist für viele Anwendungsfälle ideal. Insbesondere sollten Sie MediaSession in folgenden Fällen verwenden:

  • Sie streamen Videoinhalte im Langformat, z. B. Filme oder Live-TV.
  • Sie streamen Audioinhalte im Langformat, z. B. Podcasts oder Musikplaylists.
  • Sie entwickeln eine TV-App.

Nicht alle Anwendungsfälle passen jedoch gut zur MediaSession. In den folgenden Fällen sollten Sie nur die Player verwenden:

  • Sie präsentieren kurze Inhalte, bei denen das Nutzer-Engagement und die Interaktion entscheidend sind.
  • Es wird kein einzelnes aktives Video wiedergegeben, z. B. wenn der Nutzer durch eine Liste scrollt und mehrere Videos gleichzeitig auf dem Bildschirm angezeigt werden.
  • Sie spielen ein einmaliges Einführungs- oder Erklärvideo ab, das sich Nutzer aktiv ansehen sollen.
  • Deine Inhalte sind datenschutzrelevant und du möchtest nicht, dass externe Prozesse auf die Medienmetadaten zugreifen (z. B. im Inkognitomodus in einem Browser).

Wenn Ihr Anwendungsfall nicht zu den oben aufgeführten Anwendungsfällen passt, überlegen Sie, ob Sie damit einverstanden sind, dass die Wiedergabe in Ihrer App fortgesetzt wird, wenn der Nutzer nicht aktiv mit den Inhalten interagiert. Wenn die Antwort ja lautet, sollten Sie wahrscheinlich MediaSession auswählen. Ist das nicht der Fall, sollten Sie stattdessen Player verwenden.

Mediensitzung erstellen

Eine Mediensitzung existiert parallel zum Player, den sie verwaltet. Du kannst eine Mediensitzung mit einem Context- und einem Player-Objekt erstellen. Du solltest eine Mediensitzung bei Bedarf erstellen und initialisieren, z. B. mit der Lebenszyklusmethode onStart() oder onResume() der Activity oder Fragment oder der onCreate()-Methode der Service, zu der die Mediensitzung und der zugehörige Player gehören.

Wenn du eine Mediensitzung erstellen möchtest, initialisiere eine Player und gib sie so an MediaSession.Builder weiter:

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 Statusverwaltung

Die Media3-Bibliothek aktualisiert die Mediensitzung automatisch anhand des Status des Players. Du musst die Zuordnung von Spieler zu Sitzung also nicht manuell vornehmen.

Das ist ein Unterschied zum bisherigen Ansatz, bei dem du eine PlaybackStateCompat unabhängig vom Player erstellen und verwalten musstest, 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 App nur eine Sitzungs-Instanz erstellen soll, was der häufigste Fall ist.

Wenn eine App 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 IllegalStateException Ihre App mit der Fehlermeldung IllegalStateException: Session ID must be unique. ID= zum Absturz bringt, wurde wahrscheinlich eine Sitzung unerwartet 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 es wird eine Ausnahme ausgelöst.

Andere Kunden verwalten lassen

Die Mediensitzung ist der Schlüssel zur Steuerung der Wiedergabe. Sie können damit Befehle von externen Quellen an den Player weiterleiten, der Ihre Medien abspielt. Das können physische Tasten wie die Wiedergabetaste auf einem Headset oder einer Fernsehfernbedienung oder indirekte Befehle wie „Pause“ an Google Assistant sein. Ebenso können Sie Zugriff auf das Android-System gewähren, um die Benachrichtigungs- und Sperrbildschirmsteuerung zu vereinfachen, oder auf eine Wear OS-Smartwatch, damit Sie die Wiedergabe über das Zifferblatt steuern können. Externe Clients können über einen Mediacontroller Wiedergabebefehle an Ihre Medien-App senden. Diese werden von Ihrer Mediensitzung empfangen, die die Befehle an den Mediaplayer weiterleitet.

Ein Diagramm, das die Interaktion zwischen einer MediaSession und einer MediaController veranschaulicht.
Abbildung 1: Der Mediencontroller ermöglicht die Weitergabe von Befehlen von externen Quellen an die Mediensitzung.

Wenn ein Controller eine Verbindung zu deiner Mediensitzung herstellen möchte, wird die Methode onConnect() aufgerufen. Mithilfe des bereitgestellten ControllerInfo können Sie entscheiden, ob Sie die Anfrage annehmen oder ablehnen. Ein Beispiel für die Annahme einer Verbindungsanfrage findest du im Abschnitt Verfügbare Befehle angeben.

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

Mit anderen Rückrufmethoden kannst du beispielsweise Anfragen für benutzerdefinierte Wiedergabebefehle und Änderungen an der Playlist verarbeiten. Diese Callbacks enthalten ebenfalls ein ControllerInfo-Objekt, damit Sie die Antwort auf jede Anfrage pro Controller ändern können.

Playlist ändern

Eine Mediensitzung kann die Playlist des Players direkt ändern, wie im ExoPlayer-Leitfaden für Playlists beschrieben. Mit einem Controller kann die Playlist auch geändert werden, wenn entweder COMMAND_SET_MEDIA_ITEM oder COMMAND_CHANGE_MEDIA_ITEMS für den Controller verfügbar ist.

Wenn du der Playlist neue Elemente hinzufügst, benötigt der Player in der Regel MediaItem-Instanzen mit einem definierten URI, damit sie abgespielt werden können. Neue Elemente werden standardmäßig automatisch an Playermethoden wie player.addMediaItem weitergeleitet, wenn für sie 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 du Controller unterstützen möchtest, die Medien ohne definierten URI anfordern. Stattdessen sind in der MediaItem normalerweise mindestens eines der folgenden Felder festgelegt, um die angeforderten Medien zu beschreiben:

  • MediaItem.id: Eine generische ID, die die Medien identifiziert.
  • MediaItem.RequestMetadata.mediaUri: Ein Anfrage-URI, der ein benutzerdefiniertes Schema verwenden kann und nicht unbedingt direkt vom Player abgespielt werden kann.
  • MediaItem.RequestMetadata.searchQuery: Eine textbasierte Suchanfrage, z. B. von Google Assistant.
  • MediaItem.MediaMetadata: Strukturierte Metadaten wie „Titel“ oder „Künstler“.

Wenn du noch mehr Anpassungsoptionen für ganz neue Playlists nutzen möchtest, kannst du zusätzlich onSetMediaItems() überschreiben. Damit kannst du das Startelement und die Position in der Playlist festlegen. Du kannst beispielsweise ein einzelnes angefordertes Element zu einer ganzen Playlist erweitern und den Player anweisen, am Index des ursprünglich angeforderten Elements zu beginnen. Eine Beispielimplementierung von onSetMediaItems() mit dieser Funktion finden Sie in der Demo-App für die Sitzung.

Benutzerdefiniertes Layout und benutzerdefinierte Befehle verwalten

In den folgenden Abschnitten wird beschrieben, wie Sie Client-Apps ein benutzerdefiniertes Layout von Schaltflächen für benutzerdefinierte Befehle anzeigen und Controller zum Senden der benutzerdefinierten Befehle autorisieren.

Benutzerdefiniertes Layout der Sitzung definieren

Wenn du Client-Apps angeben möchtest, welche Wiedergabesteuerungen dem Nutzer angezeigt werden sollen, musst du das benutzerdefinierte Layout der Sitzung festlegen, wenn du die MediaSession in der onCreate()-Methode deines Dienstes erstellst.

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 Nutzer ein Medienelement in einer Liste der Favoriten speichern können. Die MediaController sendet benutzerdefinierte Befehle und die MediaSession.Callback empfängt sie.

Du kannst festlegen, welche benutzerdefinierten Sitzungsbefehle für eine MediaController verfügbar sind, wenn sie eine Verbindung zu deiner Mediensitzung herstellt. Dazu müssen Sie MediaSession.Callback.onConnect() überschreiben. Konfiguriere und gib die verfügbaren Befehle zurück, wenn du in der onConnect-Callback-Methode eine Verbindungsanfrage von einem MediaController akzeptierst:

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 du benutzerdefinierte Befehlsanfragen von einer MediaController erhalten möchtest, überschreibe die Methode onCustomCommand() in der 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)
      );
    }
    ...
  }
}

Du kannst mithilfe der packageName-Property des MediaSession.ControllerInfo-Objekts, das an Callback-Methoden übergeben wird, nachverfolgen, welcher Mediacontroller eine Anfrage stellt. So können Sie das Verhalten Ihrer App auf einen bestimmten Befehl anpassen, wenn er vom System, von Ihrer eigenen App oder von anderen Client-Apps stammt.

Benutzerdefiniertes Layout nach einer Nutzerinteraktion aktualisieren

Nachdem du einen benutzerdefinierten Befehl oder eine andere Interaktion mit dem Player verarbeitet hast, kannst du das Layout aktualisieren, das in der Benutzeroberfläche des Controllers angezeigt wird. Ein typisches Beispiel ist eine Ein-/Aus-Schaltfläche, deren Symbol sich ä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 in der Player-Benutzeroberfläche definierten Befehls anpassen möchten, z. B. play() oder seekToNext(), setzen Sie Player in ForwardingSimpleBasePlayer ein, bevor Sie ihn an MediaSession übergeben.

Kotlin

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

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

Java

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

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

Weitere Informationen zu ForwardingSimpleBasePlayer findest du im ExoPlayer-Leitfaden zur Anpassung.

Anfordernden Controller eines Player-Befehls identifizieren

Wenn ein Aufruf einer Player-Methode von einer MediaController ausgeht, kannst du die Quelle mit MediaSession.controllerForCurrentRequest identifizieren und die ControllerInfo für die aktuelle Anfrage abrufen:

Kotlin

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Java

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

Auf Medienschaltflächen reagieren

Medienschaltflächen sind Hardwareschaltflächen auf Android-Geräten und anderen Peripheriegeräten, z. B. die Wiedergabe-/Pause-Taste auf einem Bluetooth-Headset. Media3 verarbeitet Medienschalter-Ereignisse für dich, wenn sie in der Sitzung eintreffen, und ruft die entsprechende Player-Methode auf dem Sitzungsplayer auf.

Eine App kann das Standardverhalten überschreiben, indem sie MediaSession.Callback.onMediaButtonEvent(Intent) überschreibt. In einem solchen Fall kann/muss die App alle API-spezifischen Anforderungen selbst verarbeiten.

Fehlerbehandlung und -berichte

Es gibt zwei Arten von Fehlern, die von einer Sitzung ausgegeben und an Controller gemeldet werden. Schwerwiegende Fehler geben einen technischen Wiedergabefehler des Sitzungsplayers an, der die Wiedergabe unterbricht. Schwerwiegende Fehler werden dem Controller automatisch gemeldet, wenn sie auftreten. Nicht schwerwiegende Fehler sind nicht technische Fehler oder Richtlinienverstöße, die die Wiedergabe nicht unterbrechen und von der Anwendung manuell an Controller gesendet werden.

Schwerwiegende Wiedergabefehler

Ein schwerwiegender Wiedergabefehler wird vom Player an die Sitzung und dann an die Controller gemeldet, die Player.Listener.onPlayerError(PlaybackException) und Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException) aufrufen.

In diesem Fall wird der Wiedergabestatus in STATE_IDLE geändert und MediaController.getPlaybackError() gibt die PlaybackException zurück, die den Übergang verursacht hat. Ein Controller kann die PlayerException.errorCode prüfen, um Informationen zum Grund des Fehlers zu erhalten.

Für die Interoperabilität wird ein schwerwiegender Fehler an den PlaybackStateCompat der Plattformsitzung repliziert, indem sein Status in STATE_ERROR geändert und der Fehlercode und die Meldung gemäß der PlaybackException festgelegt werden.

Anpassung eines schwerwiegenden Fehlers

Um dem Nutzer lokalisierte und aussagekräftige Informationen zur Verfügung zu stellen, können der Fehlercode, die Fehlermeldung und die Fehler-Extras eines schwerwiegenden Wiedergabefehlers angepasst werden. Dazu wird beim Erstellen der Sitzung ein ForwardingPlayer verwendet:

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Java

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

Der weiterleitende Player registriert eine Player.Listener beim tatsächlichen Player und unterbricht Callbacks, die einen Fehler melden. Eine benutzerdefinierte PlaybackException wird dann an die Listener delegiert, die beim weiterleitenden Player registriert sind. Damit das funktioniert, überschreibt der weiterleitende Player Player.addListener und Player.removeListener, um Zugriff auf die Listener zu erhalten, über die ein benutzerdefinierter Fehlercode, eine Nachricht oder zusätzliche Informationen gesendet werden können:

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Nicht kritische Fehler

Nicht kritische Fehler, die nicht auf eine technische Ausnahme zurückzuführen sind, können von einer App an alle oder an einen bestimmten Controller gesendet werden:

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Java

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Ein nicht fataler Fehler, der an den Media Notification Controller gesendet wird, wird in die PlaybackStateCompat der Plattformsitzung repliziert. Dabei werden nur der Fehlercode und die Fehlermeldung entsprechend auf PlaybackStateCompat festgelegt, während PlaybackStateCompat.state nicht in STATE_ERROR geändert wird.

Nicht schwerwiegende Fehler erhalten

Ein MediaController erhält einen nicht schwerwiegenden Fehler, wenn MediaController.Listener.onError implementiert wird:

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });