Los controles de contenido multimedia de Android se encuentran cerca de la Configuración rápida. Se organizan en un carrusel deslizable las sesiones de varias apps. En el carrusel, se enumeran las sesiones en este orden:
- Transmisiones locales en el teléfono
- Transmisiones remotas, como las detectadas en dispositivos externos o sesiones de transmisión
- Sesiones reanudables anteriores, en el orden en que se reprodujeron por última vez
A partir de Android 13 (nivel de API 33), para garantizar que los usuarios puedan acceder a un conjunto rico de controles multimedia para las apps que reproducen contenido multimedia, los botones de acción en los controles multimedia se derivan del estado Player
.
De esta manera, puedes presentar un conjunto coherente de controles multimedia y una experiencia de control multimedia más pulida en todos los dispositivos.
En la Figura 1, se muestra un ejemplo de cómo se ve en un teléfono y una tablet, respectivamente.
El sistema muestra hasta cinco botones de acción en función del estado de Player
, como se describe en la siguiente tabla. En el modo compacto, solo se muestran las primeras tres ranuras de acción. Esto se alinea con la forma en que se renderizan los controles multimedia en otras plataformas de Android, como Auto, Asistente y Wear OS.
Ranura | Criterios | Acción |
---|---|---|
1 |
playWhenReady es falso o el estado de reproducción actual es STATE_ENDED .
|
Reproducir |
playWhenReady es verdadero y el estado de reproducción actual es STATE_BUFFERING .
|
Ícono giratorio de carga | |
playWhenReady es verdadero y el estado de reproducción actual es STATE_READY . |
Pausar | |
2 | El comando del reproductor COMMAND_SEEK_TO_PREVIOUS o COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM está disponible. |
Anterior |
No están disponibles los comandos del jugador COMMAND_SEEK_TO_PREVIOUS ni COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM , y hay un comando personalizado del diseño personalizado que aún no se colocó para completar el espacio. |
Personalizada | |
Los extras de la sesión incluyen un valor booleano true para la clave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . |
Vacío | |
3 | El comando del reproductor COMMAND_SEEK_TO_NEXT o COMMAND_SEEK_TO_NEXT_MEDIA_ITEM está disponible. |
Siguiente |
No están disponibles los comandos del jugador COMMAND_SEEK_TO_NEXT ni COMMAND_SEEK_TO_NEXT_MEDIA_ITEM , y hay un comando personalizado del diseño personalizado que aún no se colocó para completar el espacio. |
Personalizada | |
Los extras de la sesión incluyen un valor booleano true para la clave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . |
Vacío | |
4 | Hay un comando personalizado del diseño personalizado que aún no se colocó disponible para completar el espacio. | Personalizada |
5 | Hay un comando personalizado del diseño personalizado que aún no se colocó disponible para completar el espacio. | Personalizada |
Los comandos personalizados se colocan en el orden en que se agregaron al diseño personalizado.
Personaliza los botones de comando
Para personalizar los controles multimedia del sistema con Jetpack Media3, puedes configurar el diseño personalizado de la sesión y los comandos disponibles de los controladores según corresponda cuando implementes un MediaSessionService
:
En
onCreate()
, compila unMediaSession
y define el diseño personalizado de los botones de comando.En
MediaSession.Callback.onConnect()
, autoriza a los controladores definiendo sus comandos disponibles, incluidos los comandos personalizados, enConnectionResult
.En
MediaSession.Callback.onCustomCommand()
, responde al comando personalizado que selecciona el usuario.
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 ListenableFutureonCustomCommand( 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 obtener más información sobre cómo configurar tu MediaSession
para que clientes como el sistema puedan conectarse a tu app de música, consulta Otorga control a otros clientes.
Con Jetpack Media3, cuando implementas un MediaSession
, tu PlaybackState
se mantiene automáticamente actualizado con el reproductor multimedia. De manera similar, cuando implementas un MediaSessionService
, la biblioteca publica automáticamente una notificación MediaStyle
por ti y la mantiene actualizada.
Cómo responder a los botones de acción
Cuando un usuario presiona un botón de acción en los controles multimedia del sistema, el MediaController
del sistema envía un comando de reproducción a tu MediaSession
. Luego, MediaSession
delega esos comandos al jugador. La sesión de Media3 controla automáticamente los comandos definidos en la interfaz Player
de Media3.
Consulta Cómo agregar comandos personalizados para obtener orientación sobre cómo responder a un comando personalizado.
Comportamiento anterior a Android 13
Para la retrocompatibilidad, la IU del sistema sigue proporcionando un diseño alternativo que usa acciones de notificación para apps que no se actualizan para orientarse a Android 13 o que no incluyen información de PlaybackState
. Los botones de acción se derivan de la lista Notification.Action
adjunta a la notificación MediaStyle
. El sistema muestra hasta cinco acciones en el orden en que se agregaron. En el modo compacto, se muestran hasta tres botones, determinados por los valores que se pasan a setShowActionsInCompactView()
.
Las acciones personalizadas se muestran en el orden en que se agregaron a PlaybackState
.
En el siguiente ejemplo de código, se muestra cómo agregar acciones a la notificación MediaStyle :
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();
Cómo admitir la reanudación de contenido multimedia
La reanudación de contenido multimedia permite a los usuarios reiniciar sesiones anteriores desde el carrusel sin tener que iniciar la app. Cuando comienza la reproducción, el usuario interactúa con los controles de contenido multimedia de la manera habitual.
La función de reanudación de la reproducción se puede activar y desactivar en la app de Configuración, en las opciones Sonido > Audio. El usuario también puede acceder a Configuración presionando el ícono de ajustes que aparece después de deslizar el carrusel expandido.
Media3 ofrece APIs para facilitar la compatibilidad con la reanudación de contenido multimedia. Consulta la documentación sobre la reanudación de la reproducción con Media3 para obtener orientación sobre cómo implementar esta función.
Usa las APIs de contenido multimedia heredadas
En esta sección, se explica cómo realizar la integración con los controles multimedia del sistema con las APIs de MediaCompat heredadas.
El sistema recupera la siguiente información de MediaMetadata
de la MediaSession
y la muestra cuando está disponible:
METADATA_KEY_ALBUM_ART_URI
METADATA_KEY_TITLE
METADATA_KEY_DISPLAY_TITLE
METADATA_KEY_ARTIST
METADATA_KEY_DURATION
(si la duración no está configurada, la barra de búsqueda no mostrará el progreso)
Para asegurarte de tener una notificación de control multimedia válida y precisa, establece el valor de los metadatos METADATA_KEY_TITLE
o METADATA_KEY_DISPLAY_TITLE
en el título del contenido multimedia que se está reproduciendo.
El reproductor de contenido multimedia muestra el tiempo transcurrido del contenido multimedia que se está reproduciendo, junto con una barra de búsqueda que se asigna a la PlaybackState
de la MediaSession
.
El reproductor multimedia muestra el progreso del contenido multimedia que se está reproduciendo, junto con una barra de búsqueda que se asigna a la PlaybackState
de la MediaSession
. La barra de desplazamiento permite a los usuarios cambiar la posición y muestra el tiempo transcurrido del elemento multimedia. Para que se habilite la barra de búsqueda, debes implementar PlaybackState.Builder#setActions
e incluir ACTION_SEEK_TO
.
Ranura | Acción | Criterios |
---|---|---|
1 | Reproducir |
El estado actual de PlaybackState es uno de los siguientes:
|
Ícono giratorio de carga |
El estado actual de PlaybackState es uno de los siguientes:
|
|
Pausar | El estado actual de PlaybackState no es ninguno de los anteriores. |
|
2 | Anterior | Las acciones de PlaybackState incluyen ACTION_SKIP_TO_PREVIOUS . |
Personalizada | Las acciones de PlaybackState no incluyen ACTION_SKIP_TO_PREVIOUS , y las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se implementó. |
|
Vacío | Los extras de PlaybackState incluyen un valor booleano true para la clave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV . |
|
3 | Siguiente | Las acciones de PlaybackState incluyen ACTION_SKIP_TO_NEXT . |
Personalizada | Las acciones de PlaybackState no incluyen ACTION_SKIP_TO_NEXT , y las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se implementó. |
|
Vacío | Los extras de PlaybackState incluyen un valor booleano true para la clave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT . |
|
4 | Personalizada | Las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se realizó. |
5 | Personalizada | Las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se realizó. |
Cómo agregar acciones estándar
En los siguientes ejemplos de código, se muestra cómo agregar acciones estándar y personalizadas de PlaybackState
.
Para reproducir, pausar, ir al elemento anterior y al siguiente, establece estas acciones en PlaybackState
para la sesión multimedia.
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);
Si no quieres que haya botones en los espacios anterior o siguiente, no agregues ACTION_SKIP_TO_PREVIOUS
ni ACTION_SKIP_TO_NEXT
, sino que agrega elementos adicionales a la sesión:
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);
Agrega acciones personalizadas
Para otras acciones que quieras mostrar en los controles multimedia, puedes crear un PlaybackStateCompat.CustomAction
y agregarlo a PlaybackState
. Estas acciones se muestran en el orden en que se agregaron.
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);
Cómo responder a acciones de PlaybackState
Cuando un usuario presiona un botón, SystemUI usa MediaController.TransportControls
para enviar un comando a MediaSession
. Debes registrar una devolución de llamada
que pueda responder correctamente a estos 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); } } };
Reanudación de contenido multimedia
Si deseas que la aplicación de reproducción aparezca en el área de configuración rápida, debes crear una notificación MediaStyle
con un token MediaSession
válido.
Para mostrar el título de la notificación MediaStyle, usa NotificationBuilder.setContentTitle()
.
Para mostrar el ícono de marca del reproductor de contenido multimedia, usa NotificationBuilder.setSmallIcon()
.
Para admitir la reanudación de la reproducción, las apps deben implementar un MediaBrowserService
y una MediaSession
. Tu MediaSession
debe implementar la devolución de llamada onPlay()
.
Implementación de MediaBrowserService
Después de que se inicia el dispositivo, el sistema busca las últimas cinco apps de contenido multimedia y proporciona controles que se pueden usar para reiniciar la reproducción desde cada app.
El sistema intenta comunicarse con tu MediaBrowserService
mediante una conexión desde SystemUI. Tu app debe permitir estas conexiones; de lo contrario, no podrá reanudar la reproducción.
Se pueden identificar y verificar las conexiones de SystemUI usando el nombre de paquete com.android.systemui
y la firma. La interfaz de sistema lleva la firma de la plataforma. Puedes encontrar un ejemplo de cómo verificar la firma de la plataforma en la app de UAMP.
Para admitir la reanudación de la reproducción, tu MediaBrowserService
debe implementar estos comportamientos:
onGetRoot()
debe mostrar rápidamente una raíz no nula. Se debería manejar otra lógica compleja enonLoadChildren()
.Cuando se llama a
onLoadChildren()
en el ID de contenido multimedia raíz, el resultado debe contener un elemento FLAG_PLAYABLE secundario.MediaBrowserService
debería mostrar el elemento multimedia reproducido más recientemente cuando reciba una consulta EXTRA_RECENT. El valor que se muestre debería ser un elemento multimedia real en lugar de una función genérica.MediaBrowserService
debe proporcionar una MediaDescription adecuada, con un título y un subtítulo que no estén vacíos. También debería establecer un URI de ícono o un mapa de bits de ícono.
En los siguientes ejemplos de código, se muestra cómo 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); }