Controlli multimediali

I controlli multimediali su Android si trovano vicino alle Impostazioni rapide. Le sessioni di più app sono disposte in un carosello scorrevole. 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 precedenti riprendibili, nell'ordine in cui sono state riprodotte l'ultima volta

A partire da Android 13 (livello API 33), per garantire che gli utenti possano accedere a un ricco set di controlli multimediali per le app che riproducono contenuti multimediali, i pulsanti di azione sui controlli multimediali vengono derivati dallo stato Player.

In questo modo, puoi presentare un set coerente di controlli multimediali e un'esperienza di controllo multimediale più raffinata su tutti i dispositivi.

La Figura 1 mostra un esempio di come appare su uno smartphone e su un tablet, rispettivamente.

Controlli multimediali in termini di aspetto su smartphone e tablet,
            utilizzando l'esempio di una traccia campione che mostra come potrebbero apparire i pulsanti
Figura 1: Controlli multimediali su smartphone e tablet

Il sistema visualizza fino a cinque pulsanti di azione in base allo stato Player, come descritto nella tabella seguente. In modalità compatta, vengono visualizzati solo i primi tre slot di azione. Questo è in linea con il modo in cui i controlli multimediali vengono sottoposti a rendering in altre piattaforme Android come Auto, Assistant e Wear OS.

Slot Criteri Azione
1 playWhenReady è false o lo stato di riproduzione attuale è STATE_ENDED. Riproduci
playWhenReady è true e lo stato di riproduzione attuale è STATE_BUFFERING. Rotellina di caricamento
playWhenReady è true e lo stato di riproduzione attuale è STATE_READY. Metti in pausa
2 Le preferenze dei pulsanti multimediali contengono un pulsante personalizzato per CommandButton.SLOT_BACK Personalizzato
Il comando del player COMMAND_SEEK_TO_PREVIOUS o COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM è disponibile. Indietro
Non è disponibile né un pulsante personalizzato né uno dei comandi elencati. Vuoto
3 Le preferenze dei pulsanti multimediali contengono un pulsante personalizzato per CommandButton.SLOT_FORWARD Personalizzato
Il comando del player COMMAND_SEEK_TO_NEXT o COMMAND_SEEK_TO_NEXT_MEDIA_ITEM è disponibile. Avanti
Non è disponibile né un pulsante personalizzato né uno dei comandi elencati. Vuoto
4 Le preferenze dei pulsanti multimediali contengono un pulsante personalizzato per CommandButton.SLOT_OVERFLOW che non è ancora stato inserito. Personalizzato
5 Le preferenze dei pulsanti multimediali contengono un pulsante personalizzato per CommandButton.SLOT_OVERFLOW che non è ancora stato inserito. Personalizzato

I pulsanti di overflow personalizzati vengono inseriti nell'ordine in cui sono stati aggiunti alle preferenze dei pulsanti multimediali.

Personalizzare i pulsanti dei comandi

Per personalizzare i controlli multimediali di sistema con Jetpack Media3, puoi impostare di conseguenza le preferenze dei pulsanti multimediali della sessione e i comandi disponibili dei controller:

  1. Crea un MediaSession e definisci le preferenze dei pulsanti multimediali per i pulsanti dei comandi personalizzati.

  2. In MediaSession.Callback.onConnect(), autorizza i controller definendo i comandi disponibili, inclusi i comandi personalizzati, in ConnectionResult.

  3. 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(CommandButton.ICON_HEART_UNFILLED)
        .setDisplayName("Save to favorites")
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setMediaButtonPreferences(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)
      .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(CommandButton.ICON_HEART_UNFILLED)
            .setDisplayName("Save to favorites")
            .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())
            .setMediaButtonPreferences(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)
          .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);
    }
  }
}

Per scoprire di più sulla configurazione di MediaSession in modo che i client come il sistema possano connettersi alla tua app multimediale, consulta Concedere il controllo ad altri client.

Con Jetpack Media3, quando implementi un MediaSession, il tuo PlaybackState viene aggiornato automaticamente con il player multimediale. Allo stesso modo, quando implementi un MediaSessionService, la libreria pubblica automaticamente una MediaStyle notifica e la mantiene aggiornata.

Rispondere ai pulsanti di azione

Quando un utente tocca un pulsante di azione nei controlli multimediali di sistema, il sistema's MediaController invia un comando di riproduzione al tuo MediaSession. Il MediaSession delega quindi questi comandi al player. I comandi definiti nell'interfaccia Player `Player` di Media3 vengono gestiti automaticamente dalla sessione multimediale.

Per indicazioni su come rispondere a un comando personalizzato, consulta Aggiungere comandi personalizzati.

Supportare 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. Quando inizia la riproduzione, l'utente interagisce con i controlli multimediali nel solito modo.

La funzionalità di ripresa della riproduzione può essere attivata e disattivata utilizzando l'app Impostazioni, nelle opzioni Audio > Contenuti multimediali. L'utente può anche accedere alle Impostazioni by toccando l'icona a forma di ingranaggio che viene visualizzata dopo aver scorrendo il carosello espanso.

Media3 offre API per semplificare il supporto della ripresa dei contenuti multimediali. Per indicazioni sull'implementazione di questa funzionalità, consulta la documentazione Ripresa della riproduzione con Media3.

Utilizzare le API multimediali legacy

Questa sezione spiega come eseguire l'integrazione con i controlli multimediali di sistema utilizzando le API MediaCompat legacy.

Il sistema recupera le seguenti informazioni da MediaSession's MediaMetadata 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 avanzamento non mostra l'avanzamento)

Per assicurarti di avere una notifica di controllo multimediale valida e accurata, imposta il valore dei metadati METADATA_KEY_TITLE o METADATA_KEY_DISPLAY_TITLE sul titolo dei contenuti multimediali attualmente in riproduzione.

Il player multimediale mostra il tempo trascorso per i contenuti multimediali attualmente in riproduzione, insieme a una barra di avanzamento mappata a MediaSession PlaybackState.

Il player multimediale mostra l'avanzamento dei contenuti multimediali attualmente in riproduzione, insieme a una barra di avanzamento mappata a MediaSession PlaybackState. La barra di avanzamento consente agli utenti di modificare la posizione e visualizza il tempo trascorso per l'elemento multimediale. Affinché la barra di avanzamento sia abilitata, devi implementare PlaybackState.Builder#setActions e includere ACTION_SEEK_TO.

Slot Azione Criteri
1 Riproduci Lo stato attuale di PlaybackState è uno dei seguenti:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Rotellina di caricamento Lo stato attuale di PlaybackState è uno dei seguenti:
  • STATE_CONNECTING
  • STATE_BUFFERING
Metti in pausa Lo stato attuale di state del PlaybackState non è nessuno dei precedenti.
2 Indietro PlaybackState azioni includono ACTION_SKIP_TO_PREVIOUS.
Personalizzato PlaybackState azioni non includono ACTION_SKIP_TO_PREVIOUS e PlaybackState azioni personalizzate includono un'azione personalizzata che non è ancora stata inserita.
Vuoto PlaybackState extra includono un valore booleano true per la chiave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Avanti PlaybackState azioni includono ACTION_SKIP_TO_NEXT.
Personalizzato PlaybackState azioni non includono ACTION_SKIP_TO_NEXT e PlaybackState azioni personalizzate includono un'azione personalizzata che non è ancora stata inserita.
Vuoto PlaybackState extra includono un valore booleano true per la chiave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Personalizzato PlaybackState azioni personalizzate includono un'azione personalizzata che non è ancora stata inserita.
5 Personalizzato PlaybackState azioni personalizzate includono un'azione personalizzata che non è ancora stata inserita.

Aggiungere azioni standard

I seguenti esempi di codice illustrano come aggiungere azioni standard e personalizzate.PlaybackState

Per riprodurre, mettere in pausa, andare al brano precedente e al brano successivo, imposta queste azioni in il 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 visualizzare pulsanti negli slot precedenti o successivi, non aggiungere ACTION_SKIP_TO_PREVIOUS o ACTION_SKIP_TO_NEXT, ma aggiungi extra a la 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);

Aggiungere azioni personalizzate

Per altre azioni che vuoi mostrare sui controlli multimediali, puoi creare un PlaybackStateCompat.CustomAction e aggiungerlo a PlaybackState invece. 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 di PlaybackState

Quando un utente tocca un pulsante, SystemUI utilizza MediaController.TransportControls per inviare un comando a MediaSession. Devi registrare un callback in grado di 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 dei contenuti multimediali

Per fare in modo che la tua app player venga visualizzata nell'area delle impostazioni 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 del player multimediale, utilizza NotificationBuilder.setSmallIcon().

Per supportare la ripresa della riproduzione, le app devono implementare un MediaBrowserService e MediaSession. Il tuo MediaSession deve implementare il callback onPlay().

Implementazione di MediaBrowserService

Dopo l'avvio del dispositivo, il sistema cerca le cinque app multimediali utilizzate più di recente e fornisce 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. La tua 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. SystemUI è firmato con la firma della piattaforma. Un esempio di come verificare la firma della piattaforma è disponibile nell' app UAMP.

Per supportare la ripresa della riproduzione, il tuo MediaBrowserService deve implementare questi comportamenti:

  • onGetRoot() deve restituire rapidamente una radice non nulla. Altre logiche complesse devono essere gestite in onLoadChildren()

  • Quando onLoadChildren() viene chiamato sull'ID multimediale radice, il risultato deve contenere un elemento secondario FLAG_PLAYABLE.

  • MediaBrowserService deve restituire l'elemento multimediale riprodotto più di recente quando riceve una query EXTRA_RECENT. Il valore restituito deve essere un elemento multimediale effettivo anziché una funzione generica.

  • MediaBrowserService deve fornire un appropriato MediaDescription con un titolo e un sottotitolo non vuoti. Deve 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);
}

Comportamento precedente ad Android 13

Per la compatibilità con le versioni precedenti, l'interfaccia utente di sistema continua a fornire un layout alternativo che utilizza le azioni di notifica per le app che non vengono aggiornate per il targeting di Android 13, o che non includono informazioni su PlaybackState. I pulsanti di azione vengono derivati dall'elenco Notification.Action allegato alla MediaStyle notifica. Il sistema visualizza fino a cinque azioni nell'ordine in cui sono state aggiunte. In modalità compatta, vengono visualizzati fino a tre pulsanti, determinati dai valori passati 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)
// 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(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
.setShowActionsInCompactView(1 /* #1: pause button */))
.setContentTitle("Wonderful music")
.setContentText("My Awesome Band")
.setLargeIcon(albumArtBitmap)
.build();