Riproduzione in background con MediaSessionService

Spesso è preferibile riprodurre contenuti multimediali quando un'app non è in primo piano. Ad esempio, un lettore musicale in genere continua a riprodurre musica quando l'utente ha bloccato il dispositivo o utilizza un'altra app. La libreria Media3 fornisce una serie di interfacce che consentono di supportare la riproduzione in background.

Utilizza un MediaSessionService

Per attivare la riproduzione in background, devi inserire Player e MediaSession all'interno di un Service separato. In questo modo, il dispositivo può continuare a riprodurre contenuti multimediali anche quando l'app non è in primo piano.

MediaSessionService consente alla sessione multimediale di essere eseguita separatamente dall'attività dell'app
Figura 1: MediaSessionService consente alla sessione multimediale di essere eseguita separatamente dall'attività dell'app

Quando ospiti un player all'interno di un servizio, devi utilizzare un MediaSessionService. Per farlo, crea una classe che estenda MediaSessionService e crea la tua sessione multimediale al suo interno.

L'utilizzo di MediaSessionService consente a client esterni come l'Assistente Google, i controlli multimediali di sistema, i pulsanti multimediali su dispositivi periferici o dispositivi complementari come Wear OS di rilevare il tuo servizio, connettersi e controllare la riproduzione, il tutto senza accedere all'attività della UI della tua app. In effetti, possono essere presenti più app client connesse allo stesso MediaSessionService contemporaneamente, ognuna con il proprio MediaController.

Implementa il ciclo di vita del servizio

Devi implementare due metodi del ciclo di vita del tuo servizio:

  • onCreate() viene chiamato quando il primo controller sta per connettersi e il servizio viene istanziato e avviato. È il posto migliore per creare Player e MediaSession.
  • onDestroy() viene chiamato quando il servizio viene interrotto. Tutte le risorse, inclusi il player e la sessione, devono essere rilasciate.

Se vuoi, puoi eseguire l'override di onTaskRemoved(Intent) per personalizzare ciò che accade quando l'utente chiude l'app dalle attività recenti. Per impostazione predefinita, il servizio rimane in esecuzione se la riproduzione è in corso e viene interrotto in caso contrario.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;

  // Create your Player and MediaSession in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaSession = new MediaSession.Builder(this, player).build();
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

In alternativa a mantenere la riproduzione in background, puoi interrompere il servizio in ogni caso quando l'utente chiude l'app:

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

Per qualsiasi altra implementazione manuale di onTaskRemoved, puoi utilizzare isPlaybackOngoing() per verificare se la riproduzione è considerata in corso e se il servizio in primo piano è avviato.

Fornisci l'accesso alla sessione multimediale

Esegui l'override del metodo onGetSession() per consentire ad altri client di accedere alla sessione multimediale creata durante la creazione del servizio.

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Dichiara il servizio nel manifest

Un'app richiede le autorizzazioni FOREGROUND_SERVICE e FOREGROUND_SERVICE_MEDIA_PLAYBACK per eseguire un servizio in primo piano di riproduzione:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

Devi anche dichiarare la classe Service nel manifest con un filtro per intent di MediaSessionService e un foregroundServiceType che includa mediaPlayback.

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

Controlla la riproduzione utilizzando un MediaController

Nell'attività o nel frammento contenente la UI del player, puoi stabilire un collegamento tra la UI e la sessione multimediale utilizzando un MediaController. La UI utilizza il controller multimediale per inviare comandi dalla UI al player all'interno della sessione. Consulta la guida Crea un MediaController per informazioni dettagliate sulla creazione e sull'utilizzo di un MediaController.

Gestisci i comandi MediaController

MediaSession riceve i comandi dal controller tramite MediaSession.Callback. L'inizializzazione di un MediaSession crea un'implementazione predefinita di MediaSession.Callback che gestisce automaticamente tutti i comandi che un MediaController invia al tuo player.

Notifica

Un MediaSessionService crea automaticamente un MediaNotification che dovrebbe funzionare nella maggior parte dei casi. Per impostazione predefinita, la notifica pubblicata è una notifica MediaStyle che rimane aggiornata con le informazioni più recenti della sessione multimediale e mostra i controlli di riproduzione. MediaNotification è a conoscenza della tua sessione e può essere utilizzato per controllare la riproduzione di qualsiasi altra app connessa alla stessa sessione.

Ad esempio, un'app di streaming musicale che utilizza un MediaSessionService creerebbe un MediaNotification che mostra il titolo, l'artista e la copertina dell'album per l'elemento multimediale attualmente in riproduzione, insieme ai controlli di riproduzione basati sulla configurazione di MediaSession.

I metadati richiesti possono essere forniti nell'elemento multimediale o dichiarati come parte dell'elemento multimediale, come nello snippet seguente:

Kotlin

val mediaItem =
  MediaItem.Builder()
    .setMediaId("media-1")
    .setUri(mediaUri)
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setArtist("David Bowie")
        .setTitle("Heroes")
        .setArtworkUri(artworkUri)
        .build()
    )
    .build()

mediaController.setMediaItem(mediaItem)
mediaController.prepare()
mediaController.play()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaId("media-1")
        .setUri(mediaUri)
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setArtist("David Bowie")
                .setTitle("Heroes")
                .setArtworkUri(artworkUri)
                .build())
        .build();

mediaController.setMediaItem(mediaItem);
mediaController.prepare();
mediaController.play();

Ciclo di vita delle notifiche

La notifica viene creata non appena Player ha MediaItem istanze nella sua playlist.

Tutti gli aggiornamenti delle notifiche vengono eseguiti automaticamente in base allo stato di Player e MediaSession.

La notifica non può essere rimossa mentre il servizio in primo piano è in esecuzione. Per rimuovere immediatamente la notifica, devi chiamare Player.release() o cancellare la playlist utilizzando Player.clearMediaItems().

Se il player viene messo in pausa, interrotto o non funziona per più di 10 minuti senza ulteriori interazioni dell'utente, il servizio viene automaticamente rimosso dallo stato del servizio in primo piano in modo che possa essere eliminato dal sistema. Puoi implementare la ripresa della riproduzione per consentire a un utente di riavviare il ciclo di vita del servizio e riprendere la riproduzione in un secondo momento.

Personalizzazione delle notifiche

I metadati relativi all'elemento attualmente in riproduzione possono essere personalizzati modificando MediaItem.MediaMetadata. Se vuoi aggiornare i metadati di un elemento esistente, puoi utilizzare Player.replaceMediaItem per aggiornare i metadati senza interrompere la riproduzione.

Puoi anche personalizzare alcuni dei pulsanti mostrati nella notifica impostando le preferenze dei pulsanti multimediali personalizzati per i controlli multimediali di Android. Scopri di più sulla personalizzazione dei controlli multimediali di Android.

Per personalizzare ulteriormente la notifica, crea un MediaNotification.Provider con DefaultMediaNotificationProvider.Builder o creando un'implementazione personalizzata dell'interfaccia del provider. Tocca a te provider al tuo MediaSessionService con setMediaNotificationProvider.

Ripresa della riproduzione

Dopo l'interruzione di MediaSessionService e anche dopo il riavvio del dispositivo, è possibile offrire la ripresa della riproduzione per consentire agli utenti di riavviare il servizio e riprendere la riproduzione da dove l'avevano interrotta. Per impostazione predefinita, la ripresa della riproduzione è disattivata. Ciò significa che l'utente non può riprendere la riproduzione quando il servizio non è in esecuzione. Per attivare questa funzionalità, devi dichiarare un ricevitore di pulsanti multimediali e implementare il metodo onPlaybackResumption.

Dichiara il ricevitore dei pulsanti multimediali Media3

Inizia dichiarando MediaButtonReceiver nel file manifest:

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

Implementa il callback di ripresa della riproduzione

Quando la ripresa della riproduzione viene richiesta da un dispositivo Bluetooth o dalla funzionalità di ripresa della UI di sistema di Android, viene chiamato il metodo di callback onPlaybackResumption().

Kotlin

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(
      () -> {
        // Your app is responsible for storing the playlist, metadata (like title
        // and artwork) of the current item and the start position to use here.
        MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
        settableFuture.set(resumptionPlaylist);
      },
      MoreExecutors.directExecutor());
  return settableFuture;
}

Se hai memorizzato altri parametri, come la velocità di riproduzione, la modalità repeat o la modalità shuffle, onPlaybackResumption() è un buon punto per configurare il player con questi parametri prima che Media3 lo prepari e avvii la riproduzione al termine del callback.

Questo metodo viene chiamato durante l'avvio per creare la notifica di ripresa della UI di sistema Android dopo un riavvio del dispositivo con isForPlayback impostato su false. Per una notifica avanzata, è consigliabile compilare i campi MediaMetadata come title e artworkData o artworkUri dell'elemento corrente con valori disponibili localmente, poiché l'accesso alla rete potrebbe non essere ancora disponibile. Puoi anche aggiungere MediaConstants.EXTRAS_KEY_COMPLETION_STATUS e MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE a MediaMetadata.extras per indicare la posizione di riproduzione da cui riprendere.

Configurazione avanzata del controller e compatibilità con le versioni precedenti

Uno scenario comune è l'utilizzo di un MediaController nella UI dell'app per controllare la riproduzione e visualizzare la playlist. Allo stesso tempo, la sessione viene esposta a client esterni come i controlli multimediali di Android e l'assistente su dispositivi mobili o TV, Wear OS per gli smartwatch e Android Auto nelle auto. L'app demo della sessione Media3 è un esempio di app che implementa questo scenario.

Questi client esterni possono utilizzare API come MediaControllerCompat della libreria AndroidX legacy o android.media.session.MediaController della piattaforma Android. Media3 è completamente compatibile con le versioni precedenti della libreria legacy e fornisce l'interoperabilità con l'API della piattaforma Android.

Identifica i controller attendibili

Qualsiasi app può tentare di connettersi alla tua sessione o raccolta multimediale. Se vuoi limitare l'accesso ai controller di sistema, ai controller con autorizzazione di controllo dei contenuti multimediali e alla tua app, puoi utilizzare ControllerInfo.isTrusted() per un controllo di accesso di base. In alternativa, puoi identificare controller più specifici, come il controller delle notifiche multimediali o i controller di Android Auto, come descritto nelle sezioni seguenti.

Utilizza il controller delle notifiche multimediali

È importante capire che questi controller legacy e della piattaforma condividono lo stesso stato e la visibilità non può essere personalizzata in base al controller (ad esempio, PlaybackState.getActions() e PlaybackState.getCustomActions() disponibili). Puoi utilizzare il controller delle notifiche multimediali per configurare lo stato impostato nella sessione multimediale della piattaforma per la compatibilità con questi controller legacy e della piattaforma.

Ad esempio, un'app può fornire un'implementazione di MediaSession.Callback.onConnect() per impostare i comandi disponibili e le preferenze dei pulsanti multimediali in modo specifico per la sessione della piattaforma, come segue:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    val playerCommands =
      ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
        .remove(COMMAND_SEEK_TO_PREVIOUS)
        .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
        .remove(COMMAND_SEEK_TO_NEXT)
        .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
        .build()
    // Custom button preferences and commands to configure the platform session.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    Player.Commands playerCommands =
        ConnectionResult.DEFAULT_PLAYER_COMMANDS
            .buildUpon()
            .remove(COMMAND_SEEK_TO_PREVIOUS)
            .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .remove(COMMAND_SEEK_TO_NEXT)
            .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .build();
    // Custom button preferences and commands to configure the platform session.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

Autorizza Android Auto a inviare comandi personalizzati

Quando utilizzi un MediaLibraryService e per supportare Android Auto con l'app mobile, il controller di Android Auto richiede i comandi disponibili appropriati, altrimenti Media3 negherebbe i comandi personalizzati in arrivo da quel controller:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

L'app demo della sessione ha un modulo automotive, che dimostra il supporto di Automotive OS, che richiede un APK separato.