Pemutaran di latar belakang dengan MediaSessionService

Sering kali diinginkan untuk memutar media saat aplikasi tidak berada di latar depan. Misalnya, pemutar musik umumnya terus memutar musik saat pengguna telah mengunci perangkatnya atau menggunakan aplikasi lain. Library Media3 menyediakan serangkaian antarmuka yang memungkinkan Anda mendukung pemutaran di latar belakang.

Menggunakan MediaSessionService

Untuk mengaktifkan pemutaran di latar belakang, Anda harus menyertakan Player dan MediaSession dalam Service terpisah. Hal ini memungkinkan perangkat untuk terus menyajikan media meskipun aplikasi Anda tidak berada di latar depan.

MediaSessionService memungkinkan sesi media berjalan secara terpisah dari aktivitas aplikasi
Gambar 1: MediaSessionService memungkinkan sesi media berjalan secara terpisah dari aktivitas aplikasi

Saat menghosting pemain di dalam Layanan, Anda harus menggunakan MediaSessionService. Untuk melakukannya, buat class yang memperluas MediaSessionService dan buat sesi media di dalamnya.

Penggunaan MediaSessionService memungkinkan klien eksternal seperti Asisten Google, kontrol media sistem, tombol media di perangkat periferal, atau perangkat pendamping seperti Wear OS untuk menemukan layanan Anda, terhubung ke layanan tersebut, dan mengontrol pemutaran, semuanya tanpa mengakses aktivitas UI aplikasi Anda sama sekali. Bahkan, beberapa aplikasi klien dapat tersambung ke MediaSessionService yang sama secara bersamaan, di mana setiap aplikasi menggunakan MediaController-nya sendiri.

Menerapkan siklus proses layanan

Anda harus mengimplementasikan dua metode siklus proses layanan Anda:

  • onCreate() dipanggil saat pengontrol pertama akan terhubung dan layanan di-instantiate dan dimulai. Ini adalah tempat terbaik untuk membangun Player dan MediaSession.
  • onDestroy() dipanggil saat layanan dihentikan. Semua resource, termasuk pemutar dan sesi, harus dilepaskan.

Anda dapat mengganti onTaskRemoved(Intent) secara opsional untuk menyesuaikan apa yang terjadi saat pengguna menutup aplikasi dari tugas terbaru. Secara default, layanan tetap berjalan jika pemutaran sedang berlangsung dan dihentikan jika tidak.

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

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

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

Sebagai alternatif untuk menjaga pemutaran tetap berlangsung di latar belakang, Anda dapat menghentikan layanan dalam kasus apa pun saat pengguna menutup aplikasi:

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

Untuk penerapan onTaskRemoved manual lainnya, Anda dapat menggunakan isPlaybackOngoing() untuk memeriksa apakah pemutaran dianggap sedang berlangsung dan layanan latar depan dimulai.

Memberikan akses ke sesi media

Ganti metode onGetSession() untuk memberi klien lain akses ke sesi media Anda yang dibuat saat layanan dibuat.

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;
  }
}

Mendeklarasikan layanan dalam manifes

Aplikasi memerlukan izin FOREGROUND_SERVICE dan FOREGROUND_SERVICE_MEDIA_PLAYBACK untuk menjalankan layanan latar depan pemutaran:

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

Anda juga harus mendeklarasikan class Service dalam manifes dengan filter intent MediaSessionService dan foregroundServiceType yang menyertakan 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>

Mengontrol pemutaran menggunakan MediaController

Di Aktivitas atau Fragmen yang berisi UI pemutar, Anda dapat membuat link antara UI dan sesi media menggunakan MediaController. UI Anda menggunakan pengontrol media untuk mengirim perintah dari UI Anda ke pemutar dalam sesi. Lihat panduan Membuat MediaController untuk mengetahui detail tentang cara membuat dan menggunakan MediaController.

Menangani perintah MediaController

MediaSession menerima perintah dari pengontrol melalui MediaSession.Callback. Menginisialisasi MediaSession akan membuat implementasi default MediaSession.Callback yang otomatis menangani semua perintah yang dikirim MediaController ke pemutar Anda.

Notifikasi

MediaSessionService otomatis membuat MediaNotification untuk Anda yang seharusnya berfungsi dalam sebagian besar kasus. Secara default, notifikasi yang dipublikasikan adalah notifikasi MediaStyle yang terus diperbarui dengan informasi terbaru dari sesi media Anda dan menampilkan kontrol pemutaran. MediaNotification mengetahui sesi Anda dan dapat digunakan untuk mengontrol pemutaran aplikasi lain yang terhubung ke sesi yang sama.

Misalnya, aplikasi streaming musik yang menggunakan MediaSessionService akan membuat MediaNotification yang menampilkan judul, artis, dan sampul album untuk item media saat ini yang sedang diputar bersama kontrol pemutaran berdasarkan konfigurasi MediaSession Anda.

Metadata yang diperlukan dapat diberikan di media atau dideklarasikan sebagai bagian dari item media seperti dalam cuplikan berikut:

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

Siklus proses notifikasi

Notifikasi dibuat segera setelah Player memiliki MediaItem instance dalam playlist-nya.

Semua pembaruan notifikasi terjadi secara otomatis berdasarkan status Player dan MediaSession.

Notifikasi tidak dapat dihapus saat layanan latar depan berjalan. Untuk segera menghapus notifikasi, Anda harus memanggil Player.release() atau menghapus playlist menggunakan Player.clearMediaItems().

Jika pemutar dijeda, dihentikan, atau gagal selama lebih dari 10 menit tanpa interaksi pengguna lebih lanjut, layanan akan otomatis keluar dari status layanan latar depan sehingga dapat dihancurkan oleh sistem. Anda dapat menerapkan kelanjutan pemutaran untuk mengizinkan pengguna memulai ulang siklus proses layanan dan melanjutkan pemutaran di lain waktu.

Penyesuaian notifikasi

Metadata tentang item yang sedang diputar dapat disesuaikan dengan mengubah MediaItem.MediaMetadata. Jika ingin memperbarui metadata item yang ada, Anda dapat menggunakan Player.replaceMediaItem untuk memperbarui metadata tanpa mengganggu pemutaran.

Anda juga dapat menyesuaikan beberapa tombol yang ditampilkan di notifikasi dengan menyetel preferensi tombol media kustom untuk Kontrol media Android. Baca selengkapnya cara menyesuaikan Kontrol media Android.

Untuk menyesuaikan notifikasi lebih lanjut, buat MediaNotification.Provider dengan DefaultMediaNotificationProvider.Builder atau dengan membuat penerapan kustom antarmuka penyedia. Tambahkan penyedia ke MediaSessionService Anda dengan setMediaNotificationProvider.

Melanjutkan pemutaran

Setelah MediaSessionService dihentikan, dan bahkan setelah perangkat dimulai ulang, pemutaran dapat dilanjutkan untuk memungkinkan pengguna memulai ulang layanan dan melanjutkan pemutaran dari tempat terakhir mereka berhenti. Secara default, kelanjutan pemutaran dinonaktifkan. Artinya, pengguna tidak dapat melanjutkan pemutaran saat layanan Anda tidak berjalan. Untuk mengaktifkan fitur ini, Anda perlu mendeklarasikan penerima tombol media dan menerapkan metode onPlaybackResumption.

Mendeklarasikan penerima tombol media Media3

Mulailah dengan mendeklarasikan MediaButtonReceiver dalam manifes Anda:

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

Menerapkan callback pelanjutan pemutaran

Saat kelanjutan pemutaran diminta oleh perangkat Bluetooth atau fitur kelanjutan UI Sistem Android, metode callback onPlaybackResumption() dipanggil.

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, metadata (like title
    // and artwork) of the current item 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, 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;
}

Jika Anda telah menyimpan parameter lain seperti kecepatan pemutaran, mode pengulangan, atau mode acak, onPlaybackResumption() adalah tempat yang tepat untuk mengonfigurasi pemutar dengan parameter ini sebelum Media3 menyiapkan pemutar dan memulai pemutaran saat callback selesai.

Metode ini dipanggil selama waktu booting untuk membuat notifikasi pelanjutan UI Sistem Android setelah perangkat di-reboot. Untuk notifikasi multimedia, sebaiknya isi kolom MediaMetadata seperti title dan artworkData atau artworkUri item saat ini dengan nilai yang tersedia secara lokal, karena akses jaringan mungkin belum tersedia. Anda juga dapat menambahkan MediaConstants.EXTRAS_KEY_COMPLETION_STATUS dan MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE ke MediaMetadata.extras untuk menunjukkan posisi pemutaran lanjutan.

Konfigurasi pengontrol lanjutan dan kompatibilitas mundur

Skenario umum adalah menggunakan MediaController di UI aplikasi untuk mengontrol pemutaran dan menampilkan playlist. Pada saat yang sama, sesi diekspos ke klien eksternal seperti kontrol media Android dan Asisten di perangkat seluler atau TV, Wear OS untuk smartwatch dan Android Auto di mobil. Aplikasi demo sesi Media3 adalah contoh aplikasi yang menerapkan skenario tersebut.

Klien eksternal ini dapat menggunakan API seperti MediaControllerCompat dari library AndroidX lama atau android.media.session.MediaController dari platform Android. Media3 sepenuhnya kompatibel dengan library lama dan menyediakan interoperabilitas dengan Android platform API.

Menggunakan pengontrol notifikasi media

Penting untuk dipahami bahwa pengontrol platform dan lama ini berbagi status yang sama dan visibilitas tidak dapat disesuaikan oleh pengontrol (misalnya, PlaybackState.getActions() dan PlaybackState.getCustomActions() yang tersedia). Anda dapat menggunakan pengontrol notifikasi media untuk mengonfigurasi set status di sesi media platform agar kompatibel dengan pengontrol platform dan lama ini.

Misalnya, aplikasi dapat menyediakan implementasi MediaSession.Callback.onConnect() untuk menetapkan perintah yang tersedia dan preferensi tombol media khusus untuk sesi platform sebagai berikut:

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 button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences 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 button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

Mengizinkan Android Auto mengirim perintah kustom

Saat menggunakan MediaLibraryService dan untuk mendukung Android Auto dengan aplikasi seluler, pengontrol Android Auto memerlukan perintah yang tersedia yang sesuai, jika tidak, Media3 akan menolak perintah kustom yang masuk dari pengontrol tersebut:

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 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 for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

Aplikasi demo sesi memiliki modul otomotif, yang menunjukkan dukungan untuk Automotive OS yang memerlukan APK terpisah.