Фоновое воспроизведение с помощью MediaSessionService

Часто бывает желательно воспроизводить медиафайлы, когда приложение не находится на переднем плане. Например, музыкальный плеер обычно продолжает воспроизводить музыку, когда пользователь заблокировал устройство или использует другое приложение. Библиотека Media3 предоставляет ряд интерфейсов, позволяющих поддерживать фоновое воспроизведение.

Используйте MediaSessionService

Для включения фонового воспроизведения необходимо поместить Player и MediaSession в отдельный Service . Это позволит устройству продолжать воспроизведение медиафайлов, даже если ваше приложение не находится на переднем плане.

Сервис MediaSessionService позволяет запускать медиасессию отдельно от активности приложения.
Рисунок 1 : MediaSessionService позволяет медиасессии работать отдельно от активности приложения.

При размещении проигрывателя внутри сервиса следует использовать MediaSessionService . Для этого создайте класс, наследующий MediaSessionService , и создайте медиасессию внутри него.

Using MediaSessionService makes it possible for external clients like Google Assistant, system media controls, media buttons on peripheral devices, or companion devices like Wear OS to discover your service, connect to it, and control playback, all without accessing your app's UI activity at all. In fact, there can be multiple client apps connected to the same MediaSessionService at the same time, each app with its own MediaController .

Реализуйте жизненный цикл сервиса.

Вам необходимо реализовать два метода жизненного цикла вашего сервиса:

  • onCreate() вызывается, когда первый контроллер собирается подключиться, а служба создается и запускается. Это лучшее место для создания Player и MediaSession .
  • onDestroy() вызывается при остановке сервиса. Необходимо освободить все ресурсы, включая плеер и сессию.

При желании вы можете переопределить метод onTaskRemoved(Intent) , чтобы настроить действия, происходящие при закрытии приложения из списка последних задач. По умолчанию служба продолжает работать, если воспроизведение продолжается, и останавливается в противном случае.

Котлин

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

В качестве альтернативы непрерывному воспроизведению в фоновом режиме вы можете остановить службу в любом случае, когда пользователь закроет приложение:

Котлин

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

Java

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

Для любой другой реализации метода onTaskRemoved вручную можно использовать isPlaybackOngoing() чтобы проверить, считается ли воспроизведение продолжающимся и запущена ли служба переднего плана.

Предоставьте доступ к пресс-конференции.

Переопределите метод onGetSession() , чтобы предоставить другим клиентам доступ к вашей медиа-сессии, которая была создана при создании службы.

Котлин

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

Укажите сервис в манифесте.

Для запуска службы воспроизведения в фоновом режиме приложению требуются разрешения FOREGROUND_SERVICE и FOREGROUND_SERVICE_MEDIA_PLAYBACK :

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

Также необходимо объявить класс Service в манифесте с фильтром намерений MediaSessionService и типом foregroundServiceType , включающим 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>

Управляйте воспроизведением с помощью MediaController

В Activity или Fragment, содержащем пользовательский интерфейс вашего проигрывателя, вы можете установить связь между интерфейсом и вашей медиасессией с помощью MediaController . Ваш интерфейс использует медиаконтроллер для отправки команд из интерфейса проигрывателю в рамках сессии. Подробную информацию о создании и использовании MediaController см. в руководстве MediaController Создание MediaController MediaController .

Обработка команд MediaController

Объект MediaSession получает команды от контроллера через свой MediaSession.Callback . Инициализация объекта MediaSession создает реализацию метода MediaSession.Callback по умолчанию, которая автоматически обрабатывает все команды, отправляемые MediaController вашему плееру.

Уведомление

A MediaSessionService automatically creates a MediaNotification for you that should work in most cases. By default, the published notification is a MediaStyle notification that stays updated with the latest information from your media session and displays playback controls. The MediaNotification is aware of your session and can be used to control playback for any other apps that are connected to the same session.

Например, приложение для потоковой передачи музыки, использующее MediaSessionService , создаст MediaNotification , отображающее название, исполнителя и обложку альбома воспроизводимого медиафайла, а также элементы управления воспроизведением в соответствии с конфигурацией MediaSession .

Необходимые метаданные могут быть предоставлены непосредственно в медиафайле или указаны как часть медиаконтента, как показано в следующем фрагменте:

Котлин

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

Жизненный цикл уведомлений

Уведомление создается, как только в плейлисте Player появляются экземпляры MediaItem .

Все уведомления обновляются автоматически в зависимости от состояния Player и MediaSession .

Уведомление нельзя удалить, пока работает фоновая служба. Чтобы немедленно удалить уведомление, необходимо вызвать метод Player.release() или очистить плейлист с помощью Player.clearMediaItems() .

Если воспроизведение приостановлено, остановлено или завершилось с ошибкой более чем на 10 минут без дальнейшего взаимодействия с пользователем, служба автоматически переходит из состояния активной службы, чтобы быть уничтоженной системой. Вы можете реализовать возобновление воспроизведения , чтобы позволить пользователю перезапустить жизненный цикл службы и возобновить воспроизведение позже.

Настройка уведомлений

Метаданные воспроизводимого элемента можно настроить, изменив значение параметра MediaItem.MediaMetadata . Если вы хотите обновить метаданные существующего элемента, вы можете использовать Player.replaceMediaItem для обновления метаданных без прерывания воспроизведения.

Вы также можете настроить некоторые кнопки, отображаемые в уведомлении, задав пользовательские параметры кнопок управления мультимедиа в Android Media. Подробнее о настройке элементов управления мультимедиа в Android Media можно прочитать здесь .

Для дальнейшей настройки самого уведомления создайте MediaNotification.Provider с помощью DefaultMediaNotificationProvider.Builder или создайте собственную реализацию интерфейса поставщика. Добавьте свой поставщик в MediaSessionService с помощью setMediaNotificationProvider .

Возобновление воспроизведения

After the MediaSessionService has been terminated, and even after the device has been rebooted, it is possible to offer playback resumption to let users restart the service and resume playback where they left off. By default, playback resumption is turned off. This means the user can't resume playback when your service isn't running. To opt-in to this feature, you need to declare a media button receiver and implement the onPlaybackResumption method.

Объявите приемник кнопки мультимедиа Media3.

Для начала объявите компонент MediaButtonReceiver в вашем манифесте:

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

Реализовать функцию обратного вызова для возобновления воспроизведения.

Когда возобновление воспроизведения запрашивается устройством Bluetooth или функцией возобновления воспроизведения в пользовательском интерфейсе Android, вызывается метод обратного вызова onPlaybackResumption() .

Котлин

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

Если вы сохранили другие параметры, такие как скорость воспроизведения, режим повтора или режим перемешивания, то onPlaybackResumption() — хорошее место для настройки плеера с этими параметрами перед тем, как Media3 подготовит плеер и начнет воспроизведение после завершения обратного вызова.

This method is called during boot time to create the Android System UI resumption notification after a reboot of the device with isForPlayback set to false . For a rich notification, it is recommended to fill in MediaMetadata fields like title and artworkData or artworkUri of the current item with locally available values, as network access may not yet be available. You can also add MediaConstants.EXTRAS_KEY_COMPLETION_STATUS and MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE to the MediaMetadata.extras to indicate the resumption playback position.

Расширенные возможности настройки контроллера и обратная совместимость.

Распространенный сценарий — использование MediaController в пользовательском интерфейсе приложения для управления воспроизведением и отображения плейлиста. При этом сессия доступна внешним клиентам, таким как элементы управления мультимедиа Android и Google Assistant на мобильных устройствах или телевизорах, Wear OS для часов и Android Auto в автомобилях. Демонстрационное приложение Media3 Session является примером приложения, реализующего подобный сценарий.

Эти внешние клиенты могут использовать API, такие как MediaControllerCompat из устаревшей библиотеки AndroidX или android.media.session.MediaController платформы Android. Media3 полностью обратно совместима с устаревшей библиотекой и обеспечивает взаимодействие с API платформы Android.

Определите доверенных контроллеров

Any app can attempt to connect to your media session or library. If you want to restrict access to system controllers, controllers with media content control permission, and your own app, you can use ControllerInfo.isTrusted() for a basic access check. Alternatively, you can identify more specific controllers like the media notification controller or Android Auto controllers as described in the following sections.

Используйте контроллер уведомлений о медиаконтенте.

It's important to understand that these legacy and platform controllers share the same state and visibility can't be customized by controller (for example the available PlaybackState.getActions() and PlaybackState.getCustomActions() ). You can can use the media notification controller to configure the state set in the platform media session for compatibility to these legacy and platform controllers.

Например, приложение может предоставить реализацию метода MediaSession.Callback.onConnect() для установки доступных команд и настроек кнопок мультимедиа специально для сессии платформы следующим образом:

Котлин

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

Разрешите Android Auto отправлять пользовательские команды.

При использовании MediaLibraryService и для поддержки Android Auto в мобильном приложении контроллеру Android Auto требуются соответствующие доступные команды, иначе Media3 будет отклонять входящие пользовательские команды от этого контроллера:

Котлин

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

Демонстрационное приложение содержит автомобильный модуль , демонстрирующий поддержку Automotive OS, для которой требуется отдельный APK-файл.