Controles de mídia

Os controles de mídia no Android estão localizados perto das Configurações rápidas. As sessões de vários apps são organizadas em um carrossel deslizante. O carrossel lista as sessões nesta ordem:

  • streams reproduzidos localmente no smartphone;
  • streams remotos, como aqueles detectados em dispositivos externos ou sessões de transmissão;
  • sessões retomáveis anteriores, na ordem em que foram reproduzidas pela última vez.

A partir do Android 13 (nível 33 da API), para garantir que os usuários possam acessar um conjunto de controles de mídia para apps que reproduzem mídia, botões de ação nos controles de mídia são derivados do estado Player.

Dessa forma, você pode apresentar um conjunto consistente de controles de mídia e um conjunto de controle de mídia em vários dispositivos.

A Figura 1 mostra um exemplo dessa aparência em um smartphone e um tablet, respectivamente.

Visualização de controles de mídia em um smartphone e um tablet,
            usando como exemplo uma faixa de som para mostrar como os botões podem ser exibidos.
Figura 1 : controles de mídia em smartphones e tablets

O sistema mostra até cinco botões de ação com base no estado Player. descritos na tabela a seguir. No modo compacto, apenas as três primeiras ações slots serão exibidos. Isso se alinha com a forma como os controles de mídia são renderizados em outros Plataformas Android, como Auto, Assistente e Wear OS.

Espaço Critérios Ação
1 playWhenReady é falsa ou a reprodução atual estado é STATE_ENDED. Reproduzir
playWhenReady é verdadeiro e o estado de reprodução atual é STATE_BUFFERING. Ícone de carregamento
playWhenReady é verdadeiro e o estado de reprodução atual é STATE_READY. Pausar
2 O comando do player COMMAND_SEEK_TO_PREVIOUS ou COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM está disponível. Anterior
Os comandos do player COMMAND_SEEK_TO_PREVIOUS e COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM não estão disponíveis, e um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. Personalizado
(ainda não compatível com Media3) PlaybackState extras incluem um valor booleano true para a chave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV. Vazio
3 O comando do player COMMAND_SEEK_TO_NEXT ou COMMAND_SEEK_TO_NEXT_MEDIA_ITEM está disponível. Próxima
Os comandos do player COMMAND_SEEK_TO_NEXT e COMMAND_SEEK_TO_NEXT_MEDIA_ITEM não estão disponíveis, e um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. Personalizado
(ainda não compatível com Media3) PlaybackState extras incluem um valor booleano true para a chave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT. Vazio
4 Um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. Personalizado
5 Um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. Personalizado

Os comandos personalizados são colocados na ordem em que foram adicionados ao um layout personalizado.

Personalizar botões de comando

Para personalizar os controles de mídia do sistema com o Jetpack Media3, faça o seguinte: você pode definir o layout personalizado da sessão e os comandos disponíveis de controladores adequadamente ao implementar um MediaSessionService:

  1. Em onCreate(), crie um MediaSession e defina o layout personalizado de botões de comando.

  2. No MediaSession.Callback.onConnect(), autorizar os controladores definindo os comandos disponíveis, incluindo comandos personalizados, em ConnectionResult.

  3. No MediaSession.Callback.onCustomCommand(), respondem ao comando personalizado que é selecionado pelo usuário.

Kotlin

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder()
        .setDisplayName("Save to favorites")
        .setIconResId(R.drawable.favorite_icon)
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setCustomLayout(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailablePlayerCommands(
        ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
          .remove(COMMAND_SEEK_TO_NEXT)
          .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
          .remove(COMMAND_SEEK_TO_PREVIOUS)
          .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
          .build()
      )
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Java

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder()
            .setDisplayName("Save to favorites")
            .setIconResId(R.drawable.favorite_icon)
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setCustomLayout(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailablePlayerCommands(
              ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
                .remove(COMMAND_SEEK_TO_NEXT)
                .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
                .remove(COMMAND_SEEK_TO_PREVIOUS)
                .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
                .build())
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

Para saber mais sobre como configurar MediaSession para que clientes como o sistema pode se conectar ao seu app de música, consulte Conceder controle a outros clientes.

Com o Jetpack Media3, quando você implementa um MediaSession, seu PlaybackState são atualizados automaticamente com o player de mídia. Da mesma forma, quando você implementar um MediaSessionService, a biblioteca publica automaticamente uma Notificação do MediaStyle para você e o mantém atualizado.

Botões de resposta

Quando um usuário toca em um botão de ação nos controles de mídia do sistema, o O MediaController envia um comando de reprodução ao MediaSession. A Em seguida, o MediaSession delega esses comandos para o player. Comandos definido no Player do Media3 são gerenciadas automaticamente pela sessão.

Consulte Adicionar comandos personalizados. para receber orientações sobre como responder a um comando personalizado.

Comportamento anterior ao Android 13

Para compatibilidade com versões anteriores, a interface do sistema continua a oferecer um layout alternativo que usa ações de notificação para apps que não são atualizados para o Android 13, ou que não incluam informações de PlaybackState. Os botões de ação são derivado da lista de Notification.Action anexada ao MediaStyle notificação. O sistema exibe até cinco ações na ordem em que elas foram adicionados. No modo compacto, até três botões são mostrados, determinados pelo passados para setShowActionsInCompactView().

As ações personalizadas são posicionadas na ordem em que foram adicionadas ao PlaybackState.

O exemplo de código a seguir ilustra como adicionar ações ao MediaStyle notificação :

Kotlin

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
        // Show controls on lock screen even when user hides sensitive content.
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        // Add media control buttons that invoke intents in your media service
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
        // Apply the media style template
        .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build()

Java

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent)
        .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build();

Suporte para retomada de mídia

Ao retomar a mídia, os usuários podem reiniciar as sessões anteriores do carrossel sem precisar iniciar o app. Quando a reprodução começa, o usuário interage com os controles de mídia da maneira normal.

O recurso de retomada da reprodução pode ser ativado e desativado usando o aplicativo Configurações, na seção Som > Opções de mídia. O usuário também pode acessar as Configurações tocando no ícone de engrenagem que aparece depois de deslizar sobre o carrossel expandido.

A Media3 oferece APIs para facilitar o suporte à retomada de mídia. Consulte a Retomada de reprodução com Media3 documentação para orientações sobre como implementar esse recurso.

Como usar as APIs de mídia legadas

Esta seção explica como fazer a integração com os controles de mídia do sistema usando as APIs MediaCompat legadas.

O sistema recupera as seguintes informações do MediaMetadata do MediaSession e as exibe quando estão disponíveis:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION. Se a duração não for definida, a barra de busca não mostrar progresso)

Para garantir que você tenha uma notificação de controle de mídia válida e precisa, Defina o valor de METADATA_KEY_TITLE ou METADATA_KEY_DISPLAY_TITLE. metadados ao título da mídia em reprodução.

O player de mídia mostra o tempo decorrido da mídia em reprodução no momento com uma barra de busca mapeada para o MediaSession PlaybackState.

O player de mídia mostra o progresso da mídia em reprodução no momento, junto com uma barra de busca mapeada para o PlaybackState do MediaSession. Barra de busca permite que os usuários alterem a posição e exibe o tempo decorrido da mídia do item de linha. Para que a barra de busca seja ativada, você deve implementar PlaybackState.Builder#setActions e inclua ACTION_SEEK_TO.

Espaço Ação Critérios
1 Reproduzir O estado atual da PlaybackState é um dos seguintes:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Ícone de carregamento O estado atual do PlaybackState é um dos seguintes:
  • STATE_CONNECTING
  • STATE_BUFFERING
Pausar O estado atual do PlaybackState não está listado acima.
2 Anterior As ações PlaybackState incluem ACTION_SKIP_TO_PREVIOUS.
Personalizado As ações PlaybackState não incluem ACTION_SKIP_TO_PREVIOUS, e as ações personalizadas PlaybackState incluem uma ação personalizada que ainda não foi posicionada.
Vazio Os PlaybackState extras incluem um valor booleano true para a chave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Próxima As ações PlaybackState incluem ACTION_SKIP_TO_NEXT.
Personalizado As ações PlaybackState não incluem ACTION_SKIP_TO_NEXT, e as ações personalizadas PlaybackState incluem uma ação personalizada que ainda não foi posicionada.
Vazio Os PlaybackState extras incluem um valor booleano true para a chave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Personalizado As ações personalizadas PlaybackStateincluem uma ação personalizada que ainda não foi posicionada.
5 Personalizado As ações personalizadas PlaybackStateincluem uma ação personalizada que ainda não foi posicionada.

Adicionar ações padrão

Os exemplos de código a seguir ilustram como adicionar os padrões PlaybackState e ações personalizadas.

Para reproduzir, pausar, voltar e avançar, defina essas ações em o PlaybackState para a sessão de mídia.

Kotlin

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Java

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Se não quiser nenhum botão nos espaços anterior ou seguinte, não adicione ACTION_SKIP_TO_PREVIOUS ou ACTION_SKIP_TO_NEXT e, em vez disso, adicionar extras à sessão:

Kotlin

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Java

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Adicionar ações personalizadas

Para outras ações que você queira mostrar nos controles de mídia, é possível criar um PlaybackStateCompat.CustomAction e adicioná-lo ao PlaybackState. Essas ações são mostradas ordem em que foram adicionados.

Kotlin

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Java

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Como responder a ações PlaybackState

Quando um usuário toca em um botão, o SystemUI usa MediaController.TransportControls para enviar um comando de volta ao MediaSession. É preciso registrar um callback que podem responder adequadamente a esses eventos.

Kotlin

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Java

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Retomada de mídia

Para que o app do player apareça na área de configurações rápidas, você precisa criar uma notificação MediaStyle com um token MediaSession válido.

Para exibir o título da notificação MediaStyle, use NotificationBuilder.setContentTitle():

Para exibir o ícone de marca do player de mídia, use NotificationBuilder.setSmallIcon().

Para compatibilidade com a retomada de reprodução, os apps precisam implementar um MediaBrowserService e um MediaSession. Seu MediaSession precisa implementar o callback onPlay().

Implementação de MediaBrowserService

Depois que o dispositivo for inicializado, o sistema procurará os cinco apps de mídia mais recentes e fornecerá controles que podem ser usados para reiniciar a reprodução de cada app.

O sistema tentará entrar em contato com o MediaBrowserService com uma conexão da SystemUI. Seu app precisa permitir essas conexões. Caso contrário, não será possível retomar a reprodução.

As conexões da SystemUI podem ser identificadas e verificadas usando o nome do pacote com.android.systemui e a assinatura. A SystemUI tem a assinatura da plataforma. Um exemplo de como verificar a assinatura da plataforma pode ser encontrado no app UAMP.

Para compatibilidade com a retomada de reprodução, seu MediaBrowserService precisa implementar estes comportamentos:

  • onGetRoot() precisa retornar uma raiz não nula rapidamente. Outra lógica complexa precisa ser tratada em onLoadChildren()

  • Quando onLoadChildren() é chamado no ID de mídia raiz, o resultado precisa conter um filho FLAG_PLAYABLE.

  • MediaBrowserService precisa retornar o item de mídia reproduzido mais recentemente quando receber uma consulta EXTRA_RECENT. O valor retornado precisa ser um item de mídia real em vez de uma função genérica.

  • MediaBrowserService precisa fornecer uma MediaDescription apropriada com um título e uma legenda não vazios. Ele também precisa definir um URI de ícone ou um bitmap de ícone.

Os exemplos de código a seguir ilustram como implementar onGetRoot().

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}