Los controles multimedia de Android se encuentran cerca de la Configuración rápida. Se organizan en un carrusel deslizable las sesiones de varias apps. El carrusel enumera 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 una
conjunto de controles multimedia para apps que reproducen contenido multimedia, botones de acción en controles multimedia
se derivan del estado Player
.
De esta manera, puedes presentar un conjunto coherente de controles multimedia de control de contenido multimedia 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
que se describe en la siguiente tabla. En el modo compacto, solo las primeras tres acciones
las ranuras restantes. Esto se alinea con la forma en que los controles multimedia se renderizan en otros
Plataformas de Android, como Auto, Asistente y Wear OS
Ranura | Criterios | Acción |
---|---|---|
1 |
playWhenReady
es falso o la 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 |
Ni el comando COMMAND_SEEK_TO_PREVIOUS ni el COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM del reproductor están disponibles, y hay un comando personalizado del diseño personalizado que aún no se colocó disponible para llenar el espacio. |
Personalizada | |
(aún no es compatible con Media3) Los extras de PlaybackState 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 |
Ni el comando COMMAND_SEEK_TO_NEXT ni el COMMAND_SEEK_TO_NEXT_MEDIA_ITEM del reproductor están disponibles, y hay un comando personalizado del diseño personalizado que aún no se colocó disponible para llenar el espacio. |
Personalizada | |
(aún no es compatible con Media3) Los extras de PlaybackState incluyen un valor booleano true para la clave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . |
Vacío | |
4 | Un comando personalizado del diseño personalizado que aún no se ha colocado está disponible para llenar el espacio. | Personalizada |
5 | Un comando personalizado del diseño personalizado que aún no se ha colocado está disponible para llenar el espacio. | Personalizada |
Los comandos personalizados se colocan en el orden en que se agregaron a la un diseño personalizado.
Personaliza los botones de comando
Para personalizar los controles multimedia del sistema con Jetpack Media3, haz lo siguiente:
puedes configurar el diseño personalizado de la sesión y los comandos disponibles de
controladores en consecuencia, cuando implementes un MediaSessionService
:
En
onCreate()
, compila unMediaSession
. y definir el diseño personalizado de botones de comando.En
MediaSession.Callback.onConnect()
, para autorizar a los controladores definiendo los comandos disponibles, como comandos personalizados, en elConnectionResult
.En
MediaSession.Callback.onCustomCommand()
, responder 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); } } }
Si quieres obtener más información para configurar tu MediaSession
, de modo que clientes como el
de red puede conectarse a tu app de música, consulta
Otorgar control a otros clientes.
Con Jetpack Media3, cuando implementas un MediaSession
, tu PlaybackState
se actualiza automáticamente con el reproductor multimedia. De manera similar, cuando
implementas MediaSessionService
, la biblioteca publica automáticamente un
MediaStyle
notificación
para ti y lo mantiene actualizado.
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, la
MediaController
envía un comando de reproducción a tu MediaSession
. El
Luego, MediaSession
delega esos comandos al reproductor. Comandos
definido en el archivo Player
de Media3
interfaz de la nube son controladas automáticamente por el
sesión.
Consulta Cómo agregar comandos personalizados. para obtener orientación sobre cómo responder a un comando personalizado.
Comportamiento anterior a Android 13
Para ofrecer retrocompatibilidad, la IU del sistema sigue proporcionando un diseño alternativo.
que use acciones de notificación para apps que no se actualizan a Android 13
o que no incluyen información de PlaybackState
. Los botones de acción son
derivada de la lista Notification.Action
adjunta a MediaStyle
notificación. El sistema muestra hasta cinco acciones en el orden en que
que se agregaron. En el modo compacto, se muestran hasta tres botones, determinados por el
valores pasados 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 MediaStyle notificación :
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();
Admite la reanudación de contenido multimedia
La reanudación de contenido multimedia permite que los usuarios reinicien las sesiones anteriores desde el carrusel sin tener que iniciar la app. Cuando comienza la reproducción, el usuario interactúa con los controles multimedia de la forma habitual.
La función de reanudación de la reproducción se puede activar o desactivar en la aplicación Configuración. en la sección Sonido > Opciones multimedia. El usuario también puede acceder a la Configuración tocando el icono de ajustes que aparece después de deslizar el carrusel.
Media3 ofrece APIs para facilitar la compatibilidad con la reanudación de contenido multimedia. Consulta la Reanudación de la reproducción con Media3 de Google para obtener orientación sobre la implementación de 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 mediante las APIs heredadas de MediaCompat.
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 no se configura la duración, la barra deslizante no mostrar progreso)
Para asegurarte de tener una notificación válida y precisa,
establece el valor de METADATA_KEY_TITLE
o METADATA_KEY_DISPLAY_TITLE
.
metadatos al título del contenido multimedia que se está reproduciendo en ese momento.
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 deslizante que se asigna al PlaybackState
de MediaSession
. La barra deslizante
Permite que los usuarios cambien la posición y muestra el tiempo transcurrido del contenido multimedia.
elemento. Para habilitar la barra de búsqueda, debes implementar
PlaybackState.Builder#setActions
e incluye 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 realizó. |
|
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 realizó. |
|
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ó. |
Agrega acciones estándar
En los siguientes ejemplos de código, se muestra cómo agregar el estándar PlaybackState
y
acciones personalizadas.
Para reproducir, pausar, anterior y siguiente, establece estas acciones en
el 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 horarios anteriores ni en los siguientes, no agregues
ACTION_SKIP_TO_PREVIOUS
o ACTION_SKIP_TO_NEXT
, y, en su lugar, agrega extras 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 agrégala a PlaybackState
en su lugar. 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 las acciones de PlaybackState
Cuando un usuario presiona un botón, SystemUI usa
MediaController.TransportControls
para enviar un comando de vuelta a MediaSession
. Debes registrar una devolución de llamada
que puedan responder adecuadamente a esos 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); }