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

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

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

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

MediaSessionService позволяет запускать сеанс воспроизведения отдельно от активности приложения.
Рисунок 1. MediaSessionService позволяет медиасеансу выполняться отдельно от действия приложения.

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

Использование MediaSessionService позволяет внешним клиентам, таким как Google Ассистент, системные элементы управления мультимедиа, кнопки мультимедиа на периферийных устройствах или дополнительные устройства, например Wear OS, обнаруживать ваш сервис, подключаться к нему и управлять воспроизведением, не обращаясь к пользовательскому интерфейсу вашего приложения. К одному MediaSessionService может быть одновременно подключено несколько клиентских приложений, каждое со своим MediaController.

Реализация жизненного цикла сервиса

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

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

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

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

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

Kotlin

@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(), чтобы предоставить другим клиентам доступ к сеансу воспроизведения, созданному вместе с сервисом.

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

Объявите сервис в манифесте

Чтобы запустить активную службу воспроизведения, приложению требуются разрешения 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

MediaSession получает команды от контроллера через MediaSession.Callback. При инициализации MediaSession создается реализация MediaSession.Callback по умолчанию, которая автоматически обрабатывает все команды, отправляемые MediaController в проигрыватель.

Уведомление

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

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

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

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

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

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

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

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

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

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

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

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

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

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

После завершения работы MediaSessionService и даже после перезагрузки устройства можно предложить пользователям возобновить воспроизведение с того места, где они остановились. По умолчанию эта функция отключена. Это означает, что пользователь не может возобновить воспроизведение, если сервис не запущен. Чтобы включить эту функцию, необходимо объявить получателя кнопки мультимедиа и реализовать метод onPlaybackResumption.

Объявите получатель медиакнопки 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().

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

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

Этот метод вызывается во время загрузки, чтобы создать уведомление о возобновлении работы системного интерфейса Android после перезагрузки устройства, если для isForPlayback задано значение false. Для расширенного уведомления рекомендуется заполнить поля MediaMetadata, такие как title и artworkData или artworkUri текущего элемента, локально доступными значениями, поскольку доступ к сети может быть ещё невозможен. Вы также можете добавить в MediaMetadata.extras параметры MediaConstants.EXTRAS_KEY_COMPLETION_STATUS и MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE, чтобы указать позицию, с которой нужно возобновить воспроизведение.

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

Обычно для управления воспроизведением и показа плейлиста в интерфейсе приложения используется MediaController. В то же время сеанс доступен внешним клиентам, таким как элементы управления мультимедиа Android и Ассистент на мобильном устройстве или телевизоре, Wear OS для часов и Android Auto в автомобилях. Пример приложения, в котором реализован такой сценарий, – демонстрационное приложение для сеансов Media3.

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

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

Любое приложение может попытаться подключиться к вашему сеансу или библиотеке мультимедиа. Если вы хотите ограничить доступ к системным контроллерам, контроллерам с разрешением на управление медиаконтентом и вашему приложению, используйте ControllerInfo.isTrusted() для базовой проверки доступа. Вы также можете указать более конкретные контроллеры, например контроллер уведомлений мультимедиа или контроллеры Android Auto, как описано в следующих разделах.

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

Важно понимать, что эти устаревшие контроллеры и контроллеры платформы имеют одно и то же состояние, а видимость нельзя настроить по контроллеру (например, доступные PlaybackState.getActions() и PlaybackState.getCustomActions()). Вы можете использовать контроллер уведомлений мультимедиа, чтобы настроить состояние, заданное в сеансе мультимедиа платформы, для совместимости с этими устаревшими контроллерами и контроллерами платформы.

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

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

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

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

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

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