Controllare e pubblicizzare la riproduzione utilizzando una MediaSession

Le sessioni multimediali offrono un modo universale di interagire con un lettore audio o video. In Media3, il player predefinito è la classe ExoPlayer, che implementa l'interfaccia Player. La connessione della sessione multimediale al player consente a un'app di pubblicizzare la riproduzione di contenuti multimediali esternamente e di ricevere comandi di riproduzione da sorgenti esterne.

I comandi possono provenire da pulsanti fisici, come il pulsante di riproduzione sulle cuffie o sul telecomando della TV. Potrebbero provenire anche da app client che dispongono di un controller multimediale, ad esempio l'istruzione di "mettere in pausa" all'Assistente Google. La sessione multimediale delega questi comandi al player dell'app multimediale.

Quando scegliere una sessione multimediale

Quando implementi MediaSession, consenti agli utenti di controllare la riproduzione:

  • Tramite le cuffie. Spesso ci sono pulsanti o interazioni touch che un utente può eseguire sulle cuffie per riprodurre o mettere in pausa i contenuti multimediali o per passare alla traccia successiva o precedente.
  • Parlando con l'Assistente Google. In genere, pronuncia "Hey Google, metti in pausa" per mettere in pausa i contenuti multimediali attualmente in riproduzione sul dispositivo.
  • Tramite l'orologio Wear OS. Ciò consente di accedere più facilmente ai controlli di riproduzione più comuni durante la riproduzione sullo smartphone.
  • Tramite i Controlli multimediali. Questo carosello mostra i controlli per ogni sessione multimediale in esecuzione.
  • In TV. Consente azioni con pulsanti di riproduzione fisici, controllo della riproduzione della piattaforma e gestione dell'alimentazione (ad esempio se la TV, la soundbar o il ricevitore A/V si spengono o l'ingresso viene cambiato, la riproduzione dovrebbe interrompersi nell'app).
  • Qualsiasi altro processo esterno che deve influenzare la riproduzione.

È un'ottima soluzione per molti casi d'uso. In particolare, ti consigliamo di utilizzare MediaSession se:

  • Stai trasmettendo in streaming contenuti video di lunga durata, come film o TV in diretta.
  • Stai trasmettendo in streaming contenuti audio di lunga durata, come podcast o playlist musicali.
  • Stai creando un'app TV.

Tuttavia, non tutti i casi d'uso si adattano bene a MediaSession. Ti consigliamo di utilizzare solo Player nei seguenti casi:

  • Pubblichi contenuti nel formato breve, per i quali il coinvolgimento e l'interazione degli utenti sono fondamentali.
  • Non è presente un singolo video attivo, ad esempio un utente scorre un elenco e sullo schermo vengono visualizzati più video contemporaneamente.
  • Stai riproducendo un video introduttivo o esplicativo una tantum, che prevedi che l'utente guardi attivamente.
  • I tuoi contenuti sono sensibili alla privacy e non vuoi che processi esterni accedano ai metadati multimediali (ad esempio la modalità di navigazione in incognito in un browser)

Se il tuo caso d'uso non rientra in nessuno di quelli elencati sopra, valuta se sei d'accordo a far continuare la riproduzione della tua app quando l'utente non sta interagendo attivamente con i contenuti. Se la risposta è sì, probabilmente vorrai scegliere MediaSession. Se la risposta è no, ti consigliamo di utilizzare invece Player.

Creare una sessione multimediale

Una sessione multimediale è affiancata al player che gestisce. Puoi creare una sessione multimediale con un oggetto Context e un oggetto Player. Devi creare e inizializzare una sessione multimediale quando necessario, ad esempio il metodo del ciclo di vita onStart() o onResume() di Activity o Fragment oppure onCreate() del metodo Service che è proprietario della sessione multimediale e del player associato.

Per creare una sessione multimediale, inizializza un Player e forniscilo a MediaSession.Builder in questo modo:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Gestione automatica dello stato

La libreria Media3 aggiorna automaticamente la sessione multimediale utilizzando lo stato del player. Di conseguenza, non è necessario gestire manualmente la mappatura dal player alla sessione.

Questa è un'interruzione dell'approccio precedente in cui dovevi creare e gestire un PlaybackStateCompat indipendentemente dal player stesso, ad esempio per indicare eventuali errori.

ID sessione univoco

Per impostazione predefinita, MediaSession.Builder crea una sessione con una stringa vuota come ID sessione. Questo è sufficiente se un'app intende creare una sola istanza di sessione, il che è il caso più comune.

Se un'app desidera gestire più istanze di sessione contemporaneamente, deve garantire che l'ID sessione di ogni sessione sia univoco. L'ID sessione può essere impostato durante la creazione della sessione con MediaSession.Builder.setId(String id).

Se noti un IllegalStateException che causa l'arresto anomalo della tua app con il messaggio di errore IllegalStateException: Session ID must be unique. ID=, è probabile che sia stata creata inaspettatamente una sessione prima del rilascio di un'istanza creata in precedenza con lo stesso ID. Per evitare che le sessioni vengano divulgate a causa di un errore di programmazione, questi casi vengono rilevati e notificati generando un'eccezione.

Concedi il controllo ad altri clienti

La sessione multimediale è la chiave per controllare la riproduzione. Ti consente di indirizzare i comandi da sorgenti esterne al player che si occupa della riproduzione dei tuoi contenuti multimediali. Queste origini possono essere pulsanti fisici come il pulsante di riproduzione su un auricolare o sul telecomando della TV, oppure comandi indiretti come l'indicazione di "metti in pausa" all'Assistente Google. Analogamente, puoi concedere l'accesso al sistema Android per facilitare i controlli delle notifiche e alla schermata di blocco o a uno smartwatch Wear OS per controllare la riproduzione dal quadrante. I client esterni possono utilizzare un controller multimediale per inviare comandi di riproduzione alla tua app multimediale. Questi vengono ricevuti dalla sessione multimediale, che alla fine delega i comandi al media player.

Diagramma che mostra l'interazione tra MediaSession e MediaController.
Figura 1: il controller multimediale facilita il passaggio dei comandi da origini esterne alla sessione multimediale.

Quando un controller sta per connettersi alla tua sessione multimediale, viene chiamato il metodo onConnect(). Puoi utilizzare l'elemento ControllerInfo fornito per decidere se accettare o rifiutare la richiesta. Consulta un esempio di accettazione di una richiesta di connessione nella sezione Dichiara i comandi disponibili.

Dopo la connessione, un controller può inviare comandi di riproduzione alla sessione. La sessione delega questi comandi al player. I comandi per la riproduzione e la playlist definiti nell'interfaccia Player vengono gestiti automaticamente dalla sessione.

Altri metodi di callback ti consentono di gestire, ad esempio, le richieste di comandi di riproduzione personalizzati e di modifica della playlist. In modo simile, questi callback includono un oggetto ControllerInfo che ti consente di modificare il modo in cui rispondi a ogni richiesta in base al singolo controller.

Modificare la playlist

Una sessione multimediale può modificare direttamente la playlist del proprio player come spiegato nella guida di ExoPlayer per le playlist. I controller possono anche modificare la playlist se è disponibile COMMAND_SET_MEDIA_ITEM o COMMAND_CHANGE_MEDIA_ITEMS.

Quando aggiungi nuovi elementi alla playlist, il player di solito richiede istanze MediaItem con un URI definito per poterle riprodurre. Per impostazione predefinita, gli elementi appena aggiunti vengono inoltrati automaticamente ai metodi del player come player.addMediaItem se hanno un URI definito.

Se vuoi personalizzare le istanze MediaItem aggiunte al player, puoi eseguire l'override di onAddMediaItems(). Questo passaggio è necessario se vuoi supportare i controller che richiedono contenuti multimediali senza un URI definito. In genere, MediaItem ha uno o più dei seguenti campi impostati per descrivere il contenuto multimediale richiesto:

  • MediaItem.id: un ID generico che identifica il contenuto multimediale.
  • MediaItem.RequestMetadata.mediaUri: un URI della richiesta che può utilizzare uno schema personalizzato e non può essere necessariamente riprodotto direttamente dal player.
  • MediaItem.RequestMetadata.searchQuery: una query di ricerca testuale, ad esempio dell'Assistente Google.
  • MediaItem.MediaMetadata: metadati strutturati come "titolo" o "artista".

Per ulteriori opzioni di personalizzazione per playlist completamente nuove, puoi anche sostituire onSetMediaItems(), che ti consente di definire l'elemento iniziale e la posizione nella playlist. Ad esempio, puoi espandere un singolo elemento richiesto a un'intera playlist e indicare al player di iniziare dall'indice dell'elemento richiesto inizialmente. Un'implementazione di esempio di onSetMediaItems() con questa funzionalità è disponibile nell'app demo della sessione.

Gestire un layout personalizzato e i comandi personalizzati

Le seguenti sezioni descrivono come pubblicizzare un layout personalizzato dei pulsanti di comando personalizzati nelle app client e autorizzare i controller a inviare i comandi personalizzati.

Definisci il layout personalizzato della sessione

Per indicare alle app client quali controlli di riproduzione vuoi mostrare all'utente, imposta il layout personalizzato della sessione durante la creazione di MediaSession nel metodo onCreate() del servizio.

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

Dichiara i comandi personalizzati e del player disponibili

Le applicazioni multimediali possono definire comandi personalizzati, ad esempio in un layout personalizzato. Ad esempio, potresti voler implementare pulsanti che permettono all'utente di salvare un elemento multimediale in un elenco di elementi preferiti. L'MediaController invia comandi personalizzati e il MediaSession.Callback li riceve.

Puoi definire quali comandi di sessione personalizzati sono disponibili per un elemento MediaController quando si connette alla sessione multimediale. Per ottenere questo risultato, esegui l'override di MediaSession.Callback.onConnect(). Configura e restituisci il set di comandi disponibili quando accetti una richiesta di connessione da un MediaController nel metodo di callback onConnect:

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Per ricevere richieste di comandi personalizzate da un MediaController, sostituisci il metodo onCustomCommand() in Callback.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Puoi monitorare quale controller multimediale effettua una richiesta utilizzando la proprietà packageName dell'oggetto MediaSession.ControllerInfo che viene passata nei metodi Callback. Questo ti consente di personalizzare il comportamento dell'app in risposta a un determinato comando, se proviene dal sistema, dalla tua app o da altre app client.

Aggiornare il layout personalizzato dopo un'interazione da parte di un utente

Dopo aver gestito un comando personalizzato o qualsiasi altra interazione con il player, ti consigliamo di aggiornare il layout visualizzato nell'interfaccia utente del controller. Un esempio tipico è un pulsante di attivazione/disattivazione che cambia l'icona dopo aver attivato l'azione associata al pulsante. Per aggiornare il layout, puoi utilizzare MediaSession.setCustomLayout:

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

Personalizza il comportamento del comando di riproduzione

Per personalizzare il comportamento di un comando definito nell'interfaccia Player, ad esempio play() o seekToNext(), aggrega il tuo Player in un ForwardingPlayer.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession = 
  new MediaSession.Builder(context, forwardingPlayer).build();

Per maggiori informazioni su ForwardingPlayer, consulta la Personalizzazione della guida di ExoPlayer.

Identificare il controller richiedente del comando di un player

Quando una chiamata a un metodo Player proviene da un MediaController, puoi identificare l'origine dell'origine con MediaSession.controllerForCurrentRequest e acquisire ControllerInfo per la richiesta corrente:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

Rispondi ai pulsanti multimediali

I pulsanti multimediali sono pulsanti hardware presenti su dispositivi Android e altri dispositivi periferici, ad esempio il pulsante di riproduzione/pausa sulle cuffie Bluetooth. Media3 gestisce al posto tuo gli eventi del pulsante multimediale quando arriva alla sessione e chiama il metodo Player appropriato sul player di sessione.

Un'app può eseguire l'override del comportamento predefinito eseguendo l'override di MediaSession.Callback.onMediaButtonEvent(Intent). In tal caso, l'app può o deve gestire da sola tutte le specifiche dell'API.