Sterowanie odtwarzaniem i reklamowanie go za pomocą sesji MediaSession

Sesje multimedialne to uniwersalny sposób na interakcję z odtwarzaczem audio lub wideo. W Media3 domyślnym odtwarzaczem jest klasa ExoPlayer, która implementuje interfejs Player. Połączenie sesji multimediów z odtwarzaczem umożliwia aplikacji wyświetlanie reklam odtwarzania multimediów na zewnątrz i odbieranie poleceń odtwarzania z zewnętrznych źródeł.

Polecenia mogą pochodzić z przycisków fizycznych, takich jak przycisk odtwarzania na słuchawkach lub pilocie do telewizora. Mogą one też pochodzić z aplikacji klienckich, które mają kontroler multimediów, np. polecenie „wstrzymaj” dla Asystenta Google. Sesja multimediów deleguje te polecenia do odtwarzacza aplikacji multimedialnej.

Kiedy wybrać sesję multimediów

Gdy wdrożysz MediaSession, umożliwisz użytkownikom sterowanie odtwarzaniem:

  • za pomocą słuchawek. Często są to przyciski lub interakcje dotykowe, które użytkownik może wykonać na słuchawkach, aby włączyć lub wstrzymać odtwarzanie multimediów albo przejść do następnego lub poprzedniego utworu.
  • Rozmawiając z Asystentem Google. Typowym poleceniem jest „OK Google, wstrzymaj”, aby wstrzymać odtwarzanie multimediów na urządzeniu.
  • za pomocą zegarka z Wear OS, Dzięki temu użytkownicy będą mogli łatwiej korzystać z najczęstszych elementów sterowania odtwarzaniem podczas oglądania na telefonie.
  • za pomocą elementów sterujących multimediami. Ta karuzela pokazuje elementy sterujące każdej uruchomionej sesji multimedialnej.
  • Na telewizorze. Umożliwia wykonywanie działań przy użyciu fizycznych przycisków odtwarzania, sterowania odtwarzaniem na platformie i zarządzania energią (na przykład jeśli telewizor, soundbar lub odbiornik A/V wyłączy się lub zmieni wejście, odtwarzanie w aplikacji powinno się zatrzymać).
  • oraz inne procesy zewnętrzne, które muszą wpływać na odtwarzanie.

Jest to przydatne w wielu przypadkach. Szczególnie zalecamy używanie MediaSession w tych przypadkach:

  • Odtwarzasz długie filmy, takie jak filmy czy telewizja na żywo.
  • Odtwarzasz długie treści audio, takie jak podcasty lub playlisty muzyczne.
  • Tworzysz aplikację na telewizor.

Nie wszystkie przypadki użycia jednak pasują do MediaSession. W tych sytuacjach możesz użyć tylko Player:

  • Wyświetlasz krótkie treści, w przypadku których zaangażowanie i interakcje użytkowników są kluczowe.
  • Nie ma żadnego aktywnego filmu, tak jakby użytkownik przewijał listę i na ekranie wyświetlało się kilka filmów jednocześnie.
  • odtwarzasz jednorazowy film wprowadzający lub wyjaśniający, który użytkownik powinien aktywnie obejrzeć;
  • Twoje treści są poufne i nie chcesz, aby procesy zewnętrzne miały dostęp do metadanych multimediów (np. tryb incognito w przeglądarce).

Jeśli Twój przypadek użycia nie pasuje do żadnego z wymienionych powyżej, zastanów się, czy zgadzasz się na kontynuowanie odtwarzania przez aplikację, gdy użytkownik nie jest aktywnie zaangażowany w treści. Jeśli odpowiedź brzmi „tak”, prawdopodobnie wybierzesz MediaSession. Jeśli odpowiedź brzmi „nie”, prawdopodobnie lepiej będzie użyć funkcji Player.

Tworzenie sesji multimedialnej

Sesja multimedialna jest związana z odtwarzaczem, którym zarządza. Sesję multimedialną możesz utworzyć za pomocą obiektu Context i Player. Sesję multimediów należy utworzyć i zainicjować w razie potrzeby, np. stosując metodę cyklu życia onStart() lub onResume() funkcji Activity lub Fragment albo metodę onCreate() zasobu Service, do którego należy sesja multimediów i powiązany z nią odtwarzacz.

Aby utworzyć sesję multimediów, zainicjuj Player i prześlij ją do MediaSession.Builder w ten sposób:

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

Automatyczne zarządzanie stanem

Biblioteka Media3 automatycznie aktualizuje sesję multimediów na podstawie stanu odtwarzacza. W związku z tym nie musisz ręcznie obsługiwać mapowania z odtwarzacza na sesję.

To przerwa od starszego podejścia, w ramach którego trzeba było utworzyć i utrzymywać element PlaybackStateCompat niezależnie od samego odtwarzacza, na przykład w celu wykrycia błędów.

Unikalny identyfikator sesji

Domyślnie funkcja MediaSession.Builder tworzy sesję z pustym ciągiem znaków jako identyfikatorem sesji. To wystarczy, jeśli aplikacja zamierza utworzyć tylko jedną instancję sesji, co jest najczęściej spotykanym przypadkiem.

Jeśli aplikacja chce zarządzać kilkoma instancjami sesji jednocześnie, musi zadbać o to, aby identyfikator każdej sesji był unikalny. Identyfikator sesji można ustawić podczas tworzenia sesji za pomocą funkcji MediaSession.Builder.setId(String id).

Jeśli zobaczysz, że IllegalStateException powoduje awarię aplikacji i wyświetla się komunikat o błędzie IllegalStateException: Session ID must be unique. ID=, prawdopodobnie sesja została nieoczekiwanie utworzona przed udostępnieniem wcześniej utworzonej instancji o tym samym identyfikatorze. Aby uniknąć wycieku sesji przez błąd programowania, takie przypadki są wykrywane i powiadamiane jako wyjątek.

Przyznawanie kontroli innym klientom

Sesja multimediów jest kluczowa dla sterowania odtwarzaniem. Pozwala kierować polecenia ze źródeł zewnętrznych do odtwarzacza, w którym odtwarzasz multimedia. Źródłami mogą być przyciski fizyczne, takie jak przycisk odtwarzania na zestawie słuchawkowym lub pilocie telewizora, lub polecenia pośrednie, takie jak polecenie „zatrzymaj” skierowane do Asystenta Google. Możesz też przyznać dostęp do systemu Android, aby ułatwić obsługę powiadomień i ekranu blokady, lub do zegarka z Wear OS, aby móc sterować odtwarzaniem z tarczy zegarka. Klienci zewnętrzni mogą używać kontrolera multimediów, aby wydawać polecenia odtwarzania do Twojej aplikacji multimedialnej. Są one odbierane przez sesję multimediów, która ostatecznie deleguje polecenia do odtwarzacza multimediów.

Schemat przedstawiający interakcję między obiektem MediaSession i MediaController.
Ilustracja 1. Sterownik multimediów ułatwia przekazywanie poleceń z zewnętrznych źródeł do sesji multimediów.
.

Gdy kontroler ma zamiar połączyć się z sesją multimediów, wywoływana jest metoda onConnect(). Możesz użyć dostarczonego ControllerInfo, aby zdecydować, czy zaakceptować, czy odrzucić prośbę. Przykład akceptowania żądania połączenia znajdziesz w sekcji Zadeklaruj dostępne polecenia.

Po połączeniu kontroler może wysyłać do sesji polecenia odtwarzania. Następnie sesja przekazuje te polecenia do odtwarzacza. Polecenia odtwarzania i playlist zdefiniowane w interfejsie Player są automatycznie obsługiwane przez sesję.

Inne metody wywołania umożliwiają obsługę np. żądań dotyczących niestandardowych poleceń odtwarzania i modyfikowania playlisty. Te wywołania zwrotne również zawierają obiekt ControllerInfo, dzięki czemu możesz zmienić sposób odpowiadania na poszczególne żądania z uwzględnieniem każdego kontrolera.

Modyfikowanie playlisty

Sesja multimediów może bezpośrednio modyfikować playlistę w odtwarzaczu, zgodnie z opisem w przewodniku ExoPlayer dotyczącym playlist. Kontroler może też modyfikować playlistę, jeśli COMMAND_SET_MEDIA_ITEM lub COMMAND_CHANGE_MEDIA_ITEMS jest dostępny.

Podczas dodawania nowych elementów do playlisty odtwarzacz zwykle wymaga MediaItem wystąpieni z zdefiniowanego URI, aby można było je odtworzyć. Domyślnie nowo dodane elementy są automatycznie przekazywane do metod odtwarzacza, takich jak player.addMediaItem, jeśli mają zdefiniowany identyfikator URI.

Jeśli chcesz dostosować instancje MediaItem dodane do odtwarzacza, możesz je zastąpić, używając parametru onAddMediaItems(). Ten krok jest potrzebny, jeśli chcesz obsługiwać kontrolery, które proszą o media bez zdefiniowanego identyfikatora URI. Zwykle zawiera ono co najmniej 1 z tych pól, które opisują żądane multimedia:MediaItem

  • MediaItem.id: ogólny identyfikator mediów.
  • MediaItem.RequestMetadata.mediaUri: Identyfikator URI żądania, który może używać niestandardowego schematu i niekoniecznie jest bezpośrednio odtwarzany przez odtwarzacz.
  • MediaItem.RequestMetadata.searchQuery: tekstowe zapytanie wyszukiwania, np. z Asystenta Google.
  • MediaItem.MediaMetadata: uporządkowane metadane, takie jak „title” (tytuł) czy „artist” (artysta).

Aby uzyskać więcej opcji dostosowywania zupełnie nowych playlist, możesz dodatkowo zastąpić onSetMediaItems(), co pozwala zdefiniować element początkowy i jego pozycję na playliście. Możesz na przykład rozszerzyć pojedynczy żądany element na całą playlistę i zlecić odtwarzaczowi rozpoczęcie od indeksu pierwotnie żądanego elementu. Przykładowa implementacja onSetMediaItems() z tą funkcją jest dostępna w aplikacji demonstracyjnej sesji.

Zarządzanie układem niestandardowym i poleceniami niestandardowymi

W następnych sekcjach opisujemy, jak reklamować niestandardowy układ niestandardowych przycisków poleceń w aplikacjach klienckich i autoryzować kontrolery do wysyłania niestandardowych poleceń.

Definiowanie niestandardowego układu sesji

Aby wskazać aplikacjom klienckim, które elementy sterujące odtwarzaniem chcesz udostępnić użytkownikowi, ustaw niestandardowy układ sesji podczas tworzenia obiektu MediaSession w metodzie onCreate() usługi.

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

Zadeklaruj dostępne polecenia odtwarzacza i niestandardowe

Aplikacje multimedialne mogą definiować polecenia niestandardowe, które można na przykład używać w niestandardowym układzie. Można na przykład wdrożyć przyciski, które pozwolą użytkownikowi zapisać element multimedialny na liście ulubionych. Usługa MediaController wysyła polecenia niestandardowe, a usługa MediaSession.Callback je odbiera.

Możesz określić, które polecenia sesji niestandardowej są dostępne dla MediaController, gdy połączy się z Twoją sesją multimedialną. Aby to zrobić, zastąpij wartość MediaSession.Callback.onConnect(). Skonfiguruj i zwróć zbiór dostępnych poleceń podczas akceptowania prośby o połączenie z MediaController w ramach metody wywołania zwrotnego onConnect:

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

Aby otrzymywać żądania niestandardowych poleceń z poziomu MediaController, zastąpij metodę onCustomCommand()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)
      );
    }
    ...
  }
}

Korzystając z właściwości packageName obiektu MediaSession.ControllerInfo, możesz śledzić, który kontroler multimediów wysyła żądanie. Właściwość ta jest przekazywana do metod Callback. Pozwala to dostosować działanie aplikacji na podstawie danego polecenia, jeśli pochodzi ono z systemu, Twojej aplikacji lub innych aplikacji klienckich.

Aktualizowanie niestandardowego układu po interakcji użytkownika

Po obsłudze polecenia niestandardowego lub jakiejkolwiek innej interakcji z odtwarzaczem możesz zaktualizować układ wyświetlany w interfejsie kontrolera. Typowym przykładem jest przełącznik, którego ikona zmienia się po wywołaniu działania powiązanego z tym przyciskiem. Aby zaktualizować układ, możesz użyć: MediaSession.setCustomLayout:

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

Dostosowywanie działania poleceń odtwarzania

Aby dostosować działanie polecenia zdefiniowanego w interfejsie Player, takiego jak play() lub seekToNext(), owiń Player w element 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();

Więcej informacji o ForwardingPlayer znajdziesz w przewodniku ExoPlayer dotyczącym dostosowywania.

Identyfikowanie kontrolera żądającego polecenia odtwarzacza

Gdy wywołanie metody Player pochodzi z poziomu MediaController, możesz zidentyfikować źródło za pomocą MediaSession.controllerForCurrentRequesti uzyskać ControllerInfo dla bieżącego żądania:

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

Odpowiedź na przyciski multimediów

Przyciski multimedialne to przyciski sprzętowe dostępne na urządzeniach z Androidem i innych urządzeniach peryferyjnych, np. przycisk odtwarzania/pauzowania w słuchawkach Bluetooth. Media3 obsługuje zdarzenia przycisków multimediów, gdy docierają do sesji, i wywołuje odpowiednią metodę Playerodtwarzaczu sesji.

Aplikacja może zastąpić domyślne działanie, zastępując MediaSession.Callback.onMediaButtonEvent(Intent). W takim przypadku aplikacja może samodzielnie obsługiwać wszystkie specyfikacje interfejsu API.

Obsługa błędów i raportowanie

Istnieją 2 typy błędów, które sesja generuje i zgłasza kontrolery. Błędy krytyczne zgłaszają techniczne awarie odtwarzania w odtwarzaczu w sesji, które przerywają odtwarzanie. Błędy krytyczne są automatycznie zgłaszane kontrolerowi, gdy tylko wystąpią. Błędy niekrytyczne to błędy nietechniczne lub błędy związane z naruszeniem zasad, które nie przerywają odtwarzania i są wysyłane do kontrolerów przez aplikację ręcznie.

Krytyczne błędy odtwarzania

Krytyczny błąd odtwarzania jest zgłaszany do sesji przez odtwarzacz, a następnie zgłaszany do kontrolerów do wywołania za pomocą funkcji Player.Listener.onPlayerError(PlaybackException) i Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

W takim przypadku stan odtwarzania zmienia się na STATE_IDLE, a MediaController.getPlaybackError() zwraca wartość PlaybackException, która spowodowała przejście. Kontroler może sprawdzić PlayerException.errorCode, aby uzyskać informacje o przyczynie błędu.

W celu zapewnienia interoperacyjności fatalny błąd jest replikowany do PlaybackStateCompatsesji platformy przez przejście do stanu STATE_ERROR i ustawienie kodu błędu oraz komunikatu zgodnie z PlaybackException.

Dostosowywanie błędu krytycznego

Aby wyświetlać użytkownikowi lokalizowane i przydatne informacje, kod błędu, komunikat o błędzie i informacje dodatkowe dotyczące krytycznego błędu odtwarzania można dostosować, używając ForwardingPlayer podczas tworzenia sesji:

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

Odtwarzacz przekierowujący rejestruje Player.Listener dla rzeczywistego odtwarzacza i przechwytuje wywołania zwrotne, które zgłaszają błąd. Następnie do odbiorców zarejestrowanych na odtwarzaczu przekierowującym jest delegowany niestandardowy adres PlaybackException. Aby to działało, odtwarzacz przekazujący zastępuje Player.addListener i Player.removeListener, uzyskując dostęp do detektorów, za pomocą których mogą wysyłać niestandardowe kody błędu, komunikaty i dodatki:

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

Błędy niekrytyczne

Błędy niekrytyczne, które nie pochodzą z wyjątku technicznego, mogą być wysyłane przez aplikację do wszystkich lub tylko do określonego kontrolera:

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

Niekrytyczny błąd wysłany do kontrolera powiadomień o mediach jest replikowany do PlaybackStateCompat sesji platformy. W związku z tym tylko kod błędu i komunikat o błędzie są odpowiednio ustawione na PlaybackStateCompat, a PlaybackStateCompat.state nie zmienia się na STATE_ERROR.

Otrzymywanie błędów niekrytycznych

MediaController otrzymuje błąd niekrytyczny po zaimplementowaniu klucza MediaController.Listener.onError:

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