Mengontrol dan mengiklankan pemutaran menggunakan MediaSession

Sesi media menyediakan cara universal untuk berinteraksi dengan pemutar audio atau video. Di Media3, pemutar default-nya adalah class ExoPlayer, yang menerapkan antarmuka Player. Menghubungkan sesi media ke pemutar memungkinkan aplikasi untuk mengiklankan pemutaran media secara eksternal dan menerima perintah pemutaran dari sumber eksternal.

Perintah dapat berasal dari tombol fisik seperti tombol putar di headset atau remote control TV. Masalah ini mungkin juga berasal dari aplikasi klien yang memiliki pengontrol media, seperti menginstruksikan "jeda" ke Asisten Google. Sesi media mendelegasikan perintah ini ke pemutar aplikasi media.

Kapan harus memilih sesi media

Saat menerapkan MediaSession, Anda mengizinkan pengguna untuk mengontrol pemutaran:

  • Melalui headphone mereka. Sering kali ada tombol atau interaksi sentuh yang dapat dilakukan pengguna di headphone untuk memutar atau menjeda media atau membuka trek berikutnya atau sebelumnya.
  • Dengan berbicara dengan Asisten Google. Pola umumnya adalah mengucapkan "Ok Google, jeda" untuk menjeda media apa pun yang sedang diputar di perangkat.
  • Melalui smartwatch Wear OS. Hal ini memungkinkan akses yang lebih mudah ke kontrol pemutaran yang paling umum saat bermain di ponsel.
  • Melalui Kontrol media. Carousel ini menampilkan kontrol untuk setiap sesi media yang berjalan.
  • Di TV. Mengizinkan tindakan dengan tombol pemutaran fisik, kontrol pemutaran platform, dan pengelolaan daya (misalnya jika TV, soundbar, atau penerima A/V nonaktif atau input dialihkan, pemutaran harus berhenti di aplikasi).
  • Serta proses eksternal lainnya yang perlu memengaruhi pemutaran.

Ini bagus untuk banyak kasus penggunaan. Secara khusus, Anda harus benar-benar mempertimbangkan penggunaan MediaSession saat:

  • Anda melakukan streaming konten video berdurasi panjang, seperti film atau TV live.
  • Anda melakukan streaming konten audio berdurasi panjang, seperti podcast atau playlist musik.
  • Anda sedang membuat aplikasi TV.

Namun, tidak semua kasus penggunaan cocok dengan MediaSession. Sebaiknya Anda hanya menggunakan Player dalam kasus berikut:

  • Anda menunjukkan konten pendek, yang sangat menentukan interaksi pengguna dan interaksi pengguna.
  • Tidak ada satu video yang aktif, misalnya pengguna men-scroll daftar dan beberapa video ditampilkan di layar secara bersamaan.
  • Anda memutar video pengantar atau penjelasan satu kali, yang Anda harapkan untuk ditonton secara aktif oleh pengguna.
  • Konten Anda sensitif terhadap privasi dan Anda tidak ingin proses eksternal mengakses metadata media (misalnya, mode samaran di browser)

Jika kasus penggunaan Anda tidak sesuai dengan salah satu dari hal yang tercantum di atas, pertimbangkan apakah Anda tidak masalah jika aplikasi terus melanjutkan pemutaran saat pengguna tidak berinteraksi secara aktif dengan konten. Jika jawabannya adalah ya, Anda mungkin perlu memilih MediaSession. Jika jawabannya tidak, Anda mungkin perlu menggunakan Player.

Membuat sesi media

Sesi media berdampingan dengan pemutar yang dikelolanya. Anda dapat membuat sesi media dengan objek Context dan Player. Anda harus membuat dan melakukan inisialisasi sesi media saat diperlukan, seperti metode siklus proses onStart() atau onResume() dari metode Activity atau Fragment, atau onCreate() dari Service yang memiliki sesi media tersebut dan pemutar terkaitnya.

Untuk membuat sesi media, lakukan inisialisasi Player dan berikan ke MediaSession.Builder seperti ini:

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

Penanganan status otomatis

Library Media3 otomatis memperbarui sesi media menggunakan status pemutar. Dengan demikian, Anda tidak perlu menangani pemetaan dari pemain ke sesi secara manual.

Ini merupakan jeda dari pendekatan lama yang mengharuskan Anda membuat dan mengelola PlaybackStateCompat secara terpisah dari pemutar itu sendiri, misalnya untuk menunjukkan error.

ID sesi unik

Secara default, MediaSession.Builder akan membuat sesi dengan string kosong sebagai ID sesi. Ini sudah cukup jika aplikasi bermaksud untuk hanya membuat satu instance sesi, yang merupakan kasus paling umum.

Jika ingin mengelola beberapa instance sesi secara bersamaan, aplikasi harus memastikan bahwa ID sesi setiap sesi bersifat unik. ID sesi dapat ditetapkan saat membuat sesi dengan MediaSession.Builder.setId(String id).

Jika Anda melihat IllegalStateException membuat aplikasi mengalami error dengan pesan error IllegalStateException: Session ID must be unique. ID=, ada kemungkinan sesi telah dibuat secara tidak terduga sebelum instance yang dibuat sebelumnya dengan ID yang sama dirilis. Untuk menghindari sesi dibocorkan oleh error pemrograman, kasus tersebut akan terdeteksi dan diberi tahu dengan menampilkan pengecualian.

Memberikan kontrol kepada klien lain

Sesi media adalah kunci untuk mengontrol pemutaran. Hal ini memungkinkan Anda mengarahkan perintah dari sumber eksternal ke pemutar yang memutar media Anda. Sumber ini dapat berupa tombol fisik seperti tombol putar di headset atau remote control TV, atau perintah tidak langsung seperti menginstruksikan "jeda" ke Asisten Google. Demikian pula, Anda mungkin ingin memberikan akses ke sistem Android untuk memfasilitasi kontrol notifikasi dan layar kunci, atau ke smartwatch Wear OS sehingga Anda dapat mengontrol pemutaran dari tampilan jam. Klien eksternal dapat menggunakan pengontrol media untuk mengeluarkan perintah pemutaran ke aplikasi media Anda. Perintah ini diterima oleh sesi media Anda, yang pada akhirnya mendelegasikan perintah ke pemutar media.

Diagram yang menunjukkan interaksi antara MediaSession dan MediaController.
Gambar 1: Pengontrol media memfasilitasi penerusan perintah dari sumber eksternal ke sesi media.

Saat pengontrol akan terhubung ke sesi media Anda, metode onConnect() akan dipanggil. Anda dapat menggunakan ControllerInfo yang disediakan untuk memutuskan apakah akan menerima atau menolak permintaan tersebut. Lihat contoh penerimaan permintaan koneksi di bagian Mendeklarasikan perintah yang tersedia.

Setelah terhubung, pengontrol dapat mengirimkan perintah pemutaran ke sesi. Selanjutnya, sesi mendelegasikan perintah tersebut ke pemutar. Perintah pemutaran dan playlist yang ditentukan dalam antarmuka Player ditangani secara otomatis oleh sesi.

Metode callback lainnya memungkinkan Anda menangani, misalnya, permintaan untuk perintah pemutaran kustom dan mengubah playlist). Callback ini juga menyertakan objek ControllerInfo sehingga Anda dapat mengubah cara merespons setiap permintaan per pengontrol.

Mengubah playlist

Sesi media dapat langsung mengubah playlist pemutarnya seperti yang dijelaskan dalam Panduan ExoPlayer untuk playlist. Pengontrol juga dapat mengubah playlist jika COMMAND_SET_MEDIA_ITEM atau COMMAND_CHANGE_MEDIA_ITEMS tersedia untuk pengontrol.

Saat menambahkan item baru ke playlist, pemain biasanya memerlukan instance MediaItem dengan URI yang ditentukan agar dapat diputar. Secara default, item yang baru ditambahkan akan otomatis diteruskan ke metode pemutar, seperti player.addMediaItem, jika item tersebut memiliki URI yang ditentukan.

Jika ingin menyesuaikan instance MediaItem yang ditambahkan ke pemutar, Anda dapat mengganti onAddMediaItems(). Langkah ini diperlukan jika Anda ingin mendukung pengontrol yang meminta media tanpa URI yang ditentukan. Sebagai gantinya, MediaItem biasanya memiliki satu atau beberapa kolom berikut yang ditetapkan untuk mendeskripsikan media yang diminta:

  • MediaItem.id: ID umum yang mengidentifikasi media.
  • MediaItem.RequestMetadata.mediaUri: URI permintaan yang dapat menggunakan skema kustom dan belum tentu dapat diputar langsung oleh pemain.
  • MediaItem.RequestMetadata.searchQuery: Kueri penelusuran tekstual, misalnya dari Asisten Google.
  • MediaItem.MediaMetadata: Metadata terstruktur seperti 'judul' atau 'artis'.

Untuk opsi penyesuaian lainnya untuk playlist yang benar-benar baru, Anda juga dapat menggantikan onSetMediaItems() yang memungkinkan Anda menentukan item awal dan posisi dalam playlist. Misalnya, Anda dapat meluaskan satu item yang diminta ke seluruh playlist dan menginstruksikan pemain untuk memulai di indeks item yang pertama kali diminta. Contoh penerapan onSetMediaItems() dengan fitur ini dapat ditemukan di aplikasi demo sesi.

Mengelola tata letak khusus dan perintah khusus

Bagian berikut menjelaskan cara mengiklankan tata letak kustom tombol perintah kustom ke aplikasi klien dan mengizinkan pengontrol untuk mengirim perintah kustom.

Menentukan tata letak kustom sesi

Untuk menunjukkan kepada aplikasi klien kontrol pemutaran mana yang ingin Anda tampilkan kepada pengguna, setel tata letak kustom sesi saat mem-build MediaSession dalam metode onCreate() di layanan Anda.

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

Mendeklarasikan pemutar yang tersedia dan perintah kustom

Aplikasi media dapat menentukan perintah kustom yang misalnya dapat digunakan dalam tata letak kustom. Misalnya, Anda mungkin ingin mengimplementasikan tombol yang memungkinkan pengguna menyimpan item media ke daftar item favorit. MediaController mengirimkan perintah kustom dan MediaSession.Callback menerimanya.

Anda dapat menentukan perintah sesi kustom yang tersedia untuk MediaController saat terhubung ke sesi media Anda. Anda mencapai hal ini dengan mengganti MediaSession.Callback.onConnect(). Konfigurasikan dan tampilkan rangkaian perintah yang tersedia saat menerima permintaan koneksi dari MediaController dalam metode 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();
  }
}

Untuk menerima permintaan perintah kustom dari MediaController, ganti metode onCustomCommand() di 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)
      );
    }
    ...
  }
}

Anda dapat melacak pengontrol media mana yang membuat permintaan menggunakan properti packageName dari objek MediaSession.ControllerInfo yang diteruskan ke metode Callback. Hal ini memungkinkan Anda menyesuaikan perilaku aplikasi sebagai respons terhadap perintah yang diberikan jika berasal dari sistem, aplikasi Anda sendiri, atau aplikasi klien lainnya.

Memperbarui tata letak kustom setelah interaksi pengguna

Setelah menangani perintah kustom atau interaksi lain dengan pemutar, Anda mungkin perlu mengupdate tata letak yang ditampilkan di UI pengontrol. Contoh umumnya adalah tombol yang mengubah ikonnya setelah memicu tindakan yang terkait dengan tombol ini. Untuk memperbarui tata letak, Anda dapat menggunakan 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));

Menyesuaikan perilaku perintah pemutaran

Untuk menyesuaikan perilaku perintah yang ditentukan dalam antarmuka Player, seperti play() atau seekToNext(), gabungkan Player Anda dalam 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();

Untuk mengetahui informasi selengkapnya tentang ForwardingPlayer, lihat panduan ExoPlayer tentang Penyesuaian.

Mengidentifikasi pengontrol yang meminta perintah pemutar

Saat panggilan ke metode Player berasal dari MediaController, Anda dapat mengidentifikasi sumber asal dengan MediaSession.controllerForCurrentRequest dan mendapatkan ControllerInfo untuk permintaan saat ini:

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

Merespons tombol media

Tombol media adalah tombol hardware yang ditemukan di perangkat Android dan perangkat periferal lainnya, seperti tombol putar/jeda di headset Bluetooth. Media3 menangani peristiwa tombol media saat tiba di sesi dan memanggil metode Player yang sesuai di pemutar sesi.

Aplikasi dapat mengganti perilaku default dengan mengganti MediaSession.Callback.onMediaButtonEvent(Intent). Dalam kasus semacam ini, aplikasi dapat/perlu menangani sendiri semua spesifikasi API.