Фоновое воспроизведение с помощью 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()
  }
}

Ява

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

Ява

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

Ява

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

В действии или фрагменте, содержащем пользовательский интерфейс вашего проигрывателя, вы можете установить связь между пользовательским интерфейсом и вашим медиа-сеансом с помощью 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()

Ява

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

Ява

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

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

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

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

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

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

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

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

Котлин

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 button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Ява

@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 button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .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(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 for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Ява

@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 for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

В демонстрационном приложении сеанса есть автомобильный модуль , демонстрирующий поддержку автомобильной ОС, для которой требуется отдельный APK.