Odtwarzanie w tle z użyciem usługi MediaSessionService

Często pożądane jest odtwarzanie multimediów, gdy aplikacja nie jest na pierwszym planie. Na przykład odtwarzacz muzyki zwykle odtwarza muzykę, gdy użytkownik zablokuje urządzenie lub korzysta z innej aplikacji. Biblioteka Media3 udostępnia serię interfejsów, które umożliwiają obsługę odtwarzania w tle.

Używanie MediaSessionService

Aby włączyć odtwarzanie w tle, musisz umieścić Player i MediaSession w osobnej usłudze. Dzięki temu urządzenie może nadal odtwarzać multimedia, nawet gdy aplikacja nie jest na pierwszym planie.

MediaSessionService umożliwia uruchamianie sesji multimedialnej niezależnie od aktywności aplikacji.
Ilustracja 1. MediaSessionService umożliwia uruchamianie sesji multimedialnej niezależnie od aktywności aplikacji

Jeśli odtwarzacz jest hostowany w Usłudze, należy użyć MediaSessionService. Aby to zrobić, utwórz klasę, która rozszerza MediaSessionService, i utwórz w niej sesję multimedialną.

Użycie MediaSessionService umożliwia klientom zewnętrznym, takim jak Asystent Google, systemowe opcje sterowania multimediami, przyciski multimedialne na urządzeniach peryferyjnych lub urządzenia towarzyszące, np. Wear OS, wykrywanie Twojej usługi, łączenie się z nią i sterowanie odtwarzaniem bez konieczności uzyskiwania dostępu do aktywności interfejsu aplikacji. W rzeczywistości z jednym MediaSessionService może być połączonych wiele aplikacji klienckich jednocześnie, a każda z nich ma własny MediaController.

Wdrażanie cyklu życia usługi

Musisz zaimplementować 2 metody cyklu życia usługi:

  • Metoda onCreate() jest wywoływana, gdy pierwszy kontroler ma się połączyć, a usługa jest tworzona i uruchamiana. To najlepsze miejsce do budowania Player i MediaSession.
  • Metoda onDestroy() jest wywoływana, gdy usługa jest zatrzymywana. We wszystkich zasobach, w tym w odtwarzaczu i sesji, należy zwolnić pamięć.

Możesz opcjonalnie zastąpić metodę onTaskRemoved(Intent), aby dostosować, co się stanie, gdy użytkownik zamknie aplikację z poziomu ostatnich zadań. Domyślnie usługa działa, jeśli odtwarzanie jest w toku, a w przeciwnym razie jest zatrzymywana.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;

  // Create your Player and MediaSession in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaSession = new MediaSession.Builder(this, player).build();
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Zamiast kontynuować odtwarzanie w tle, możesz zatrzymać usługę, gdy użytkownik zamknie aplikację:

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

W przypadku każdej innej ręcznej implementacji onTaskRemoved możesz użyć isPlaybackOngoing(), aby sprawdzić, czy odtwarzanie jest uznawane za trwające i czy usługa na pierwszym planie została uruchomiona.

Zapewnianie dostępu do sesji multimedialnej

Zastąp metodę onGetSession(), aby umożliwić innym klientom dostęp do sesji multimedialnej utworzonej podczas tworzenia usługi.

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Deklarowanie usługi w pliku manifestu

Aby uruchomić usługę odtwarzania na pierwszym planie, aplikacja wymaga uprawnień FOREGROUND_SERVICE i FOREGROUND_SERVICE_MEDIA_PLAYBACK:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

Musisz też zadeklarować klasę Service w pliku manifestu z filtrem intencji MediaSessionService i foregroundServiceType, który zawiera mediaPlayback.

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

Sterowanie odtwarzaniem za pomocą MediaController

W aktywności lub fragmencie zawierającym interfejs odtwarzacza możesz utworzyć połączenie między interfejsem a sesją multimedialną za pomocą MediaController. Interfejs użytkownika używa kontrolera multimediów do wysyłania poleceń z interfejsu do odtwarzacza w ramach sesji. Szczegółowe informacje o tworzeniu i używaniu MediaController znajdziesz w przewodniku Tworzenie MediaController.

Obsługa poleceń MediaController

MediaSession odbiera polecenia z kontrolera przez MediaSession.Callback. Inicjowanie MediaSession tworzy domyślną implementację MediaSession.Callback, która automatycznie obsługuje wszystkie polecenia wysyłane przez MediaController do odtwarzacza.

Powiadomienie

MediaSessionService automatycznie tworzy MediaNotification, który powinien działać w większości przypadków. Domyślnie opublikowane powiadomienie to powiadomienie MediaStyle, które jest aktualizowane o najnowsze informacje z sesji multimedialnej i wyświetla elementy sterujące odtwarzaniem. MediaNotification wie o Twojej sesji i może służyć do sterowania odtwarzaniem w innych aplikacjach połączonych z tą samą sesją.

Na przykład aplikacja do odtwarzania strumieniowego muzyki, która używa MediaSessionService, utworzy MediaNotification, który wyświetla tytuł, wykonawcę i okładkę albumu dla aktualnie odtwarzanego elementu multimedialnego wraz z elementami sterującymi odtwarzaniem na podstawie konfiguracji MediaSession.

Wymagane metadane można podać w multimediach lub zadeklarować jako część elementu multimedialnego, jak w tym fragmencie kodu:

Kotlin

val mediaItem =
  MediaItem.Builder()
    .setMediaId("media-1")
    .setUri(mediaUri)
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setArtist("David Bowie")
        .setTitle("Heroes")
        .setArtworkUri(artworkUri)
        .build()
    )
    .build()

mediaController.setMediaItem(mediaItem)
mediaController.prepare()
mediaController.play()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaId("media-1")
        .setUri(mediaUri)
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setArtist("David Bowie")
                .setTitle("Heroes")
                .setArtworkUri(artworkUri)
                .build())
        .build();

mediaController.setMediaItem(mediaItem);
mediaController.prepare();
mediaController.play();

Cykl życia powiadomień

Powiadomienie jest tworzone, gdy Player ma MediaItem instancje na playliście.

Wszystkie aktualizacje powiadomień są generowane automatycznie na podstawie stanu Player i MediaSession.

Nie można usunąć powiadomienia, gdy usługa na pierwszym planie jest uruchomiona. Aby natychmiast usunąć powiadomienie, musisz wywołać Player.release() lub wyczyścić playlistę za pomocą Player.clearMediaItems().

Jeśli odtwarzacz zostanie wstrzymany, zatrzymany lub ulegnie awarii na dłużej niż 10 minut bez dalszych interakcji użytkownika, usługa zostanie automatycznie wyłączona ze stanu usługi na pierwszym planie, aby system mógł ją usunąć. Możesz wdrożyć wznawianie odtwarzania, aby umożliwić użytkownikowi ponowne uruchomienie cyklu życia usługi i wznowienie odtwarzania w późniejszym czasie.

Dostosowywanie powiadomień

Metadane dotyczące aktualnie odtwarzanego elementu można dostosować, modyfikując MediaItem.MediaMetadata. Jeśli chcesz zaktualizować metadane istniejącego elementu, możesz użyć Player.replaceMediaItem, aby zaktualizować metadane bez przerywania odtwarzania.

Możesz też dostosować niektóre przyciski wyświetlane w powiadomieniu, ustawiając niestandardowe preferencje przycisków multimedialnych w opcjach sterowania multimediami na Androidzie. Więcej informacji o dostosowywaniu opcji sterowania multimediami na Androidzie

Aby dodatkowo dostosować powiadomienie, utwórz MediaNotification.Provider za pomocą DefaultMediaNotificationProvider.Builder lub niestandardowej implementacji interfejsu dostawcy. Dodaj dostawcę do MediaSessionService za pomocą setMediaNotificationProvider.

Wznowienie odtwarzania

Po zakończeniu działania usługi MediaSessionService, a nawet po ponownym uruchomieniu urządzenia, można zaoferować wznowienie odtwarzania, aby użytkownicy mogli ponownie uruchomić usługę i wznowić odtwarzanie od miejsca, w którym zostało przerwane. Domyślnie wznowienie odtwarzania jest wyłączone. Oznacza to, że użytkownik nie może wznowić odtwarzania, gdy usługa nie jest uruchomiona. Aby włączyć tę funkcję, musisz zadeklarować odbiornik przycisku multimediów i wdrożyć metodę onPlaybackResumption.

Deklarowanie odbiornika przycisków multimedialnych Media3

Zacznij od zadeklarowania MediaButtonReceiver w pliku manifestu:

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

Implementowanie wywołania zwrotnego wznowienia odtwarzania

Gdy urządzenie Bluetooth lub funkcja wznawiania interfejsu systemu Android zażąda wznowienia odtwarzania, wywoływana jest metoda wywołania zwrotnego onPlaybackResumption().

Kotlin

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(
      () -> {
        // Your app is responsible for storing the playlist, metadata (like title
        // and artwork) of the current item and the start position to use here.
        MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
        settableFuture.set(resumptionPlaylist);
      },
      MoreExecutors.directExecutor());
  return settableFuture;
}

Jeśli masz zapisane inne parametry, takie jak szybkość odtwarzania, tryb powtarzania lub tryb odtwarzania losowego, onPlaybackResumption() to dobre miejsce na skonfigurowanie odtwarzacza z tymi parametrami, zanim Media3 przygotuje odtwarzacz i rozpocznie odtwarzanie po zakończeniu wywołania zwrotnego.

Ta metoda jest wywoływana podczas uruchamiania, aby utworzyć powiadomienie o wznowieniu interfejsu systemu Android po ponownym uruchomieniu urządzenia z ustawieniem isForPlayback ustawionym na false. W przypadku powiadomienia rozszerzonego zalecamy wypełnienie pól MediaMetadata, takich jak title i artworkData lub artworkUri, bieżącego elementu wartościami dostępnymi lokalnie, ponieważ dostęp do sieci może nie być jeszcze możliwy. Możesz też dodać MediaConstants.EXTRAS_KEY_COMPLETION_STATUS i MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE do MediaMetadata.extras, aby wskazać pozycję wznowienia odtwarzania.

Zaawansowana konfiguracja kontrolera i zgodność wsteczna

Typowym scenariuszem jest użycie elementu MediaController w interfejsie aplikacji do sterowania odtwarzaniem i wyświetlania playlisty. Jednocześnie sesja jest udostępniana klientom zewnętrznym, takim jak opcje sterowania multimediami na Androidzie i Asystent na urządzeniach mobilnych lub telewizorach, Wear OS na zegarkach i Android Auto w samochodach. Przykładem aplikacji, która implementuje taki scenariusz, jest aplikacja demonstracyjna sesji Media3.

Te zewnętrzne klienty mogą używać interfejsów API takich jak MediaControllerCompat starszej biblioteki AndroidX lub android.media.session.MediaController platformy Androida. Media3 jest w pełni zgodna wstecznie ze starszą biblioteką i zapewnia interoperacyjność z interfejsem API platformy Androida.

Identyfikowanie zaufanych kontrolerów

Każda aplikacja może próbować połączyć się z Twoją sesją lub biblioteką multimediów. Jeśli chcesz ograniczyć dostęp do kontrolerów systemowych, kontrolerów z uprawnieniami do sterowania treściami multimedialnymi i własnej aplikacji, możesz użyć ControllerInfo.isTrusted() do podstawowego sprawdzania dostępu. Możesz też określić bardziej szczegółowe kontrolery, takie jak kontroler powiadomień multimedialnych lub kontrolery Androida Auto, jak opisano w sekcjach poniżej.

Korzystanie z kontrolera powiadomień o multimediach

Ważne jest, aby pamiętać, że te starsze kontrolery i kontrolery platformy mają ten sam stan, a widoczności nie można dostosować według kontrolera (np. dostępnych PlaybackState.getActions() i PlaybackState.getCustomActions()). Możesz użyć kontrolera powiadomień multimedialnych, aby skonfigurować stan ustawiony w sesji multimedialnej platformy pod kątem zgodności z tymi starszymi kontrolerami i kontrolerami platformy.

Na przykład aplikacja może udostępniać implementację MediaSession.Callback.onConnect(), aby ustawić dostępne polecenia i ustawienia przycisków multimedialnych specjalnie dla sesji platformy w ten sposób:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    val playerCommands =
      ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
        .remove(COMMAND_SEEK_TO_PREVIOUS)
        .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
        .remove(COMMAND_SEEK_TO_NEXT)
        .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
        .build()
    // Custom button preferences and commands to configure the platform session.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    Player.Commands playerCommands =
        ConnectionResult.DEFAULT_PLAYER_COMMANDS
            .buildUpon()
            .remove(COMMAND_SEEK_TO_PREVIOUS)
            .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .remove(COMMAND_SEEK_TO_NEXT)
            .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .build();
    // Custom button preferences and commands to configure the platform session.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

Autoryzowanie Androida Auto do wysyłania poleceń niestandardowych

Gdy używasz MediaLibraryService i chcesz obsługiwać Androida Auto za pomocą aplikacji mobilnej, kontroler Androida Auto wymaga odpowiednich dostępnych poleceń. W przeciwnym razie Media3 odrzuci przychodzące polecenia niestandardowe z tego kontrolera:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

Aplikacja demonstracyjna sesji ma moduł samochodowy, który pokazuje obsługę systemu operacyjnego Automotive OS wymagającego osobnego pakietu APK.