Sterowanie odtwarzaniem i reklamowanie go za pomocą sesji MediaSession

Sesje multimedialne zapewniają uniwersalny sposób interakcji z odtwarzaczem audio lub wideo. W Media3 domyślnym odtwarzaczem jest klasa ExoPlayer, która implementuje interfejs Player. Połączenie sesji multimedialnej z odtwarzaczem umożliwia aplikacji przekazywanie informacji o odtwarzaniu multimediów źródłom zewnętrznym i otrzymywanie poleceń odtwarzania ze źródeł zewnętrznych.

Polecenia mogą pochodzić z fizycznych przycisków, takich jak przycisk odtwarzania na zestawie słuchawkowym lub pilocie do telewizora. Mogą też pochodzić z aplikacji klienckich, które mają kontroler multimediów, np. polecenie „wstrzymaj” dla asystentów AI, takich jak Gemini czy Asystent Google. Sesja multimedialna przekazuje te polecenia do odtwarzacza aplikacji do multimediów.

Kiedy wybrać sesję multimedialną

Gdy wdrożysz MediaSession, użytkownicy będą mogli sterować odtwarzaniem:

  • Przez słuchawki. Słuchawki często mają przyciski lub funkcje dotykowe, które umożliwiają odtwarzanie i wstrzymywanie multimediów oraz przechodzenie do następnego lub poprzedniego utworu.
  • Rozmawiając z asystentami AI, takimi jak Gemini czy Asystent Google. Często używane polecenie to „OK Google, wstrzymaj”, które wstrzymuje multimedia odtwarzane w danym momencie na urządzeniu.
  • Za pomocą zegarka z Wear OS. Ułatwia to dostęp do najczęściej używanych elementów sterujących odtwarzaniem podczas korzystania z telefonu.
  • Za pomocą opcji sterowania multimediami. Ta karuzela zawiera elementy sterujące dla każdej aktywnej sesji multimedialnej.
  • Na telewizorze. Umożliwia to wykonywanie działań za pomocą fizycznych przycisków odtwarzania, sterowania odtwarzaniem na platformie i zarządzania zasilaniem (np. jeśli telewizor, soundbar lub amplituner wyłączy się lub zmieni wejście, odtwarzanie w aplikacji powinno się zatrzymać).
  • Za pomocą elementów sterujących multimediami w Androidzie Auto. Umożliwia to bezpieczne sterowanie odtwarzaniem podczas jazdy.
  • Za pomocą wszystkich innych procesów zewnętrznych, które mają wpływ na odtwarzanie.

To świetne rozwiązanie w wielu przypadkach. Warto rozważyć użycie MediaSession, gdy:

  • przesyłasz strumieniowo długie treści wideo, takie jak filmy czy telewizja na żywo;
  • przesyłasz strumieniowo długie treści audio, takie jak podcasty czy playlisty muzyczne;
  • tworzysz aplikację TV.

Nie wszystkie przypadki użycia pasują jednak do MediaSession. W tych przypadkach warto użyć tylko interfejsu Player:

  • Wyświetlasz krótkie treści, które nie wymagają zewnętrznego sterowania ani odtwarzania w tle.
  • Nie ma jednego aktywnego filmu, np. użytkownik przewija listę i na ekranie wyświetla się jednocześnie kilka filmów.
  • Odtwarzasz jednorazowy film wprowadzający lub film z wyjaśnieniem, który użytkownik ma aktywnie oglądać bez konieczności korzystania z zewnętrznych elementów sterujących odtwarzaniem.
  • Twoje treści są poufne i nie chcesz, aby zewnętrzne procesy 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 chcesz, aby aplikacja kontynuowała odtwarzanie, gdy użytkownik nie ogląda treści w sposób aktywny. Jeśli tak, prawdopodobnie lepiej wybrać MediaSession. Jeśli nie, prawdopodobnie zamiast tego lepiej użyć Player.

Tworzenie sesji multimedialnej

Sesja multimedialna działa razem z odtwarzaczem, którym zarządza. Sesję multimedialną możesz utworzyć za pomocą obiektów Context i Player. Sesję multimedialną należy utworzyć i zainicjować, gdy jest potrzebna, np. w metodzie cyklu życia onStart() lub onResume() elementu Activity lub Fragment albo w metodzie onCreate() elementu Service, który jest właścicielem sesji multimedialnej i powiązanego z nią odtwarzacza.

Aby utworzyć sesję multimedialną, zainicjuj Player i podaj go w 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();

Automatyczna obsługa stanu

Biblioteka Media3 automatycznie aktualizuje sesję multimedialną na podstawie stanu odtwarzacza. Dzięki temu nie musisz ręcznie mapować odtwarzacza na sesję.

Różni się to od sesji multimedialnej platformy, w której trzeba było utworzyć i utrzymywać obiekt PlaybackState niezależnie od samego odtwarzacza, na przykład w celu wskazania błędów.

Unikalny identyfikator sesji

Domyślnie klasa MediaSession.Builder tworzy sesję z pustym ciągiem znaków jako identyfikatorem sesji. Jest to wystarczające, jeśli aplikacja ma utworzyć tylko jedną instancję sesji, co jest najczęstszym przypadkiem.

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

Jeśli widzisz, że IllegalStateException powoduje awarię aplikacji i wyświetla komunikat o błędzie IllegalStateException: Session ID must be unique. ID=, prawdopodobnie sesja została nieoczekiwanie utworzona, zanim została zwolniona wcześniej utworzona instancja o tym samym identyfikatorze. Aby uniknąć wycieku sesji z powodu błędu programistycznego, takie przypadki są wykrywane i zgłaszane przez wyjątek.

Przyznawanie kontroli innym klientom

Sesja multimedialna jest kluczem do sterowania odtwarzaniem. Umożliwia kierowanie poleceń ze źródeł zewnętrznych do odtwarzacza, który odtwarza multimedia. Źródłami mogą być przyciski fizyczne, np. przycisk odtwarzania na zestawie słuchawkowym lub pilocie do telewizora, albo polecenia pośrednie, np. polecenie „wstrzymaj” wydane Gemini lub Asystentowi Google. Możesz też przyznać dostęp do systemu Android, aby ułatwić sterowanie powiadomieniami i ekranem blokady, lub do zegarka z Wear OS, aby sterować odtwarzaniem z poziomu tarczy zegarka. Klienty zewnętrzne mogą używać kontrolera multimediów do wydawania poleceń odtwarzania aplikacji do multimediów. Sesja multimedialna odbiera te polecenia i przekazuje je do odtwarzacza.

Interakcja między MediaSession a MediaController.
Ilustracja 1. Kontroler multimediów ułatwia przekazywanie poleceń ze źródeł zewnętrznych do sesji multimedialnej.

Gdy kontroler ma się połączyć z sesją multimedialną, sesja wywołuje metodę onConnectAsync(). Na podstawie podanego obiektu ControllerInfo możesz zaakceptować lub odrzucić prośbę. Przykład akceptowania prośby o połączenie znajdziesz w sekcji Deklarowanie i obsługa poleceń niestandardowych.

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

Inne metody wywołania zwrotnego umożliwiają obsługę np. żądań poleceń niestandardowych i modyfikowania playlisty. Te wywołania zwrotne również zawierają obiekt ControllerInfo, dzięki czemu możesz modyfikować sposób odpowiadania na każde żądanie na poziomie kontrolera.

Modyfikowanie playlisty

Sesja multimedialna może bezpośrednio modyfikować playlistę odtwarzacza, jak wyjaśniono w przewodniku dotyczącym playlist w bibliotece ExoPlayer. Kontrolery mogą też modyfikować playlistę, jeśli mają dostęp do COMMAND_SET_MEDIA_ITEM lub COMMAND_CHANGE_MEDIA_ITEMS.

Podczas dodawania nowych elementów do playlisty odtwarzacz zwykle wymaga instancji MediaItem z określonymi identyfikatorami 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 dodawane do odtwarzacza, możesz zastąpić onAddMediaItems(). Ten krok jest potrzebny, jeśli chcesz obsługiwać kontrolery, które żądają multimediów bez zdefiniowanego identyfikatora URI. Zamiast tego w MediaItem zwykle ustawia się co najmniej jedno z tych pól, aby opisać żądane multimedia:

  • MediaItem.mediaId: ogólny identyfikator multimediów.
  • MediaItem.RequestMetadata.mediaUri: identyfikator URI żądania, który może używać schematu niestandardowego i nie musi być bezpośrednio odtwarzany przez odtwarzacz.
  • MediaItem.RequestMetadata.searchQuery: tekst wyszukiwanego hasła, np. z Gemini lub Asystenta Google.
  • MediaItem.mediaMetadata: uporządkowane metadane, takie jak „tytuł” lub „wykonawca”.

Aby uzyskać więcej opcji dostosowywania zupełnie nowych playlist, możesz dodatkowo zastąpić metodę onSetMediaItems(), która pozwala zdefiniować element początkowy i jego pozycję na playliście. Możesz na przykład rozszerzyć pojedynczy żądany element na całą playlistę i polecić odtwarzaczowi rozpoczęcie od indeksu pierwotnie żądanego elementu. Przykładową implementację onSetMediaItems() z tą funkcją znajdziesz w aplikacji demonstracyjnej sesji.

Publikowanie metadanych playlisty

Media3 udostępnia interfejsy API do publikowania uporządkowanych metadanych playlisty wraz z odtwarzaniem aktywnej ścieżki. Ujednolicenie sposobu, w jaki aplikacja wypełnia informacje o przynależności do playlisty i metadane opisowe w sesjach multimedialnych, umożliwia Androidowi udostępnianie punktów wejścia na poziomie systemu do odtwarzania w aplikacji.

W tym kontekście termin playlista odnosi się do dowolnej uporządkowanej grupy utworów, które mają być odtwarzane po kolei. Dotyczy to nie tylko list poszczególnych elementów do odtworzenia zdefiniowanych przez użytkownika lub aplikację, ale też grup, takich jak albumy muzyczne, odcinki podcastów i rozdziały audiobooków.

Deklarowanie identyfikatora playlisty i jej metadanych

W Media3 identyfikacja playlisty i metadane opisowe playlisty są podzielone na 2 zakresy MediaMetadata:

  • Identyfikator playlisty na poziomie elementu (MediaItem.mediaMetadata.playlistId):
    • Oznacz odtwarzany element MediaItem jako część playlisty, wywołując MediaMetadata.Builder.setPlaylistId na elemencie MediaMetadata, podczas gdy MediaItem.mediaId jednoznacznie identyfikuje pojedynczy utwór.
    • playlistId to nieprzezroczysty, zdefiniowany przez aplikację ciąg znaków, który jednoznacznie identyfikuje playlistę w różnych sesjach, dopóki można ją wznowić (np. 37i9dQZF1DX76Wlfdnj7AP). Nie należy go używać w przypadku innej playlisty. Połączone kontrolery nie interpretują tego identyfikatora, tylko go przechowują i wysyłają z powrotem do aplikacji, aby wznowić odtwarzanie playlisty.
    • Każdy utwór należący do playlisty ma ten sam identyfikator playlistId. W przypadku utworów, które nie są odtwarzane w kontekście playlisty, np. pojedynczego samodzielnego utworu, pozostaw parametr playlistId nieustawiony (null).
  • Metadane playlisty na poziomie odtwarzacza (Player.getPlaylistMetadata):
    • Wypełnij dedykowaną instancję MediaMetadata atrybutami opisowymi playlisty (setTitle, setArtworkUri i setMediaType) i ustaw ją w odtwarzaczu za pomocą Player.setPlaylistMetadata.
    • Gdy połączony kontroler wykryje wartość currentMediaItem.mediaMetadata.playlistId inną niż null, sprawdza MediaController.getPlaylistMetadata, aby pobrać tytuł playlisty i identyfikator URI grafiki.
    • Aktualizuj metadane playlisty za każdym razem, gdy zmienia się aktywna playlista (np. gdy użytkownik uruchamia inną playlistę), aby getPlaylistMetadata zawsze opisywało playlistę, do której odwołuje się playlistId bieżącego elementu.

W tym przykładzie pokazujemy, jak opublikować metadane playlisty na kanale Player i powiązać z nią utwór MediaItem:

Kotlin

// 1. Declare the playlist descriptive metadata on the Player
val playlistMetadata =
  MediaMetadata.Builder()
    .setTitle("Morning Vibes")
    .setArtworkUri(Uri.parse("https://example.com/image/morning-vibes"))
    .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
    .build()
player.setPlaylistMetadata(playlistMetadata)

// 2. Mark the playing track(s) with the playlistId it belongs to
val trackItem =
  MediaItem.Builder()
    .setMediaId("foobar:track:6rqhFgbbKwnb9MLmUQDhG6")
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setTitle("Example Track")
        .setArtist("Example Artist")
        .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
        .setPlaylistId("37i9dQZF1DX76Wlfdnj7AP")
        .build()
    )
    .build()
player.setMediaItem(trackItem)

Java

// 1. Declare the playlist descriptive metadata on the Player
MediaMetadata playlistMetadata =
    new MediaMetadata.Builder()
        .setTitle("Morning Vibes")
        .setArtworkUri(Uri.parse("https://example.com/image/morning-vibes"))
        .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
        .build();
player.setPlaylistMetadata(playlistMetadata);

// 2. Mark the playing track(s) with the playlistId it belongs to
MediaItem trackItem =
    new MediaItem.Builder()
        .setMediaId("foobar:track:6rqhFgbbKwnb9MLmUQDhG6")
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setTitle("Example Track")
                .setArtist("Example Artist")
                .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
                .setPlaylistId("37i9dQZF1DX76Wlfdnj7AP")
                .build())
        .build();
player.setMediaItem(trackItem);

Wznawianie playlisty

Gdy użytkownik wybierze playlistę, którą chce wznowić, kontroler połączy się z sesją multimedialną aplikacji i wyśle playlistę MediaItem, której mediaId jest ustawiony na zapisany playlistId:

Kotlin

val playlistItem =
  MediaItem.Builder()
    .setMediaId("37i9dQZF1DX76Wlfdnj7AP")
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
        .setIsPlayable(true)
        .build()
    )
    .build()
controller.setMediaItem(playlistItem)
controller.prepare()
controller.play()

Java

MediaItem playlistItem =
    new MediaItem.Builder()
        .setMediaId("37i9dQZF1DX76Wlfdnj7AP")
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
                .setIsPlayable(true)
                .build())
        .build();
controller.setMediaItem(playlistItem);
controller.prepare();
controller.play();

Aplikacja rozszerza playlistę MediaItem na możliwe do odtworzenia utwory w MediaSession.Callback.onSetMediaItems (lub onAddMediaItems) i ponownie publikuje playlistId i playlistMetadata, jak pokazano w poprzedniej sekcji.

Zarządzanie ustawieniami przycisków multimedialnych

Każdy kontroler, np. interfejs systemu, Android Auto czy Wear OS, może samodzielnie decydować, które przyciski wyświetlać użytkownikowi. Aby wskazać, które elementy sterujące odtwarzaniem chcesz udostępnić użytkownikowi, możesz określić ustawienia przycisków multimedialnych na urządzeniu MediaSession. Te preferencje składają się z listy uporządkowanej instancji CommandButton, z których każda definiuje preferencje dotyczące przycisku w interfejsie użytkownika.

Definiowanie przycisków poleceń

Instancje CommandButton służą do określania preferencji dotyczących przycisków multimedialnych. Każdy przycisk określa 3 aspekty elementu interfejsu:

  1. Ikona definiująca wygląd. Podczas tworzenia CommandButton.Builder ikona musi być ustawiona na jedną ze wstępnie zdefiniowanych stałych. Pamiętaj, że nie jest to rzeczywista bitmapa ani obraz. Ogólna stała pomaga kontrolerom wybrać odpowiedni zasób, aby zapewnić spójny wygląd i działanie w ich interfejsie. Jeśli żadna ze stałych wartości ikon nie pasuje do Twojego przypadku użycia, możesz zamiast niej użyć setCustomIconResId.
  2. Polecenie, które definiuje działanie wywoływane, gdy użytkownik wejdzie w interakcję z przyciskiem. Możesz użyć setPlayerCommand w przypadku Player.Command lub setSessionCommand w przypadku wstępnie zdefiniowanego lub niestandardowego SessionCommand.
  3. Miejsce, które określa, gdzie przycisk powinien być umieszczony w interfejsie kontrolera. To pole jest opcjonalne i jest ustawiane automatycznie na podstawie ikony i polecenia. Na przykład pozwala określić, że przycisk powinien być wyświetlany w obszarze nawigacji po prawej stronie interfejsu, a nie w domyślnym obszarze ukrytych ikon.

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

Podczas ustalania preferencji dotyczących przycisków multimedialnych stosowany jest ten algorytm:

  1. Dla każdego CommandButton w ustawieniach przycisków multimedialnych umieść przycisk w pierwszym dostępnym i dozwolonym miejscu.
  2. Jeśli którekolwiek z miejsc na środku, po prawej i po lewej nie są wypełnione przyciskiem, dodaj do tego miejsca domyślne przyciski.

Możesz użyć CommandButton.DisplayConstraints, aby wygenerować podgląd tego, jak ustawienia przycisków multimedialnych będą rozwiązywane w zależności od ograniczeń wyświetlania interfejsu.

Ustawianie preferencji przycisków multimedialnych

Najprostszym sposobem ustawienia preferencji dotyczących przycisków multimedialnych jest zdefiniowanie listy podczas tworzenia MediaSession. Możesz też zastąpić MediaSession.Callback.onConnectAsync(), aby dostosować ustawienia przycisków multimedialnych do każdego podłączonego kontrolera.

Kotlin

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Java

MediaSession mediaSession =
    new MediaSession.Builder(context, player)
        .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
        .build();

Aktualizowanie ustawień przycisków multimedialnych po interakcji użytkownika

Po obsłużeniu interakcji z odtwarzaczem możesz zaktualizować przyciski wyświetlane w interfejsie kontrolera. Typowym przykładem jest przycisk przełączania, który zmienia ikonę i działanie po wywołaniu działania powiązanego z tym przyciskiem. Aby zaktualizować ustawienia przycisków multimedialnych, możesz użyć MediaSession.setMediaButtonPreferences, aby zaktualizować ustawienia wszystkich kontrolerów lub konkretnego kontrolera:

Kotlin

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton));

Dodawanie poleceń niestandardowych i dostosowywanie działania domyślnego

Dostępne polecenia odtwarzacza można rozszerzyć o polecenia niestandardowe. Można też przechwytywać przychodzące polecenia odtwarzacza i przyciski multimediów, aby zmienić działanie domyślne.

Deklarowanie i obsługa poleceń niestandardowych

Aplikacje multimedialne mogą definiować polecenia niestandardowe, których można używać na przykład w ustawieniach przycisków multimedialnych. Możesz na przykład zaimplementować przyciski, które pozwolą użytkownikowi zapisać element multimedialny na liście ulubionych. MediaController wysyła polecenia niestandardowe, a MediaSession.Callback je odbiera.

Aby zdefiniować niestandardowe polecenia, musisz zastąpić MediaSession.Callback.onConnectAsync(), aby ustawić niestandardowe polecenia dostępne dla każdego podłączonego kontrolera.

Kotlin

private class CustomMediaSessionCallback : MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  override fun onConnectAsync(
    session: MediaSession,
    controller: ControllerInfo,
  ): ListenableFuture<ConnectionResult> {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return Futures.immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
}

Java

private static class CustomMediaSessionCallback implements MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  @Override
  public ListenableFuture<ConnectionResult> onConnectAsync(
      MediaSession session, ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return Futures.immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
}

Aby otrzymywać niestandardowe żądania poleceń z MediaController, zastąp metodę onCustomCommand() w Callback.

Kotlin

private class CustomCallback : MediaSession.Callback {
  // ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: 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))
    }
    // ...
    return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
  }
}

Java

private static class CustomCallback 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));
    }
    // ...
    return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
  }
}

Możesz śledzić, który kontroler multimediów wysyła żądanie, korzystając z właściwości packageName obiektu MediaSession.ControllerInfo przekazywanego do metod Callback. Dzięki temu możesz dostosować działanie aplikacji w odpowiedzi na dane polecenie, jeśli pochodzi ono z systemu, Twojej aplikacji lub innych aplikacji klienckich.

Dostosowywanie domyślnych poleceń odtwarzacza

Wszystkie domyślne polecenia i obsługa stanu są delegowane do interfejsu Player, które znajduje się w sesji MediaSession. Aby dostosować działanie polecenia zdefiniowanego w interfejsie Player, np. play() lub seekToNext(), umieść Player w ForwardingSimpleBasePlayer przed przekazaniem go do MediaSession:

Kotlin

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

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

Java

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

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

Więcej informacji o ForwardingSimpleBasePlayer znajdziesz w przewodniku po bibliotece ExoPlayer w sekcji Dostosowywanie.

Określanie kontrolera, który wysłał polecenie do odtwarzacza

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

Kotlin

private class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) {
  private lateinit var session: MediaSession

  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

private static final class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  private MediaSession session;

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

Dostosowywanie obsługi przycisków multimedialnych

Przyciski multimedialne to fizyczne przyciski na urządzeniach z Androidem i innych urządzeniach peryferyjnych, np. przycisk odtwarzania/wstrzymywania na zestawie słuchawkowym Bluetooth. Media3 obsługuje zdarzenia przycisków multimedialnych, gdy docierają one do sesji, i wywołuje odpowiednią metodę Player w odtwarzaczu sesji.

Zalecamy obsługę wszystkich przychodzących zdarzeń przycisków multimedialnych w odpowiedniej metodzie Player. W bardziej zaawansowanych przypadkach użycia zdarzenia przycisków multimedialnych można przechwytywać w MediaSession.Callback.onMediaButtonEvent().

Obsługa błędów i raportowanie

Sesja może zgłaszać kontrolerom 2 rodzaje błędów. Błędy krytyczne to błędy techniczne w odtwarzaczu sesji, które przerywają odtwarzanie. Są one zgłaszane do kontrolera automatycznie, gdy wystąpią. Błędy niekrytyczne to błędy nietechniczne lub związane z zasadami, które nie przerywają odtwarzania i są wysyłane do kontrolerów ręcznie przez aplikację.

Krytyczne błędy odtwarzania

Odtwarzacz zgłasza do sesji krytyczny błąd odtwarzania, a następnie zgłasza go do kontrolerów za pomocą Player.Listener.onPlayerError(PlaybackException) i Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

W takim przypadku stan odtwarzania zmienia się na STATE_IDLE, a MediaController.getPlayerError() zwraca wyjątek PlaybackException, który spowodował zmianę stanu. Kontroler może sprawdzić PlaybackException.errorCode, aby uzyskać informacje o przyczynie błędu.

Ustawianie błędu niestandardowego odtwarzacza

Oprócz błędów krytycznych zgłaszanych przez odtwarzacz aplikacja może ustawić niestandardowy wyjątek PlaybackException na poziomie MediaSession za pomocą MediaSession.setPlaybackException(PlaybackException). Umożliwia to aplikacji sygnalizowanie stanu błędu podłączonym kontrolerom. Wyjątek można ustawić dla wszystkich podłączonych kontrolerów lub dla konkretnego obiektu ControllerInfo.

Gdy aplikacja ustawia PlaybackException za pomocą tego interfejsu API:

  • Połączone instancje MediaController zostaną powiadomione. Wywołania zwrotne Listener.onPlayerError(PlaybackException) i Listener.onPlayerErrorChanged(@Nullable PlaybackException) na kontrolerze zostaną wywołane z podanym wyjątkiem.

  • Metoda MediaController.getPlayerError() zwróci wartość PlaybackException ustawioną przez aplikację.

  • Stan odtwarzania na kontrolerach, których dotyczy problem, zmieni się na Player.STATE_IDLE.

  • Dostępne polecenia zostaną usunięte. Pozostaną tylko polecenia odczytu, takie jak COMMAND_GET_TIMELINE, które zostały już wcześniej przyznane. Stan Timeline zostaje na przykład zamrożony w stanie, w którym wyjątek został zastosowany do kontrolera. Polecenia, które próbują zmienić stan odtwarzacza, np. COMMAND_PLAY, są usuwane, dopóki aplikacja nie usunie wyjątku odtwarzania dla danego kontrolera.

Aby wyczyścić wcześniej ustawiony niestandardowy PlaybackException i przywrócić normalne raportowanie stanu odtwarzacza, aplikacja może wywołać MediaSession.setPlaybackException(/* playbackException= */ null) lub MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null).

Dostosowywanie błędów krytycznych

Aby przekazywać użytkownikowi zlokalizowane i przydatne informacje, możesz dostosować kod błędu, komunikat o błędzie i dodatkowe informacje o błędzie krytycznym odtwarzania pochodzącym z rzeczywistego odtwarzacza. Aby to zrobić, użyj ForwardingSimpleBasePlayer podczas tworzenia sesji:

Kotlin

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

Java

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

Podklasa ForwardingSimpleBasePlayer może zastąpić getState(), aby przechwycić błąd i dostosować kod błędu, komunikat lub dodatkowe informacje. W ten sam sposób możesz też wygenerować nowe błędy, które nie występują w oryginalnym odtwarzaczu:

Kotlin

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

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon().setPlayerError(customizePlaybackException(state.playerError!!)).build()
    }
    return state
  }

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

Java

private static class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon().setPlayerError(customizePlaybackException(state.playerError)).build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    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;
      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);
  }
}

Błędy niekrytyczne

Błędy niekrytyczne, które nie wynikają z wyjątku technicznego, mogą być wysyłane przez aplikację do wszystkich kontrolerów lub do konkretnego kontrolera:

Kotlin

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

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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));

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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);
}

Gdy do kontrolera powiadomień o multimediach zostanie wysłany błąd niekrytyczny, kod błędu i komunikat o błędzie zostaną powielone w sesji multimedialnej platformy, a wartość PlaybackState.state nie zostanie zmieniona na STATE_ERROR.

Otrzymywanie błędów niekrytycznych

MediaController otrzymuje błąd niekrytyczny, implementując 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.
              }
            });