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 reklamowanie odtwarzania multimediów na zewnątrz i odbieranie poleceń odtwarzania ze źródeł zewnętrznych.

Polecenia mogą być wykonywane z przycisków fizycznych, takich jak przycisk odtwarzania na słuchawkach czy pilota telewizora. Mogą też pochodzić z aplikacji klienckich mających kontroler multimediów, np. instruujących Asystenta Google „wstrzymanie”. Sesja multimediów przekazuje te polecenia do odtwarzacza aplikacji do multimediów.

Kiedy wybrać sesję multimediów

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

  • Przez słuchawki. 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. Często używany schemat jest mówienie „OK Google, wstrzymaj”, by wstrzymać odtwarzanie multimediów.
  • Na zegarku z Wear OS. Ułatwia to dostęp do najczęściej używanych elementów sterujących odtwarzaniem podczas gry na telefonie.
  • Za pomocą opcji sterowania multimediami. Ta karuzela pokazuje elementy sterujące każdej uruchomionej sesji multimedialnej.
  • Na telewizorze. Umożliwia wykonywanie działań związanych z fizycznymi przyciskami odtwarzania, sterowaniem odtwarzaniem na platformie i zarządzaniem zasilaniem.
  • oraz wszelkie inne procesy zewnętrzne, które muszą wpłynąć na odtwarzanie.

Taka konfiguracja sprawdza się w wielu zastosowaniach. Używanie właściwości MediaSession zalecamy szczególnie wtedy, gdy:

  • Prowadzisz długie treści wideo, np. filmy lub telewizję na żywo.
  • Odtwarzasz długie treści audio, takie jak podcasty lub playlisty muzyczne.
  • Tworzysz aplikację telewizyjną.

Jednak nie wszystkie przypadki użycia dobrze pasują do MediaSession. Funkcji Player możesz używać tylko w tych przypadkach:

  • Wyświetlasz krótkie treści, w których kluczowe znaczenie ma interakcja i zaangażowanie użytkowników.
  • Nie ma żadnego aktywnego filmu, tak jak by użytkownik przewijał listę i na ekranie wyświetlało się kilka filmów jednocześnie.
  • Odtwarzam jednorazowy film z wprowadzeniem lub wyjaśnieniem, który powinien aktywnie oglądać użytkownik.
  • Twoje treści mają ochronę prywatności 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 żadnej z powyższych sytuacji, zastanów się, czy możesz nadal odtwarzać aplikację, gdy użytkownik nie wchodzi w interakcję z treściami. Jeśli odpowiedź jest twierdząca, prawdopodobnie wybierz MediaSession. Jeśli odpowiedź brzmi „nie”, prawdopodobnie lepiej będzie użyć funkcji Player.

Tworzenie sesji multimediów

Sesja multimedialna działa razem z odtwarzaczem, którym zarządza. Sesję multimediów można 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 obiekt 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 stanów

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 MediaSession.Builder tworzy sesję z pustym ciągiem znaków jako jej identyfikator. Jest to wystarczające, jeśli aplikacja zamierza utworzyć tylko jedną instancję sesji, co jest najczęściej spotykanym przypadkiem.

Jeśli aplikacja chce zarządzać wieloma wystąpieniami sesji jednocześnie, musi zadbać o to, by identyfikator każdej sesji był niepowtarzalny. 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.

Przyznaj kontrolę innym klientom

Sesja multimediów to klucz do sterowania odtwarzaniem. Pozwala kierować polecenia ze źródeł zewnętrznych do odtwarzacza, w którym odtwarzasz multimedia. Mogą to być przyciski fizyczne, takie jak przycisk odtwarzania na słuchawkach lub pilocie telewizora, albo polecenia pośrednie, np. polecenie „wstrzymaj” Asystentowi Google. Możesz też przyznać dostęp do systemu Android, który ułatwia sterowanie powiadomieniami i ekranem blokady, lub do zegarka z Wear OS, który umożliwia sterowanie odtwarzaniem z poziomu tarczy zegarka. Klienty zewnętrzne mogą używać kontrolera multimediów do wydawania poleceń dotyczących odtwarzania w Twojej aplikacji do multimediów. Są one odbierane przez sesję multimediów, która ostatecznie przekazuje te polecenia do odtwarzacza.

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

Gdy kontroler ma zamiar połączyć się z sesją multimediów, wywoływana jest metoda onConnect(). Możesz użyć podanego ControllerInfo, aby zdecydować, czy chcesz 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 dotyczące odtwarzania i playlist zdefiniowane w interfejsie Player są automatycznie obsługiwane przez sesję.

Inne metody wywołania zwrotnego umożliwiają na przykład obsługę żądań niestandardowych poleceń odtwarzania czy 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. Kontrolery również mogą modyfikować playlistę, jeśli COMMAND_SET_MEDIA_ITEM lub COMMAND_CHANGE_MEDIA_ITEMS są dostępne dla kontrolera.

Gdy dodajesz nowe elementy do playlisty, odtwarzacz zazwyczaj wymaga instancji MediaItem ze zdefiniowanym identyfikatorem URI, aby umożliwić odtwarzanie. 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ć onAddMediaItems(). Ten krok jest potrzebny, gdy chcesz obsługiwać kontrolery, które żądają multimediów bez zdefiniowanego identyfikatora URI. Zamiast tego MediaItem zwykle ma ustawione co najmniej 1 z tych pól do opisywania żądanych multimediów:

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

Aby uzyskać więcej opcji dostosowywania do zupełnie nowych playlist, możesz też zastąpić parametr onSetMediaItems(), który pozwala określić pozycję początkową i pozycję playlisty. Możesz na przykład rozwinąć żądany element do całej playlisty i poinstruować odtwarzacz, aby rozpoczynał od indeksu pierwotnie żądanego elementu. Przykładową implementację metody onSetMediaItems() z tą funkcją znajdziesz w aplikacji demonstracyjnej sesji.

Zarządzanie układem niestandardowym i poleceniami niestandardowymi

W poniższych 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 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ępny odtwarzacz i polecenia niestandardowe

Aplikacje multimedialne mogą definiować polecenia niestandardowe, których można używać na przykład w układzie niestandardowym. Można na przykład wdrożyć 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 będą dostępne dla MediaController, gdy połączy się on z sesją multimediów. Jest to możliwe, zastępując ustawienie 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 polecenia z MediaController, zastąp metodę onCustomCommand() w tabeli 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)
      );
    }
    ...
  }
}

Aby prześledzić, który kontroler mediów wysyła żądanie, użyj właściwości packageName obiektu MediaSession.ControllerInfo przekazywanej 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.

Aktualizacja układu niestandardowego po interakcji użytkownika

Po wykonaniu niestandardowego polecenia lub innej interakcji z odtwarzaczem możesz zaktualizować układ wyświetlany w interfejsie kontrolera. Typowym przykładem jest przycisk przełączania, który zmienia swoją ikonę 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));

Dostosowywanie działania poleceń 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.

Identyfikowanie kontrolera żądającego polecenia odtwarzacza

Jeśli wywołanie metody Player pochodzi z metody MediaController, możesz zidentyfikować źródło pochodzenia 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 na urządzeniach z Androidem i innych urządzeniach peryferyjnych, np. przycisk odtwarzania/wstrzymywania na słuchawkach Bluetooth. Media3 obsługuje za Ciebie zdarzenia przycisku multimediów po dotarciu do sesji i wywołuje odpowiednią metodę Player w odtwarzaczu sesji.

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