Pemutaran di latar belakang dengan MediaSessionService

Sering kali diinginkan untuk memutar media saat aplikasi tidak berada di latar depan. Sebagai misalnya, sebuah pemutar musik umumnya tetap memutar musik ketika pengguna telah mengunci di perangkat mereka 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 berisi Player dan MediaSession di dalam Service terpisah. Hal ini memungkinkan perangkat untuk terus menyajikan media bahkan saat aplikasi Anda tidak ada di dalam latar depan.

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

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

Menggunakan MediaSessionService memungkinkan klien eksternal seperti Google Asisten, kontrol media sistem, atau perangkat pendamping seperti Wear OS untuk menemukan layanan Anda, menyambungkannya, dan mengontrol pemutaran, semuanya tanpa perlu aktivitas UI aplikasi Anda. Faktanya, bisa saja ada beberapa aplikasi klien yang terhubung ke MediaSessionService yang sama di waktu yang sama, setiap aplikasi dengan MediaController.

Mengimplementasikan siklus proses layanan

Anda perlu menerapkan tiga metode siklus proses layanan Anda:

  • onCreate() dipanggil saat pengontrol pertama akan terhubung dan dibuat instance-nya dan dimulai. Ini adalah tempat terbaik untuk membangun Player dan MediaSession.
  • onTaskRemoved(Intent) dipanggil saat pengguna menutup aplikasi dari tugas terbaru mereka. Jika pemutaran sedang berlangsung, aplikasi dapat memilih untuk mempertahankan layanan yang berjalan di latar depan. Jika pemutar dijeda, layanan tidak berada dalam latar depan dan perlu dihentikan.
  • onDestroy() dipanggil saat layanan dihentikan. Semua materi termasuk pemutar dan sesi perlu dirilis.

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

Sebagai alternatif untuk menjaga pemutaran tetap berjalan di latar belakang, aplikasi dapat menghentikan layanan dalam keadaan apa pun bila pengguna menutup aplikasi:

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

Memberikan akses ke sesi media

Ganti metode onGetSession() untuk memberi klien lain akses ke media Anda sesi yang dibangun 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 untuk menjalankan layanan latar depan. Tambahkan Izin FOREGROUND_SERVICE ke manifes, dan jika Anda menargetkan API 34 dan di atas juga FOREGROUND_SERVICE_MEDIA_PLAYBACK:

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

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

Anda harus mendefinisikan foregroundServiceType yang mencakup mediaPlayback saat aplikasi Anda berjalan di perangkat dengan Android 10 (level API 29) dan yang lebih tinggi.

Mengontrol pemutaran menggunakan MediaController

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

Menangani perintah UI

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

Notifikasi

MediaSessionService secara otomatis membuat MediaNotification untuk Anda yang dalam kebanyakan kasus. Secara default, notifikasi yang dipublikasikan Notifikasi MediaStyle yang terus mengikuti informasi terbaru dari sesi media 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 yang sedang diputar bersama kontrol pemutaran berdasarkan Konfigurasi MediaSession.

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

Aplikasi dapat menyesuaikan tombol perintah kontrol Media Android. Baca selengkapnya menyesuaikan Android Media kontrol.

Penyesuaian notifikasi

Untuk menyesuaikan notifikasi, buat MediaNotification.Provider dengan DefaultMediaNotificationProvider.Builder atau dengan membuat implementasi khusus dari antarmuka penyedia. Tambahkan penyedia layanan ke MediaSessionService Anda dengan setMediaNotificationProvider.

Melanjutkan pemutaran

Tombol media adalah tombol hardware yang ditemukan di perangkat Android dan perangkat periferal lainnya seperti tombol putar atau jeda pada headset Bluetooth. Media3 menangani input tombol media saat layanan berjalan.

Mendeklarasikan penerima tombol media Media3

Media3 menyertakan API untuk memungkinkan pengguna melanjutkan pemutaran setelah aplikasi dihentikan dan bahkan setelah perangkat dimulai ulang. Secara default, pemutaran dilanjutkan akan dinonaktifkan. Ini berarti pengguna tidak dapat melanjutkan pemutaran saat layanan Anda tidak berjalan. Untuk ikut serta, 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 melanjutkan pemutaran

Jika pemutaran kembali diminta oleh perangkat Bluetooth atau Fitur melanjutkan UI Sistem Android, onPlaybackResumption() metode callback akan 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 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;
}

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

Konfigurasi pengontrol lanjutan dan kompatibilitas mundur

Skenario yang umum adalah menggunakan MediaController di UI aplikasi untuk mengontrol memutar dan menampilkan playlist. Pada saat yang sama, sesi terekspos 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 mengimplementasikan skenario tersebut.

Klien eksternal ini dapat menggunakan API seperti MediaControllerCompat klien lama Library AndroidX atau android.media.session.MediaController Android Google Workspace for Education. Media3 sepenuhnya kompatibel dengan library lama dan menyediakan interoperabilitas dengan API framework Android.

Menggunakan pengontrol notifikasi media

Penting untuk dipahami bahwa pengontrol {i>framework<i} atau {i>framework<i} ini membaca nilai yang sama dari framework PlaybackState.getActions() dan PlaybackState.getCustomActions(). Untuk menentukan tindakan dan tindakan kustom dari sesi framework, aplikasi dapat menggunakan pengontrol notifikasi media dan mengatur perintah yang tersedia dan tata letak khusus. Layanan ini menghubungkan media pengontrol notifikasi ke sesi Anda, dan sesi tersebut menggunakan ConnectionResult yang ditampilkan oleh onConnect() callback Anda untuk dikonfigurasi dan tindakan kustom dari sesi framework.

Dengan skenario khusus seluler, aplikasi dapat memberikan implementasi MediaSession.Callback.onConnect() untuk menyetel perintah yang tersedia dan tata letak khusus khusus untuk sesi kerangka kerja 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 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();
}

Mengizinkan Android Auto mengirim perintah kustom

Saat menggunakan MediaLibraryService dan untuk mendukung Android Auto dengan aplikasi seluler, pengontrol Android Auto memerlukan perintah yang tersedia, jika tidak Media3 akan menolak perintah khusus yang masuk dari {i>controller<i} itu:

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

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