Sterowanie odtwarzaniem i reklamowanie go za pomocą sesji MediaSession

Sesje multimedialne to uniwersalny sposób na interakcję z odtwarzaczem audio i wideo. W Media3 domyślnym odtwarzaczem jest klasa ExoPlayer, która implementuje interfejs Player. Połączenie sesji multimediów z odtwarzaczem umożliwia aplikacji reklamowanie odtwarzania multimediów na zewnątrz i odbieranie 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, na przykład instruowanie Asystenta Google „wstrzymanie”. Sesja multimediów przekazuje te polecenia do odtwarzacza aplikacji do multimediów.

Kiedy wybrać sesję multimediów

Implementacja funkcji MediaSession umożliwia użytkownikom sterowanie odtwarzaniem:

  • Przez słuchawki. Często użytkownik może wykonywać na słuchawkach przyciski lub interakcje dotykowe, aby włączyć lub wstrzymać odtwarzanie multimediów albo przejść do następnego bądź poprzedniego utworu.
  • Rozmawiając z Asystentem Google. Częstym wzorcem jest polecenie „OK Google, wstrzymaj” w celu wstrzymania odtwarzania multimediów.
  • Na zegarku z Wear OS. Ułatwia to dostęp do najpopularniejszych elementów sterujących odtwarzaniem na telefonie.
  • Za pomocą Sterowanie multimediami. Ta karuzela zawiera elementy sterujące każdej uruchomionej sesji multimediów.
  • Na telewizorze. Umożliwia korzystanie z fizycznych przycisków odtwarzania, sterowania odtwarzaniem na platformie i zarządzania energią (np. po wyłączeniu telewizora, soundbara lub amplitunera AV lub zmianie wejścia, odtwarzanie powinno zostać zatrzymane w aplikacji).
  • Oraz wszelkie inne procesy zewnętrzne, które mają wpływ na odtwarzanie.

Sprawdza się to w wielu przypadkach. W szczególności rozważ użycie atrybutu MediaSession, gdy:

  • Korzystasz z długich treści wideo, takich jak filmy lub telewizja na żywo.
  • Strumieniujesz długie treści audio, takie jak podcasty lub playlisty muzyczne.
  • Tworzysz aplikację TV.

Jednak nie wszystkie przypadki użycia są dobrze dopasowane do MediaSession. Parametru Player warto używać w tych przypadkach:

  • Wyświetlasz krótkie treści, w przypadku których zaangażowanie i interakcja ma kluczowe znaczenie.
  • Nie ma jednego aktywnego filmu, na przykład użytkownik przewija listę, a na ekranie wyświetla się wiele filmów jednocześnie.
  • Twój film stanowi jednorazowy film wprowadzający lub wyjaśniający, którego użytkownik powinien aktywnie obejrzeć.
  • Twoje treści chronią prywatność i nie chcesz, aby procesy zewnętrzne miały dostęp do metadanych multimediów (np. w trybie incognito w przeglądarce).

Jeśli Twój przypadek użycia nie pasuje do żadnej z tych kategorii, zastanów się, czy możesz kontynuować odtwarzanie aplikacji, gdy użytkownik nie wchodzi w interakcję z treścią. Jeśli odpowiedź jest twierdząca, prawdopodobnie chcesz wybrać MediaSession. Jeśli odpowiedź jest przecząca, lepiej użyć polecenia Player.

Utwórz sesję multimediów

Sesja multimediów jest umieszczona obok odtwarzacza, którym zarządza. Sesję multimediów możesz utworzyć za pomocą obiektów Context i Player. Należy utworzyć i zainicjować sesję multimediów, gdy jest to konieczne. Może to być na przykład metoda cyklu życia onStart() lub onResume() obiektu Activity lub Fragment albo metoda onCreate() obiektu Service, do którego należy sesja multimediów i powiązany z nią odtwarzacz.

Aby utworzyć sesję multimediów, zainicjuj Player i przekaż go 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();

Automatyczna obsługa stanu

Biblioteka Media3 automatycznie aktualizuje sesję multimediów na podstawie stanu odtwarzacza. Dzięki temu nie musisz ręcznie obsługiwać mapowania między odtwarzaczem a sesją.

To przerwa w porównaniu ze starszym podejściem, w ramach którego trzeba było tworzyć i utrzymywać element PlaybackStateCompat niezależnie od samego odtwarzacza, na przykład aby wskazać ewentualne błędy.

Unikalny identyfikator sesji

Domyślnie MediaSession.Builder tworzy sesję z pustym ciągiem znaków jako identyfikatorem sesji. To wystarczające, jeśli aplikacja chce utworzyć tylko 1 instancję sesji, co jest najczęstsze.

Jeśli aplikacja chce zarządzać wieloma 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 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 w wyniku błędu programowania, takie przypadki są wykrywane i powiadamiane przez zgłoszenie wyjątku.

Przekazywanie kontroli innym klientom

Sesja multimedialna jest kluczem do kontrolowania odtwarzania. Pozwala kierować polecenia ze źródeł zewnętrznych do odtwarzacza odtwarzającego multimedia. Mogą to być fizyczne przyciski, np. przycisk odtwarzania na słuchawkach lub pilocie do telewizora, albo pośrednie polecenia, takie jak polecenie „wstrzymania” Asystentowi Google. Możesz też przyznać dostęp systemowi Android, aby ułatwić sterowanie powiadomieniami i ekranem blokady, a zegarkowi z Wear OS, który umożliwi sterowanie odtwarzaniem z poziomu tarczy zegarka. Klienty zewnętrzne mogą używać kontrolera multimediów do wydawania poleceń odtwarzania w aplikacji do multimediów. Są one odbierane przez sesję multimediów, która ostatecznie przekazuje polecenia do odtwarzacza.

Diagram przedstawiający interakcję między obiektami MediaSession i MediaController.
Rysunek 1. Kontroler multimediów ułatwia przekazywanie poleceń ze źródeł zewnętrznych do sesji multimediów.

Gdy kontroler ma się połączyć z sesją multimediów, wywoływana jest metoda onConnect(). Możesz użyć udostępnionego ControllerInfo, aby zdecydować, czy chcesz zaakceptować, czy odrzucić żądanie. Przykład akceptowania prośby o połączenie znajdziesz w sekcji Deklarowanie dostępnych poleceń.

Po nawiązaniu połączenia kontroler może wysyłać do sesji polecenia odtwarzania. W sesji te polecenia są przekazywane graczowi. Zdefiniowane w interfejsie Player polecenia dotyczące odtwarzania i playlisty są obsługiwane automatycznie przez sesję.

Inne metody wywołania zwrotnego pozwalają obsługiwać np. żądania niestandardowych poleceń odtwarzania czy modyfikowanie playlisty. Wywołania te podobnie zawierają obiekt ControllerInfo, dzięki czemu możesz modyfikować sposób odpowiadania na każde żądanie z uwzględnieniem poszczególnych kontrolerów.

Modyfikowanie playlisty

Sesja multimediów może bezpośrednio modyfikować playlistę w odtwarzaczu, zgodnie z opisem w przewodniku po playlistach dotyczącym odtwarzacza ExoPlay. Kontrolery mogą też modyfikować playlistę, jeśli ma ona dostęp do funkcji COMMAND_SET_MEDIA_ITEM lub COMMAND_CHANGE_MEDIA_ITEMS.

Gdy dodajesz nowe elementy do playlisty, odtwarzacz zwykle wymaga instancji MediaItem z zdefiniowanym identyfikatorem URI, aby można je było 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 zastąpić instancję onAddMediaItems(). Ten krok jest potrzebny, gdy chcesz obsługiwać kontrolery, które żądają multimediów bez zdefiniowanego identyfikatora URI. Zamiast tego MediaItem zwykle zawiera co najmniej jedno z tych pól opisujących żądane multimedia:

  • MediaItem.id: ogólny identyfikator identyfikujący multimedia.
  • MediaItem.RequestMetadata.mediaUri: identyfikator URI żądania, który może korzystać ze schematu niestandardowego i niekoniecznie musi być odtwarzany bezpośrednio przez odtwarzacz.
  • MediaItem.RequestMetadata.searchQuery: zapytanie tekstowe, np. z Asystenta Google.
  • MediaItem.MediaMetadata: uporządkowane metadane, np. „tytuł” lub „wykonawca”.

Aby uzyskać więcej opcji dostosowywania całkowicie nowych playlist, możesz dodatkowo zastąpić parametr onSetMediaItems(), który pozwala określić element początkowy i pozycję na playliście. Możesz na przykład rozwinąć jeden żądany element do całej playlisty i poinstruować odtwarzacz, aby zaczynał od indeksu elementu, który był początkowo żądany. Przykładową implementację interfejsu onSetMediaItems() z tą funkcją znajdziesz w prezentacji sesji.

Zarządzanie układem niestandardowym i poleceniami niestandardowymi

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

Zdefiniuj niestandardowy układ sesji

Aby wskazać aplikacjom klienckim, które elementy sterujące odtwarzaniem chcesz wyświetlić użytkownikowi, ustaw niestandardowy układ sesji podczas tworzenia MediaSession w metodzie onCreate() swojej 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();
}

Deklarowanie dostępnego odtwarzacza i poleceń niestandardowych

Aplikacje multimedialne mogą definiować polecenia niestandardowe, których można używać np. w układach niestandardowych. 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 otrzymuje.

Możesz określić, które niestandardowe polecenia sesji mają być dostępne dla MediaController podczas nawiązywania połączenia z sesją multimediów. Aby to zrobić, zastąpisz pole MediaSession.Callback.onConnect(). Skonfiguruj i zwróć zestaw dostępnych poleceń podczas akceptowania żądania połączenia z MediaController w metodzie 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ć niestandardowe żądania poleceń z: MediaController, zastąp metodę onCustomCommand() w 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 przekazywanego do metod Callback, możesz śledzić, który kontroler multimediów wysyła żądanie. Dzięki temu możesz dostosować działanie aplikacji w odpowiedzi na dane polecenie, jeśli pochodzi ono z systemu, Twojej własnej aplikacji lub innych aplikacji klienckich.

Aktualizowanie układu niestandardowego po interakcji użytkownika

Po obsłudze niestandardowego polecenia lub dowolnej innej interakcji z odtwarzaczem możesz zaktualizować układ wyświetlany w interfejsie kontrolera. Typowym przykładem jest przycisk przełączania, którego ikonę zmienia się po uruchomieniu powiązanego z nim działania. 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));

Dostosuj działanie polecenia odtwarzania

Aby dostosować działanie polecenia zdefiniowanego w interfejsie Player, np. play() lub seekToNext(), umieść Player w 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.

Zidentyfikuj kontroler żądający polecenia odtwarzacza

Jeśli wywołanie metody Player pochodzi z MediaController, możesz zidentyfikować źródło za pomocą MediaSession.controllerForCurrentRequest i 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();
  }
}

Reagowanie na przyciski multimediów

Przyciski multimediów to przyciski sprzętowe dostępne 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 multimediów, gdy docierają do sesji, i wywołuje odpowiednią metodę Player w odtwarzaczu sesji.

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