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

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

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

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

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

При размещении плеера внутри Service следует использовать 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

В Activity или Fragment, содержащем ваш пользовательский интерфейс проигрывателя, вы можете установить связь между пользовательским интерфейсом и вашим сеансом мультимедиа с помощью MediaController . Ваш пользовательский интерфейс использует медиаконтроллер для отправки команд из вашего пользовательского интерфейса проигрывателю в сеансе. Подробную информацию о создании и использовании MediaController см. в руководстве MediaController a 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, metadata (like title
    // and artwork) of the current item 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, 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 после перезагрузки устройства. Для расширенного уведомления рекомендуется заполнить поля MediaMetadata , такие как title и artworkData или artworkUri текущего элемента, локально доступными значениями, поскольку сетевой доступ может быть еще недоступен. Вы также можете добавить MediaConstants.EXTRAS_KEY_COMPLETION_STATUS и MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE в MediaMetadata.extras , чтобы указать позицию возобновления воспроизведения.

Расширенная конфигурация контроллера и обратная совместимость

Распространенным сценарием является использование 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.