I controlli multimediali in Android si trovano accanto alle Impostazioni rapide. Le sessioni di più app vengono organizzate in un carosello a scorrimento. Il carosello elenca le sessioni in questo ordine:
- Stream riprodotti localmente sullo smartphone
- Stream remoti, ad esempio quelli rilevati su dispositivi esterni o sessioni di trasmissione
- Sessioni ripristinabili precedenti, nell'ordine in cui sono state giocate l'ultima volta
A partire da Android 13 (livello API 33), per garantire che gli utenti possano accedere a un insieme avanzato di controlli multimediali per le app che riproducono contenuti multimediali, i pulsanti di azione sui controlli multimediali provengono dallo stato Player
.
In questo modo, puoi presentare un insieme coerente di controlli multimediali e un'esperienza di controllo dei contenuti multimediali più elegante su tutti i dispositivi.
La figura 1 mostra un esempio di come appare rispettivamente su un telefono e un tablet.
Il sistema mostra fino a cinque pulsanti di azione in base allo stato Player
, come
descritto nella seguente tabella. In modalità compatta vengono
visualizzati solo i primi tre slot di azione. Ciò è in linea con il modo in cui viene eseguito il rendering dei controlli multimediali in altre
piattaforme Android, come Auto, Assistente e Wear OS.
Slot | Criteri | Azione |
---|---|---|
1 |
playWhenReady
è false o lo stato di
riproduzione corrente è STATE_ENDED .
|
Riproduci |
playWhenReady è true e lo stato di riproduzione corrente è STATE_BUFFERING .
|
Rotella di caricamento | |
playWhenReady è true e lo stato di riproduzione corrente è STATE_READY . |
Metti in pausa | |
2 | È disponibile il comando del player COMMAND_SEEK_TO_PREVIOUS o COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM . |
Precedente |
Non sono disponibili né i comandi COMMAND_SEEK_TO_PREVIOUS né COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM , ma è disponibile un comando personalizzato dal layout personalizzato che non è stato ancora inserito per riempire l'area. |
Personalizzata | |
(non ancora supportato con Media3) Gli extra PlaybackState includono un valore booleano true per la chiave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . |
Vuoto | |
3 | È disponibile il comando del player COMMAND_SEEK_TO_NEXT o COMMAND_SEEK_TO_NEXT_MEDIA_ITEM . |
Avanti |
Non sono disponibili né i comandi COMMAND_SEEK_TO_NEXT né COMMAND_SEEK_TO_NEXT_MEDIA_ITEM , ma è disponibile un comando personalizzato dal layout personalizzato che non è stato ancora inserito per riempire l'area. |
Personalizzata | |
(non ancora supportato con Media3) Gli extra PlaybackState includono un valore booleano true per la chiave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . |
Vuoto | |
4 | Un comando personalizzato del layout personalizzato che non è stato ancora inserito è disponibile per riempire l'area. | Personalizzata |
5 | Un comando personalizzato del layout personalizzato che non è stato ancora inserito è disponibile per riempire l'area. | Personalizzata |
I comandi personalizzati vengono inseriti nell'ordine in cui sono stati aggiunti al layout personalizzato.
Personalizza i pulsanti di comando
Per personalizzare i controlli multimediali di sistema con Jetpack Media3,
puoi impostare di conseguenza il layout personalizzato della sessione e i comandi disponibili
dei controller, quando implementi un MediaSessionService
:
In
onCreate()
, crea unaMediaSession
e definisci il layout personalizzato dei pulsanti di comando.In
MediaSession.Callback.onConnect()
, autorizza i controller definendo i loro comandi disponibili, inclusi i comandi personalizzati, nelConnectionResult
.In
MediaSession.Callback.onCustomCommand()
, rispondi al comando personalizzato selezionato dall'utente.
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); } } }
Per scoprire di più sulla configurazione di MediaSession
in modo che i client come
il sistema possano connettersi alla tua app multimediale, consulta
Concedi il controllo ad altri client.
Con Jetpack Media3, quando implementi un MediaSession
, il tuo PlaybackState
viene aggiornato automaticamente con il media player. Allo stesso modo, quando
implementi un MediaSessionService
, la libreria pubblica automaticamente una
notifica
MediaStyle
per te e la mantiene aggiornata.
Rispondere ai pulsanti di azione
Quando un utente tocca un pulsante di azione nei controlli multimediali di sistema, l'elemento MediaController
del sistema invia un comando di riproduzione al tuo MediaSession
. MediaSession
quindi delega questi comandi al player. I comandi definiti nell'interfaccia Player
di Media3 vengono gestiti automaticamente dalla sessione multimediale.
Consulta Aggiungere comandi personalizzati per indicazioni su come rispondere a un comando personalizzato.
Comportamento prima di Android 13
Per garantire la compatibilità con le versioni precedenti, l'UI di sistema continua a fornire un layout alternativo
che utilizza le azioni di notifica per le app che non vengono aggiornate per avere come target Android 13
o che non includono informazioni su PlaybackState
. I pulsanti di azione
sono derivati dall'elenco Notification.Action
allegato alla notifica
MediaStyle
. Il sistema visualizza fino a cinque azioni nell'ordine in cui sono state aggiunte. In modalità compatta, vengono mostrati fino a tre pulsanti, a seconda dei
valori trasferiti a setShowActionsInCompactView()
.
Le azioni personalizzate vengono inserite nell'ordine in cui sono state aggiunte a PlaybackState
.
Il seguente esempio di codice illustra come aggiungere azioni alla notifica 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();
Supporta la ripresa dei contenuti multimediali
La ripresa dei contenuti multimediali consente agli utenti di riavviare le sessioni precedenti dal carosello senza dover avviare l'app. All'avvio della riproduzione, l'utente interagisce con i controlli multimediali come di consueto.
La funzionalità di ripresa della riproduzione può essere attivata e disattivata utilizzando le opzioni Audio > Contenuti multimediali dell'app Impostazioni. L'utente può accedere alle Impostazioni anche toccando l'icona a forma di ingranaggio visualizzata dopo aver fatto scorrere il dito sul carosello espanso.
Media3 offre API per semplificare il supporto della ripresa dei contenuti multimediali. Consulta la documentazione sulla ripresa della riproduzione con Media3 per indicazioni sull'implementazione di questa funzionalità.
Utilizzo delle API multimediali legacy
Questa sezione spiega come eseguire l'integrazione con i controlli multimediali del sistema utilizzando le API MediaCompat legacy.
Il sistema recupera le seguenti informazioni dal MediaMetadata
di MediaSession
e le visualizza quando sono disponibili:
METADATA_KEY_ALBUM_ART_URI
METADATA_KEY_TITLE
METADATA_KEY_DISPLAY_TITLE
METADATA_KEY_ARTIST
METADATA_KEY_DURATION
(Se la durata non è impostata, la barra di scorrimento non mostra l'avanzamento)
Per assicurarti di ricevere una notifica valida e accurata sul controllo dei contenuti multimediali, imposta il valore dei metadati METADATA_KEY_TITLE
o METADATA_KEY_DISPLAY_TITLE
sul titolo dei contenuti multimediali attualmente in riproduzione.
Il media player mostra il tempo trascorso per i contenuti multimediali
attualmente in riproduzione, insieme a una barra di scorrimento mappata a MediaSession
PlaybackState
.
Il media player mostra l'avanzamento dei contenuti multimediali attualmente in riproduzione, insieme a una barra di scorrimento mappata a PlaybackState
di MediaSession
. La barra di scorrimento consente agli utenti di cambiare la posizione e mostra il tempo trascorso per l'elemento multimediale. Per attivare la barra di scorrimento, devi implementare
PlaybackState.Builder#setActions
e includere ACTION_SEEK_TO
.
Slot | Azione | Criteri |
---|---|---|
1 | Riproduci |
Lo stato corrente di PlaybackState è uno dei seguenti:
|
Rotella di caricamento |
Lo stato corrente di PlaybackState è uno dei seguenti:
|
|
Metti in pausa | Lo stato corrente di PlaybackState non è uno dei precedenti. |
|
2 | Precedente | Le azioni di PlaybackState includono ACTION_SKIP_TO_PREVIOUS . |
Personalizzata | PlaybackState azioni non includono ACTION_SKIP_TO_PREVIOUS e PlaybackState azioni personalizzate includono un'azione personalizzata che non è stata ancora eseguita. |
|
Vuoto | PlaybackState Gli extra includono un valore booleano true per la chiave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV . |
|
3 | Avanti | Le azioni di PlaybackState includono ACTION_SKIP_TO_NEXT . |
Personalizzata | PlaybackState azioni non includono ACTION_SKIP_TO_NEXT e PlaybackState azioni personalizzate includono un'azione personalizzata che non è stata ancora eseguita. |
|
Vuoto | PlaybackState Gli extra includono un valore booleano true per la chiave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT . |
|
4 | Personalizzata | PlaybackState azioni personalizzate includono un'azione personalizzata che non è stata ancora eseguita. |
5 | Personalizzata | PlaybackState azioni personalizzate includono un'azione personalizzata che non è stata ancora eseguita. |
Aggiungi azioni standard
I seguenti esempi di codice spiegano come aggiungere azioni standard e
personalizzate di PlaybackState
.
Per i contenuti di riproduzione, pausa, precedente e successivo, imposta queste azioni nell'elemento PlaybackState
per la sessione multimediale.
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 non vuoi nessun pulsante negli spazi precedenti o successivi, non aggiungere ACTION_SKIP_TO_PREVIOUS
o ACTION_SKIP_TO_NEXT
, ma aggiungi extra alla sessione:
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);
Aggiungi azioni personalizzate
Per le altre azioni che vuoi mostrare nei controlli multimediali, puoi creare una PlaybackStateCompat.CustomAction
e aggiungerla alla PlaybackState
. Queste azioni vengono mostrate
nell'ordine in cui sono state aggiunte.
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);
Rispondere alle azioni PlaybackState
Quando un utente tocca un pulsante, SystemUI utilizza MediaController.TransportControls
per inviare un comando all'MediaSession
. Devi registrare un callback che possa
rispondere correttamente a questi eventi.
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); } } };
Ripresa contenuti multimediali
Per mostrare l'app del player nell'area delle impostazioni rapide,
devi creare una notifica MediaStyle
con un token MediaSession
valido.
Per visualizzare il titolo della notifica MediaStyle, utilizza NotificationBuilder.setContentTitle()
.
Per visualizzare l'icona del brand per il media player, utilizza
NotificationBuilder.setSmallIcon()
.
Per supportare la ripresa della riproduzione, le app devono implementare MediaBrowserService
e MediaSession
. MediaSession
deve implementare il callback onPlay()
.
Implementazione di MediaBrowserService
Una volta avviato il dispositivo, il sistema cerca le cinque app multimediali utilizzate più di recente e fornisce i controlli che possono essere utilizzati per riavviare la riproduzione da ogni app.
Il sistema tenta di contattare il tuo MediaBrowserService
con una connessione da
SystemUI. L'app deve consentire queste connessioni, altrimenti non può supportare la ripresa della riproduzione.
Le connessioni da SystemUI possono essere identificate e verificate utilizzando il nome del pacchetto com.android.systemui
e la firma. La SystemUI è firmata con
la firma della piattaforma. Nell'app UAMP puoi trovare un esempio di come eseguire il controllo con la firma della piattaforma.
Per supportare la ripresa della riproduzione, MediaBrowserService
deve
implementare i seguenti comportamenti:
onGetRoot()
deve restituire rapidamente una radice non nullo. Occorre gestire altre logiche complesse inonLoadChildren()
Quando
onLoadChildren()
viene chiamato sull'ID multimediale principale, il risultato deve contenere un elemento secondario FLAG_PLAYABLE.MediaBrowserService
dovrebbe restituire l'elemento multimediale riprodotto più di recente quando riceve una query EXTRA_RECENT. Il valore restituito deve essere un effettivo elemento multimediale e non una funzione generica.MediaBrowserService
deve fornire un elemento MediaDescription appropriato con un titolo e un sottotitolo non vuoti. Dovrebbe anche impostare un URI dell'icona o una bitmap dell'icona.
I seguenti esempi di codice illustrano come implementare 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); }