Odtwarzanie w tle z użyciem usługi MediaSessionService

Często zdarza się, że chcemy odtwarzać multimedia, gdy aplikacja nie jest na pierwszym planie. Na przykład odtwarzacz muzyki zwykle odtwarza muzykę, gdy użytkownik zablokuje urządzenie lub użyje innej aplikacji. Biblioteka Media3 udostępnia serię interfejsów, które umożliwiają obsługę odtwarzania w tle.

Korzystanie z MediaSessionService

Aby włączyć odtwarzanie w tle, musisz umieścić Player i MediaSession w osobnej usłudze. Dzięki temu urządzenie będzie nadal wyświetlać treści multimedialne, nawet gdy aplikacja nie jest na pierwszym planie.

Usługa MediaSessionService umożliwia prowadzenie sesji multimediów niezależnie od aktywności aplikacji.
Ilustracja 1.: MediaSessionService pozwala na uruchamianie sesji multimedialnej niezależnie od działania aplikacji

Podczas hostowania odtwarzacza w ramach usługi należy użyć MediaSessionService. Aby to zrobić, utwórz klasę, która rozszerza klasę MediaSessionService, i w jej ramach utwórz sesję multimediów.

Dzięki MediaSessionService klienci zewnętrzni, np. Asystent Google, systemowe funkcje sterowania multimediami czy urządzenia towarzyszące, takie jak Wear OS, mogą wykrywać Twoją usługę, łączyć się z nią i sterować odtwarzaniem bez dostępu do aktywności interfejsu użytkownika aplikacji. W tym samym czasie do tego samego MediaSessionService może być podłączonych kilka aplikacji klienta, z których każda ma swój własny MediaController.

Wdrażanie cyklu życia usługi

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

  • onCreate() jest wywoływany, gdy pierwszy kontroler ma się połączyć, a usługa jest tworzona i uruchamiana. To najlepsze miejsce do tworzenia PlayerMediaSession.
  • onTaskRemoved(Intent) jest wywoływany, gdy użytkownik zamknie aplikację z ostatnich zadań. Jeśli odtwarzanie jest w toku, aplikacja może zdecydować się na pozostawienie usługi na pierwszym planie. Jeśli odtwarzacz jest wstrzymany, usługa nie jest na pierwszym planie i trzeba ją zatrzymać.
  • Funkcja onDestroy() jest wywoływana, gdy usługa jest zatrzymywana. Wszystkie zasoby, w tym odtwarzacz i sesja, muszą zostać zwolnione.

Kotlin

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

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

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

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

Java

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

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

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

Zamiast kontynuować odtwarzanie w tle, aplikacja może w każdym przypadku zatrzymać usługę, gdy użytkownik ją zamknie:

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

Przyznawanie dostępu do sesji multimediów

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

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

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

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] lifecycle methods omitted

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

Zadeklaruj usługę w pliku manifestu

Aplikacja wymaga uprawnienia do uruchamiania usługi na pierwszym planie. Dodaj do pliku manifestu uprawnienia FOREGROUND_SERVICE, a jeśli kierujesz aplikację na interfejs API 34 lub nowszy, także uprawnienia 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 za pomocą filtra intencji MediaSessionService.

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

Musisz zdefiniować foregroundServiceType, który obejmuje mediaPlayback, gdy aplikacja działa na urządzeniu z Androidem 10 (poziom interfejsu API 29) lub nowszym.

sterować odtwarzaniem za pomocą MediaController,

W aktywności lub fragmencie zawierającym interfejs użytkownika odtwarzacza możesz utworzyć link między interfejsem a sesją multimediów za pomocą elementu MediaController. Twój interfejs użytkownika używa sterownika multimediów, aby wysyłać polecenia z interfejsu do odtwarzacza w trakcie sesji. Szczegółowe informacje o tworzeniu i używaniu MediaController znajdziesz w przewodniku Tworzenie MediaController.

Obsługa poleceń interfejsu

Urządzenie MediaSession otrzymuje polecenia od kontrolera za pomocą interfejsu 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 dla Ciebie 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 multimediów i wyświetla elementy sterujące odtwarzaniem. MediaNotification jest świadomy Twojej sesji i może służyć do sterowania odtwarzaniem w innych aplikacjach, które są połączone z tą samą sesją.

Na przykład aplikacja do strumieniowego odtwarzania muzyki korzystająca z elementu MediaSessionService utworzy element 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 elementu MediaSession.

Wymagane metadane można podać w multimediów lub zadeklarować jako część elementu multimediów, 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();

Aplikacje mogą dostosowywać przyciski poleceń w Android Media controls. Więcej informacji o dostosowywaniu elementów sterujących multimediami w Androidzie

Personalizacja powiadomień

Aby dostosować powiadomienie, utwórz MediaNotification.Provider za pomocą funkcji DefaultMediaNotificationProvider.Builder lub stwórz niestandardową implementację interfejsu dostawcy. Dodaj dostawcę do MediaSessionService za pomocą setMediaNotificationProvider.

Wznowienie odtwarzania

Przyciski multimedialne to przyciski sprzętowe dostępne na urządzeniach z Androidem i innych urządzeniach peryferyjnych, np. przycisk odtwarzania lub pauzy na słuchawkach Bluetooth. Gdy usługa jest uruchomiona, Media3 obsługuje dla Ciebie przyciski multimedialne.

Zadeklaruj odbiornik przycisku multimediów Media3

Media3 zawiera interfejs API, który umożliwia użytkownikom wznowienie odtwarzania po zakończeniu działania aplikacji, a nawet po jej ponownym uruchomieniu. Domyślnie wznawianie odtwarzania jest wyłączone. Oznacza to, że użytkownik nie może wznowić odtwarzania, gdy usługa nie działa. Aby wyrazić zgodę, zacznij od zadeklarowania elementu 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>

Wdrażanie wywołania zwrotnego wznowienia odtwarzania

Gdy odtwarzanie zostanie wznowione przez urządzenie Bluetooth lub funkcję wznawiania w interfejsie użytkownika Androida, wywoływana jest metoda wywołania zwrotnego onPlaybackResumption().

Kotlin

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    val resumptionPlaylist = restorePlaylist()
    settable.set(resumptionPlaylist)
  }
  return settable
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession,
    ControllerInfo controller
) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(() -> {
    // Your app is responsible for storing the playlist 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 czy losowy, onPlaybackResumption() to dobre miejsce na skonfigurowanie odtwarzacza za pomocą tych parametrów, zanim Media3 przygotuje odtwarzacz i rozpocznie odtwarzanie po zakończeniu wywołania zwrotnego.

Zaawansowana konfiguracja kontrolera i wsteczna zgodność

Typowym scenariuszem jest użycie MediaController w interfejsie aplikacji do sterowania odtwarzaniem i wyświetlania playlisty. Jednocześnie sesja jest dostępna dla klientów zewnętrznych, takich jak elementy sterujące multimediami w Androidzie i Asystent na urządzeniach mobilnych lub telewizorach, Wear OS na zegarkach oraz Android Auto w samochodach. Aplikacja demonstracyjna sesji Media3 to przykład aplikacji, która realizuje taki scenariusz.

Klienci zewnętrzni mogą używać interfejsów API, takich jak MediaControllerCompat starszej biblioteki AndroidX lub android.media.session.MediaController w ramach frameworku Androida. Media3 jest w pełni zgodny wstecznie z poprzednią biblioteką i zapewnia interoperacyjność z interfejsem API Androida.

Korzystanie z sterownika powiadomień o multimediach

Pamiętaj, że te starsze lub ramowe kontrolery odczytują te same wartości z ramowych elementów PlaybackState.getActions()PlaybackState.getCustomActions(). Aby określić działania i działania niestandardowe sesji frameworku, aplikacja może użyć kontrolera powiadomień o mediach i ustawić dostępne polecenia oraz układ niestandardowy. Usługa łączy kontroler powiadomień multimedialnych z Twoją sesją, a sesja używa parametru ConnectionResult zwracanego przez funkcję onConnect(), aby skonfigurować działania i działania niestandardowe sesji frameworku.

W przypadku scenariusza tylko na urządzeniach mobilnych aplikacja może udostępnić implementację MediaSession.Callback.onConnect(), aby ustawić dostępne polecenia i schemat dostosowania do sesji frameworku w ten sposób:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

Autoryzowanie Androida Auto do wysyłania niestandardowych poleceń

Jeśli używasz sterownika MediaLibraryService i chcesz obsługiwać Androida Auto w aplikacji mobilnej, sterownik Androida Auto wymaga odpowiednich dostępnych poleceń, w przeciwnym razie Media3 odrzuci przychodzące polecenia niestandardowe z tego sterownika:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS
          .buildUpon()
          .add(customCommandSeekBackward)
          .add(customCommandSeekForward)
          .build();
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

Aplikacja demonstracyjna sesji zawiera moduł motoryzacyjny, który demonstruje obsługę systemu operacyjnego Automotive, który wymaga osobnego pliku APK.