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

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

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

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

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

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

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

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

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

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

Котлин

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

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

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

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

  // 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?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Ява

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

Предоставить доступ к медиа-сессии

Переопределите метод 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 , а если вы ориентируетесь на API 34 и выше, также FOREGROUND_SERVICE_MEDIA_PLAYBACK :

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

Вы также должны объявить свой класс Service в манифесте с помощью фильтра намерений MediaSessionService .

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
    </intent-filter>
</service>

Вы должны определить foregroundServiceType , который включает mediaPlayback , когда ваше приложение работает на устройстве с Android 10 (уровень API 29) и выше.

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

Приложения могут настраивать кнопки управления Android Media. Узнайте больше о настройке элементов управления Android Media .

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

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

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

Мультимедийные кнопки — это аппаратные кнопки, имеющиеся на устройствах Android и других периферийных устройствах, например кнопка воспроизведения или паузы на гарнитуре Bluetooth. Media3 обрабатывает ввод мультимедийных кнопок во время работы службы.

Объявите приемник медиа-кнопок Media3

Media3 включает API, позволяющий пользователям возобновить воспроизведение после закрытия приложения и даже после перезапуска устройства. По умолчанию возобновление воспроизведения отключено. Это означает, что пользователь не может возобновить воспроизведение, если ваша служба не запущена. Чтобы принять участие, начните с объявления 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() . Чтобы определить действия и настраиваемые действия сеанса платформы, приложение может использовать контроллер мультимедийных уведомлений и установить доступные команды и настраиваемый макет. Служба подключает контроллер мультимедийных уведомлений к вашему сеансу, и сеанс использует ConnectionResult , возвращаемый функцией onConnect() вашего обратного вызова, для настройки действий и настраиваемых действий сеанса платформы.

В сценарии только для мобильных устройств приложение может предоставить реализацию 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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout 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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout 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 with default custom layout 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 without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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