Cómo entregar contenido con un MediaLibraryService

Las apps de contenido multimedia suelen contener colecciones de elementos multimedia organizados en una jerarquía. Por ejemplo, canciones en un álbum o episodios de TV en una playlist. Esta jerarquía de elementos multimedia se conoce como biblioteca multimedia.

Ejemplos de contenido multimedia organizado en una jerarquía
Figura 1: Ejemplos de jerarquías de elementos multimedia que forman una biblioteca multimedia.

Un MediaLibraryService proporciona una API estandarizada para entregar y acceder a tu biblioteca de contenido multimedia. Esto puede ser útil, por ejemplo, cuando agregas compatibilidad con Android Auto a tu app de música, que proporciona su propia IU segura para el conductor para tu biblioteca de música.

Compila una MediaLibraryService

La implementación de un MediaLibraryService es similar a implementar un MediaSessionService, excepto que, en el método onGetSession(), debes mostrar un MediaLibrarySession en lugar de un MediaSession.

Kotlin

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

  // If desired, validate the controller before returning the media library session
  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? =
    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();
  }
}

Recuerda declarar tu Service y los permisos necesarios en el archivo de manifiesto también:

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

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- For targetSdk 34+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

Usa un MediaLibrarySession

La API de MediaLibraryService espera que tu biblioteca de contenido multimedia esté estructurada en un formato de árbol, con un solo nodo raíz y nodos secundarios que puedan reproducirse o explorarse.

Un MediaLibrarySession extiende la API de MediaSession para agregar APIs de navegación de contenido. En comparación con la devolución de llamada MediaSession, la devolución de llamada MediaLibrarySession agrega métodos como los siguientes:

  • onGetLibraryRoot() para cuando un cliente solicita el MediaItem raíz de un árbol de contenido
  • onGetChildren() para cuando un cliente solicita los elementos secundarios de un MediaItem en el árbol de contenido
  • onGetSearchResult() para cuando un cliente solicita resultados de la búsqueda del árbol de contenido para una búsqueda determinada

Los métodos de devolución de llamada relevantes incluirán un objeto LibraryParams con indicadores adicionales sobre el tipo de árbol de contenido que le interesa a una app cliente.

Botones de comando para elementos multimedia

Una app de sesión puede declarar botones de comando compatibles con un MediaItem en MediaMetadata. Esto permite asignar una o más entradas CommandButton a un elemento multimedia que un controlador puede mostrar y usar para enviar el comando personalizado del elemento a la sesión de una manera conveniente.

Cómo configurar botones de comando en el lado de la sesión

Cuando se compila la sesión, una app de sesión declara el conjunto de botones de comando que una sesión puede controlar como comandos personalizados:

Kotlin

val allCommandButtons =
  listOf(
    CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
      .setDisplayName(context.getString(R.string.add_to_playlist))
      .setDisplayName("Add to playlist")
      .setIconResId(R.drawable.playlist_add)
      .setSessionCommand(SessionCommand(COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
      .setExtras(playlistAddExtras)
      .build(),
    CommandButton.Builder(CommandButton.ICON_RADIO)
      .setDisplayName(context.getString(R.string.radio_station))
      .setIconResId(R.drawable.radio)
      .setSessionCommand(SessionCommand(COMMAND_RADIO, Bundle.EMPTY))
      .setExtras(radioExtras)
      .build(),
    // possibly more here
  )

// 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("Add to playlist")
            .setIconUri(Uri.parse("http://www.example.com/icon/playlist_add"))
            .setSessionCommand(new SessionCommand(COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
            .setExtras(playlistAddExtras)
            .build(),
        new CommandButton.Builder(CommandButton.ICON_RADIO)
            .setDisplayName("Radio station")
            .setIconUri(Uri.parse("http://www.example.com/icon/radio"))
            .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();

Cuando se compila un elemento multimedia, una app de sesión puede agregar un conjunto de IDs de comando compatibles que hacen referencia a los comandos de sesión de los botones de comando que se configuraron cuando se compiló la sesión:

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

Cuando un controlador o un navegador se conecta o llama a otro método de la sesión Callback, la app de la sesión puede inspeccionar el ControllerInfo que se pasa a la devolución de llamada para obtener la cantidad máxima de botones de comando que puede mostrar un controlador o un navegador. El ControllerInfo que se pasa a un método de devolución de llamada proporciona un método get para acceder a este valor de forma conveniente. De forma predeterminada, el valor se establece en 0, lo que indica que el navegador o el controlador no admiten esta función:

Kotlin

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

  val settableFuture = SettableFuture.create<LibraryResult<MediaItem>>()

  val maxCommandsForMediaItems = browser.maxCommandsForMediaItems
  scope.launch {
    loadMediaItem(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;
}

Cuando se controla una acción personalizada que se envió para un elemento multimedia, la app de la sesión puede obtener el ID del elemento multimedia de los argumentos Bundle que se pasan a 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);
}

Cómo usar los botones de comando como navegador o controlador

En el lado de MediaController, una app puede declarar la cantidad máxima de botones de comando que admite para un elemento multimedia cuando compila MediaController o MediaBrowser:

Kotlin

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

Java

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

Cuando se conecta a la sesión, la app del controlador puede recibir los botones de comando que admite el elemento multimedia y para los que el controlador tiene el comando disponible que otorga la app de la sesión:

Kotlin

val commandButtonsForMediaItem: List<CommandButton> =
  controller.getCommandButtonsForMediaItem(mediaItem)

Java

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

Para mayor comodidad, un MediaController puede enviar comandos personalizados específicos del elemento multimedia con MediaController.sendCustomCommand(SessionCommand, MediaItem, Bundle):

Kotlin

controller.sendCustomCommand(addToPlaylistButton.sessionCommand!!, mediaItem, Bundle.EMPTY)

Java

controller.sendCustomCommand(
    checkNotNull(addToPlaylistButton.sessionCommand), mediaItem, Bundle.EMPTY);