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

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

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

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

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

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

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

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

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

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

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

Котлин

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

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

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

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

Котлин

override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

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

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

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

Котлин

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

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

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

Уведомление

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

Например, приложение для потоковой передачи музыки, использующее 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 .

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

После завершения работы 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() .

Котлин

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo,
    isForPlayback: Boolean,
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // 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()
    settable.set(resumptionPlaylist)
  }
  return settable
}

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 текущего элемента, локально доступными значениями, поскольку доступ к сети может быть еще недоступен. Вы также можете добавить MediaConstants.EXTRAS_KEY_COMPLETION_STATUS и MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE в MediaMetadata.extras , чтобы указать позицию возобновления воспроизведения.

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

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

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

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

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

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

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

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

Котлин

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): 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 AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        listOf(seekBackButton, seekForwardButton)
      )
      .setAvailablePlayerCommands(playerCommands)
      .build()
  }
  // Default commands with default button preferences for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    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 new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build();
  }
  // Default commands with default button preferences for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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

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

Котлин

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommand)
      .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 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(customCommand)
          .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 for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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