Exibir conteúdo com um MediaLibraryService

Os apps de mídia geralmente contêm coleções de itens de mídia organizados em uma hierarquia. Por exemplo, músicas em um álbum ou episódios de TV em uma playlist. Essa hierarquia de itens de mídia é conhecida como biblioteca de mídia.

Exemplos de conteúdo de mídia organizado em uma hierarquia
Figura 1: exemplos de hierarquias de itens de mídia que formam uma biblioteca de mídia.

Um MediaLibraryService fornece uma API padronizada para servir e acessar sua biblioteca de mídia. Isso pode ser útil, por exemplo, ao adicionar suporte ao Android Auto ao seu app de mídia, que fornece a própria interface segura para motoristas para a biblioteca de mídia.

Criar um MediaLibraryService

Implementar um MediaLibraryService é semelhante a implementar um MediaSessionService, exceto que no método onGetSession(), você precisa retornar um MediaLibrarySession em vez de um 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();
  }
}

Não se esqueça de declarar o Service e as permissões necessárias no arquivo de manifesto também:

<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" />

Use um MediaLibrarySession

A API MediaLibraryService espera que a biblioteca de mídia seja estruturada em um formato de árvore, com um único nó raiz e nós filhos que podem ser reproduzidos ou navegáveis.

Um MediaLibrarySession amplia a API MediaSession para adicionar APIs de navegação de conteúdo. Em comparação com o callback MediaSession, o callback MediaLibrarySession adiciona métodos como:

  • onGetLibraryRoot() para quando um cliente solicita o MediaItem raiz de uma árvore de conteúdo
  • onGetChildren() para quando um cliente solicita os filhos de um MediaItem na árvore de conteúdo
  • onGetSearchResult() para quando um cliente solicita resultados de pesquisa da árvore de conteúdo para uma determinada consulta.

Os métodos de callback relevantes vão incluir um objeto LibraryParams com outros indicadores sobre o tipo de árvore de conteúdo em que um app cliente está interessado.

Botões de comando para itens de mídia

Um app de sessão pode declarar botões de comando aceitos por um MediaItem no MediaMetadata. Isso permite atribuir uma ou mais entradas CommandButton a um item de mídia que um controlador pode mostrar e usar para enviar o comando personalizado do item à sessão de maneira conveniente.

Configurar botões de comando no lado da sessão

Ao criar a sessão, um app de sessão declara o conjunto de botões de comando que uma sessão pode processar 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();

Ao criar um item de mídia, um app de sessão pode adicionar um conjunto de IDs de comando aceitos que fazem referência a comandos de sessão de botões de comando que foram configurados ao criar a sessão:

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

Quando um controlador ou navegador se conecta ou chama outro método da sessão Callback, o app da sessão pode inspecionar o ControllerInfo transmitido para o callback para receber o número máximo de botões de comando que um controlador ou navegador pode exibir. O ControllerInfo transmitido para um método de callback fornece um getter para acessar esse valor de maneira conveniente. Por padrão, o valor é definido como 0, o que indica que o navegador ou controlador não oferece suporte a esse recurso:

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

Ao processar uma ação personalizada enviada para um item de mídia, o app de sessão pode receber o ID do item de mídia dos argumentos Bundle transmitidos para 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);
}

Usar botões de comando como um navegador ou controlador

No lado do MediaController, um app pode declarar o número máximo de botões de comando compatíveis com um item de mídia ao criar o MediaController ou MediaBrowser:

Kotlin

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

Java

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

Quando conectado à sessão, o app de controle pode receber os botões de comando compatíveis com o item de mídia e para os quais o controlador tem o comando disponível concedido pelo app de sessão:

Kotlin

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

Java

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

Para sua conveniência, um MediaController pode enviar comandos personalizados específicos do item de mídia com MediaController.sendCustomCommand(SessionCommand, MediaItem, Bundle):

Kotlin

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

Java

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