Как предоставлять контент с помощью MediaLibraryService

Мультимедийные приложения часто содержат коллекции мультимедийных объектов, организованные в иерархию. Например, это могут быть треки в альбоме или серии телешоу в плейлисте. Такая иерархия мультимедийных объектов называется медиатекой.

Примеры медиаконтента, упорядоченного по иерархии
Рисунок 1. Примеры иерархий мультимедийных объектов, формирующих медиатеку.

MediaLibraryService предоставляет стандартизированный API для показа медиатеки и доступа к ней. Это может быть полезно, например, при добавлении поддержки Android Auto в мультимедийное приложение, которое предоставляет собственный безопасный для водителя интерфейс для вашей медиатеки.

Создать MediaLibraryService

Реализация MediaLibraryService похожа на реализацию MediaSessionService, за исключением того, что в методе onGetSession() вы должны возвращать MediaLibrarySession вместо MediaSession.

Kotlin

class PlaybackService : MediaLibraryService() {
  private var mediaLibrarySession: MediaLibrarySession? = null
  private val callback: MediaLibrarySession.Callback =
    object : MediaLibrarySession.Callback {
      /* ... */
    }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
    // If desired, validate the controller before returning the media library session
    return mediaLibrarySession
  }

  // Create your player and media library session in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaLibrarySession = MediaLibrarySession.Builder(this, player, callback).build()
  }

  // Remember to release the player and media library session in onDestroy
  override fun onDestroy() {
    mediaLibrarySession?.run {
      player.release()
      release()
      mediaLibrarySession = null
    }
    super.onDestroy()
  }
}

Java

class PlaybackService extends MediaLibraryService {
  MediaLibrarySession mediaLibrarySession = null;
  MediaLibrarySession.Callback callback = new MediaLibrarySession.Callback() {
        /* ... */
      };

  @Override
  public MediaLibrarySession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    // If desired, validate the controller before returning the media library session
    return mediaLibrarySession;
  }

  // Create your player and media library session in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaLibrarySession = new MediaLibrarySession.Builder(this, player, callback).build();
  }

  // Remember to release the player and media library session in onDestroy
  @Override
  public void onDestroy() {
    if (mediaLibrarySession != null) {
      mediaLibrarySession.getPlayer().release();
      mediaLibrarySession.release();
      mediaLibrarySession = null;
    }
    super.onDestroy();
  }
}
Не забудьте также объявить Service и необходимые разрешения в файле манифеста:

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

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

Рекомендуется зарегистрировать ваш сервис как платформу и интерфейс сервиса Media3:

  • Для совместимости с клиентами, использующими API сеансов мультимедиа платформы, рекомендуется включить <action android:name="android.media.browse.MediaBrowserService"/> в элемент intent-filter. Media3 автоматически обеспечивает обратную совместимость с этим интерфейсом сервиса.

  • Чтобы приложения, использующие Media3, могли взаимодействовать с вашим сервисом через Media3 API, вместе с платформой необходимо указать <action android:name="androidx.media3.session.MediaLibraryService"/>.

Это позволяет приложениям находить ваш сервис через PackageManager и подключаться к MediaBrowser через любой из этих интерфейсов.

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

MediaLibraryService API ожидает, что медиатека будет иметь древовидную структуру с одним корневым узлом и дочерними узлами, которые могут быть воспроизводимыми или доступными для просмотра.

MediaLibrarySession расширяет MediaSession API, добавляя API для просмотра контента. По сравнению с обратным вызовом MediaSession обратный вызов MediaLibrarySession добавляет такие методы, как:

  • onGetLibraryRoot() – когда клиент запрашивает корневой каталог MediaItem дерева контента.
  • onGetChildren() – когда клиент запрашивает дочерние элементы MediaItem в дереве контента.
  • onGetSearchResult() – когда клиент запрашивает результаты поиска из дерева контента по определенному запросу.

Соответствующие методы обратного вызова будут включать объект LibraryParams с дополнительными сигналами о типе дерева контента, который интересует клиентское приложение.

Кнопки команд для мультимедийных объектов

Приложение сеанса может объявлять кнопки команд, которые поддерживаются MediaItem в MediaMetadata. Это позволяет назначить одну или несколько записей CommandButton мультимедийному объекту, который контроллер может показывать и использовать для удобной отправки специальной команды для объекта в сеанс.

Настройка кнопок команд на стороне сеанса

При создании сеанса приложение объявляет набор кнопок команд, которые сеанс может обрабатывать как пользовательские команды:

Kotlin

val allCommandButtons =
  listOf(
    CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
      .setDisplayName(context.getString(R.string.add_to_playlist))
      .setSessionCommand(SessionCommand(COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
      .setExtras(playlistAddExtras)
      .build(),
    CommandButton.Builder(CommandButton.ICON_RADIO)
      .setDisplayName(context.getString(R.string.radio_station))
      .setSessionCommand(SessionCommand(COMMAND_RADIO, Bundle.EMPTY))
      .setExtras(radioExtras)
      .build(),
  )
// Add all command buttons for media items supported by the session.
val session =
  MediaSession.Builder(context, player)
    .setCommandButtonsForMediaItems(allCommandButtons)
    .build()

Java

ImmutableList<CommandButton> allCommandButtons =
    ImmutableList.of(
        new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
            .setDisplayName(context.getString(R.string.add_to_playlist))
            .setSessionCommand(new SessionCommand(COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
            .setExtras(playlistAddExtras)
            .build(),
        new CommandButton.Builder(CommandButton.ICON_RADIO)
            .setDisplayName(context.getString(R.string.radio_station))
            .setSessionCommand(new SessionCommand(COMMAND_RADIO, Bundle.EMPTY))
            .setExtras(radioExtras)
            .build());
// Add all command buttons for media items supported by the session.
MediaSession session =
    new MediaSession.Builder(context, player)
        .setCommandButtonsForMediaItems(allCommandButtons)
        .build();

При создании объекта мультимедиа приложение сеанса может добавить набор поддерживаемых идентификаторов команд, которые ссылаются на команды сеанса для кнопок команд, настроенных при создании сеанса:

Kotlin

val mediaItem =
  MediaItem.Builder()
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setSupportedCommands(listOf(COMMAND_PLAYLIST_ADD, COMMAND_RADIO))
        .build()
    )
    .build()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setSupportedCommands(ImmutableList.of(COMMAND_PLAYLIST_ADD, COMMAND_RADIO))
                .build())
        .build();

Когда контроллер или браузер подключается или вызывает другой метод сеанса Callback, приложение сеанса может проверить ControllerInfo, переданный обратному вызову, чтобы получить максимальное количество кнопок команд, которые может отображать контроллер или браузер. Объект ControllerInfo, переданный в метод обратного вызова, предоставляет метод получения для удобного доступа к этому значению. По умолчанию задано значение 0, которое указывает, что браузер или контроллер не поддерживает эту функцию.

Kotlin

override fun onGetItem(
  session: MediaLibrarySession,
  browser: MediaSession.ControllerInfo,
  mediaId: String,
): ListenableFuture<LibraryResult<MediaItem>> {
  val settableFuture = SettableFuture.create<LibraryResult<MediaItem>>()

  val maxCommandsForMediaItems = browser.maxCommandsForMediaItems
  loadMediaItemAsync(settableFuture, mediaId, maxCommandsForMediaItems)

  return settableFuture
}

Java

@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
    MediaLibraryService.MediaLibrarySession session,
    ControllerInfo browser,
    String mediaId) {
  SettableFuture<LibraryResult<MediaItem>> settableFuture = SettableFuture.create();

  int maxCommandsForMediaItems = browser.getMaxCommandsForMediaItems();
  loadMediaItemAsync(settableFuture, mediaId, maxCommandsForMediaItems);

  return settableFuture;
}

При обработке специального действия, отправленного для объекта медиа, приложение сеанса может получить идентификатор объекта медиа из аргументов Bundle, переданных в onCustomCommand:

Kotlin

override fun onCustomCommand(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
  customCommand: SessionCommand,
  args: Bundle,
): ListenableFuture<SessionResult> {
  val mediaItemId = args.getString(MediaConstants.EXTRA_KEY_MEDIA_ID)
  return if (mediaItemId != null)
    handleCustomCommandForMediaItem(controller, customCommand, mediaItemId, args)
  else handleCustomCommand(controller, customCommand, args)
}

Java

@Override
public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session,
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args) {
  String mediaItemId = args.getString(MediaConstants.EXTRA_KEY_MEDIA_ID);
  return mediaItemId != null
      ? handleCustomCommandForMediaItem(controller, customCommand, mediaItemId, args)
      : handleCustomCommand(controller, customCommand, args);
}

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

На стороне MediaController приложение может объявить максимальное количество кнопок команд, которые оно поддерживает для мультимедийного объекта, при создании MediaController или MediaBrowser:

Kotlin

val browserFuture =
  MediaBrowser.Builder(context, sessionToken).setMaxCommandsForMediaItems(3).buildAsync()

Java

ListenableFuture<MediaBrowser> browserFuture =
    new MediaBrowser.Builder(context, sessionToken).setMaxCommandsForMediaItems(3).buildAsync();

При подключении к сеансу приложение-контроллер может получать кнопки команд, которые поддерживаются мультимедийным объектом и для которых у контроллера есть доступная команда, предоставленная приложением сеанса:

Kotlin

val commandButtonsForMediaItem = controller.getCommandButtonsForMediaItem(mediaItem)

Java

ImmutableList<CommandButton> commandButtonsForMediaItem =
    controller.getCommandButtonsForMediaItem(mediaItem);

Kotlin

val future =
  controller.sendCustomCommand(
    requireNotNull(addToPlaylistButton.sessionCommand),
    mediaItem,
    Bundle.EMPTY,
  )

Java

ListenableFuture<SessionResult> future =
    controller.sendCustomCommand(
        checkNotNull(addToPlaylistButton.sessionCommand), mediaItem, Bundle.EMPTY);