Riproduzione in background con MediaSessionService

Spesso è preferibile riprodurre contenuti multimediali quando un'app non è in primo piano. Ad esempio, in genere un music player 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 la riproduzione in background.

Utilizzo di un MediaSessionService

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

MediaSessionService consente l'esecuzione della sessione multimediale separatamente dall'attività dell'app
Figura 1: MediaSessionService consente di eseguire la sessione multimediale separatamente dall'attività dell'app

Quando ospiti un player all'interno di un servizio, dovresti usare un MediaSessionService. Per farlo, crea un corso che estenda MediaSessionService" e crea la tua sessione multimediale al suo interno.

L'utilizzo di MediaSessionService consente a clienti esterni come l'Assistente Google, i controlli multimediali di sistema o dispositivi complementari come Wear OS di scoprire il tuo servizio, connettersi al servizio e controllare la riproduzione, il tutto senza accedere all'attività dell'interfaccia utente della tua app. Di fatto, possono esserci più app client collegate alla stessa MediaSessionService contemporaneamente, ogni app con il proprio MediaController.

Implementare il ciclo di vita dei servizi

Devi implementare tre metodi del ciclo di vita del servizio:

  • onCreate() viene chiamato quando il primo controller sta per connettersi e viene creata un'istanza e il servizio viene avviato. È il posto migliore per creare Player e MediaSession.
  • onTaskRemoved(Intent) viene richiamata quando l'utente chiude l'app dalle attività recenti. Se la riproduzione è in corso, l'app può scegliere di mantenere il servizio in esecuzione in primo piano. Se il player è in pausa, il servizio non è in primo piano e deve essere interrotto.
  • onDestroy() viene chiamato quando il servizio viene interrotto. Tutte le risorse, inclusi player e sessione, devono essere rilasciate.

Kotlin

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

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

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

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

Java

public 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();
  }

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

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

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

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

Fornisci l'accesso alla sessione multimediale

Esegui l'override del metodo onGetSession() per concedere ad altri client l'accesso alla tua sessione multimediale creata al momento della creazione del servizio.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

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

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] lifecycle methods omitted

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

Dichiara il servizio nel manifest

Un'app richiede l'autorizzazione per eseguire un servizio in primo piano. Aggiungi l'autorizzazione FOREGROUND_SERVICE al manifest e, se scegli come target l'API 34 e versioni successive, anche FOREGROUND_SERVICE_MEDIA_PLAYBACK:

<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.

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

Devi definire un foregroundServiceType che includa mediaPlayback quando la tua app è in esecuzione su un dispositivo con Android 10 (livello API 29) e versioni successive.

Controlla la riproduzione usando un MediaController

Nell'Attività o nel frammento che contiene la tua UI del player, puoi stabilire un collegamento tra l'UI e la tua sessione multimediale utilizzando un MediaController. L'interfaccia utente utilizza il controller multimediale per inviare comandi dalla tua UI al player all'interno della sessione. Consulta la guida Creare una MediaController per i dettagli sulla creazione e sull'utilizzo di un MediaController.

Gestire i comandi UI

MediaSession riceve i comandi dal controller tramite il suo MediaSession.Callback. L'inizializzazione di un elemento MediaSession crea un'implementazione predefinita di MediaSession.Callback che gestisce automaticamente tutti i comandi inviati da MediaController 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 di MediaStyle che rimane aggiornata con le informazioni più recenti relative alla sessione multimediale e mostra i controlli di riproduzione. L'MediaNotification conosce la tua sessione e può essere utilizzato per controllare la riproduzione di qualsiasi altra app collegata alla stessa sessione.

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

I metadati richiesti possono essere forniti nei contenuti multimediali o dichiarati come parte dell'elemento multimediale come nel seguente snippet:

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();

Le app possono personalizzare i pulsanti di comando dei controlli di Android Media. Scopri di più sulla personalizzazione dei controlli multimediali di Android.

Personalizzazione delle notifiche

Per personalizzare la notifica, crea un MediaNotification.Provider con DefaultMediaNotificationProvider.Builder o crea un'implementazione personalizzata dell'interfaccia del provider. Aggiungi il tuo provider a MediaSessionService con setMediaNotificationProvider.

Ripresa della riproduzione

I pulsanti multimediali sono pulsanti hardware presenti su dispositivi Android e altri dispositivi periferici, ad esempio il pulsante di riproduzione o pausa sulle cuffie Bluetooth. Media3 gestisce gli input dei pulsanti multimediali quando il servizio è in esecuzione.

Dichiara il ricevitore del pulsante multimediale Media3

Media3 include un'API che consente agli utenti di riprendere la riproduzione dopo l'arresto di un'app e anche dopo il riavvio del dispositivo. 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 la funzionalità, inizia dichiarando MediaButtonReceiver nel manifest:

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

Implementare il callback di ripresa della riproduzione

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

Kotlin

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    val resumptionPlaylist = restorePlaylist()
    settable.set(resumptionPlaylist)
  }
  return settable
}

Java

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

Se hai memorizzato altri parametri come velocità di riproduzione, modalità di ripetizione o modalità casuale, onPlaybackResumption() è ideale per configurare il player con questi parametri prima che Media3 prepara il player e avvii la riproduzione al termine del callback.

Configurazione avanzata del controller e compatibilità con le versioni precedenti

Uno scenario comune è l'utilizzo di un MediaController nell'interfaccia utente dell'app per controllare la riproduzione e visualizzare la playlist. Allo stesso tempo, la sessione è esposta a client esterni come i controlli multimediali di Android e l'assistente su dispositivi mobili o TV, Wear OS per orologi 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 Android precedente o android.media.session.MediaController del framework Android. Media3 è completamente compatibile con le versioni precedenti della libreria e fornisce l'interoperabilità con l'API Android Framework.

Utilizzare il controller di notifica dei contenuti multimediali

È importante capire che questi controller legacy o del framework leggono gli stessi valori dei framework PlaybackState.getActions() e PlaybackState.getCustomActions(). Per determinare azioni e azioni personalizzate della sessione del framework, un'app può utilizzare il controller di notifica dei contenuti multimediali e impostare i comandi disponibili e il layout personalizzato. Il servizio connette il controller delle notifiche multimediali alla sessione e la sessione utilizza il ConnectionResult restituito dal onConnect() del callback per configurare azioni e azioni personalizzate della sessione del framework.

Dato uno scenario solo per dispositivi mobili, un'app può fornire un'implementazione di MediaSession.Callback.onConnect() per impostare i comandi disponibili e un layout personalizzato appositamente per la sessione del framework, come indicato di seguito:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

Autorizza Android Auto a inviare comandi personalizzati

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

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS
          .buildUpon()
          .add(customCommandSeekBackward)
          .add(customCommandSeekForward)
          .build();
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

L'app demo sessione ha un modulo Automotive che dimostra il supporto del sistema operativo Automotive che richiede un APK separato.