Mengontrol dan mengiklankan pemutaran menggunakan MediaSession

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

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

Kapan harus memilih sesi media

Saat menerapkan MediaSession, Anda mengizinkan pengguna mengontrol pemutaran:

  • Melalui headphone mereka. Sering kali ada tombol atau interaksi sentuh yang dapat dilakukan pengguna di headphone mereka untuk memutar atau menjeda media atau beralih ke lagu berikutnya atau sebelumnya.
  • Dengan berbicara kepada asisten AI, termasuk Gemini, atau Asisten Google. Pola umumnya adalah mengucapkan "Ok Google, jeda" untuk menjeda media apa pun yang sedang diputar di perangkat.
  • Melalui smartwatch Wear OS mereka. Hal ini memudahkan akses ke kontrol pemutaran yang paling umum saat memutar di ponsel.
  • Melalui Kontrol media. Carousel ini menampilkan kontrol untuk setiap sesi media yang berjalan.
  • Di TV. Memungkinkan tindakan dengan tombol pemutaran fisik, kontrol pemutaran platform, dan pengelolaan daya (misalnya, jika TV, soundbar, atau penerima A/V dimatikan atau input dialihkan, pemutaran akan berhenti di aplikasi).
  • Melalui kontrol media Android Auto. Hal ini memungkinkan kontrol pemutaran yang aman saat mengemudi.
  • Dan proses eksternal lainnya yang perlu memengaruhi pemutaran.

Hal ini sangat cocok untuk banyak kasus penggunaan. Secara khusus, Anda harus sangat mempertimbangkan untuk menggunakan MediaSession jika:

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

Namun, tidak semua kasus penggunaan cocok dengan MediaSession. Anda mungkin ingin menggunakan Player saja dalam kasus berikut:

  • Anda menampilkan konten berdurasi singkat, yang tidak memerlukan kontrol eksternal atau pemutaran di latar belakang.
  • Tidak ada satu video aktif, seperti pengguna men-scroll daftar dan beberapa video ditampilkan di layar secara bersamaan.
  • Anda memutar video pengantar atau penjelasan satu kali, yang Anda harapkan akan ditonton secara aktif oleh pengguna tanpa memerlukan kontrol pemutaran eksternal.
  • 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 yang tercantum di atas, pertimbangkan apakah Anda setuju jika aplikasi melanjutkan pemutaran saat pengguna tidak berinteraksi secara aktif dengan konten. Jika jawabannya ya, Anda mungkin ingin memilih MediaSession. Jika jawabannya tidak, sebaiknya gunakan 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 Activity atau Fragment, atau metode onCreate() dari Service yang memiliki sesi media dan pemutar terkait.

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 mengupdate sesi media menggunakan status pemutar. Oleh karena itu, Anda tidak perlu menangani pemetaan dari pemutar ke sesi secara manual.

Hal ini berbeda dengan sesi media platform, yang mengharuskan Anda membuat dan mempertahankan PlaybackState secara independen dari pemutar itu sendiri, misalnya untuk menunjukkan error.

ID sesi unik

Secara default, MediaSession.Builder membuat sesi dengan string kosong sebagai ID sesi. Hal ini sudah cukup jika aplikasi hanya ingin 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 Anda error dengan pesan error IllegalStateException: Session ID must be unique. ID=, kemungkinan sesi telah dibuat secara tidak terduga sebelum instance yang dibuat sebelumnya dengan ID yang sama dirilis. Untuk menghindari kebocoran sesi akibat error pemrograman, kasus tersebut akan dideteksi dan diberitahukan dengan menampilkan pengecualian.

Memberikan kontrol kepada klien lain

Sesi media adalah kunci untuk mengontrol pemutaran. Dengan begitu, Anda dapat mengarahkan perintah dari sumber eksternal ke pemutar yang berfungsi memutar media Anda. Sumber ini dapat berupa tombol fisik seperti tombol putar di headset atau remote TV, atau perintah tidak langsung seperti menginstruksikan "jeda" ke Gemini atau Asisten Google. Demikian pula, Anda mungkin ingin memberikan akses ke sistem Android untuk 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. Sesi media Anda menerima perintah ini dan mendelegasikannya ke pemutar media.

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, sesi akan memanggil onConnectAsync(). Anda dapat menggunakan ControllerInfo yang disediakan untuk memutuskan apakah akan menerima atau menolak permintaan tersebut. Untuk contoh penerimaan permintaan koneksi, lihat Mendeklarasikan dan menangani perintah kustom.

Setelah terhubung, pengontrol dapat mengirim perintah pemutaran ke sesi. Sesi kemudian mendelegasikan perintah tersebut ke pemutar. Perintah pemutaran dan playlist yang ditentukan dalam antarmuka Player akan otomatis ditangani oleh sesi.

Metode callback lainnya memungkinkan Anda menangani, misalnya, permintaan untuk perintah 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, pemutar 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 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.mediaId: ID generik yang mengidentifikasi media.
  • MediaItem.RequestMetadata.mediaUri: URI permintaan yang dapat menggunakan skema kustom dan tidak harus dapat diputar langsung oleh pemutar.
  • MediaItem.RequestMetadata.searchQuery: Kueri penelusuran tekstual, misalnya dari Gemini atau Asisten Google.
  • MediaItem.mediaMetadata: Metadata terstruktur seperti 'judul' atau 'artis'.

Untuk opsi penyesuaian lainnya untuk playlist yang benar-benar baru, Anda juga dapat mengganti onSetMediaItems(), yang memungkinkan Anda menentukan item awal dan posisi dalam playlist. Misalnya, Anda dapat memperluas satu item yang diminta ke seluruh playlist dan menginstruksikan pemutar untuk memulai pada indeks item yang awalnya diminta. Untuk contoh implementasi onSetMediaItems() dengan fitur ini, lihat aplikasi demo sesi.

Memublikasikan metadata playlist

Media3 menyediakan API untuk memublikasikan metadata playlist terstruktur bersama pemutaran lagu aktif. Dengan menstandarkan cara aplikasi mengisi keanggotaan playlist dan metadata deskriptif di seluruh sesi media, Android dapat mengekspos titik entri tingkat sistem untuk pemutaran di aplikasi Anda.

Dalam konteks ini, istilah playlist mengacu pada pengelompokan trek yang diurutkan untuk diputar secara berurutan. Hal ini berlaku tidak hanya untuk daftar item yang dapat diputar yang ditentukan pengguna atau aplikasi, tetapi juga untuk pengelompokan seperti album musik, episode podcast, dan bab buku audio.

Mendeklarasikan ID playlist dan metadata playlist

Di Media3, identifikasi playlist dan metadata deskriptif playlist dibagi menjadi dua cakupan MediaMetadata:

  • ID playlist tingkat item (MediaItem.mediaMetadata.playlistId):
    • Tandai MediaItem yang sedang diputar sebagai bagian dari playlist dengan memanggil MediaMetadata.Builder.setPlaylistId pada MediaMetadata item, sementara MediaItem.mediaId secara unik mengidentifikasi masing-masing lagu.
    • playlistId adalah string buram yang ditentukan aplikasi yang secara unik mengidentifikasi playlist di seluruh sesi selama playlist dapat dilanjutkan (misalnya, 37i9dQZF1DX76Wlfdnj7AP), dan tidak boleh digunakan kembali untuk playlist yang berbeda. Pengontrol yang terhubung tidak menafsirkan ID ini; pengontrol menyimpannya dan mengirimkannya kembali ke aplikasi Anda untuk melanjutkan playlist.
    • Setiap lagu yang termasuk dalam playlist memiliki playlistId yang sama. Biarkan playlistId tidak disetel (null) untuk lagu yang tidak diputar dari konteks playlist, seperti lagu mandiri tunggal.
  • Metadata playlist tingkat pemutar (Player.getPlaylistMetadata):
    • Isi instance MediaMetadata khusus dengan atribut deskriptif playlist (setTitle, setArtworkUri, dan setMediaType) dan tetapkan di pemutar menggunakan Player.setPlaylistMetadata.
    • Saat pengontrol yang terhubung mendeteksi currentMediaItem.mediaMetadata.playlistId non-null, pengontrol akan memeriksa MediaController.getPlaylistMetadata untuk mengambil judul playlist dan URI poster.
    • Perbarui metadata playlist setiap kali playlist aktif berubah (misalnya, saat pengguna memulai playlist lain) sehingga getPlaylistMetadata selalu mendeskripsikan playlist yang dirujuk oleh playlistId item saat ini.

Contoh berikut menunjukkan cara memublikasikan metadata playlist di Player dan mengaitkan lagu MediaItem dengan playlist tersebut:

Kotlin

// 1. Declare the playlist descriptive metadata on the Player
val playlistMetadata =
  MediaMetadata.Builder()
    .setTitle("Morning Vibes")
    .setArtworkUri(Uri.parse("https://example.com/image/morning-vibes"))
    .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
    .build()
player.setPlaylistMetadata(playlistMetadata)

// 2. Mark the playing track(s) with the playlistId it belongs to
val trackItem =
  MediaItem.Builder()
    .setMediaId("foobar:track:6rqhFgbbKwnb9MLmUQDhG6")
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setTitle("Example Track")
        .setArtist("Example Artist")
        .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
        .setPlaylistId("37i9dQZF1DX76Wlfdnj7AP")
        .build()
    )
    .build()
player.setMediaItem(trackItem)

Java

// 1. Declare the playlist descriptive metadata on the Player
MediaMetadata playlistMetadata =
    new MediaMetadata.Builder()
        .setTitle("Morning Vibes")
        .setArtworkUri(Uri.parse("https://example.com/image/morning-vibes"))
        .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
        .build();
player.setPlaylistMetadata(playlistMetadata);

// 2. Mark the playing track(s) with the playlistId it belongs to
MediaItem trackItem =
    new MediaItem.Builder()
        .setMediaId("foobar:track:6rqhFgbbKwnb9MLmUQDhG6")
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setTitle("Example Track")
                .setArtist("Example Artist")
                .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
                .setPlaylistId("37i9dQZF1DX76Wlfdnj7AP")
                .build())
        .build();
player.setMediaItem(trackItem);

Melanjutkan playlist

Saat pengguna memilih playlist untuk dilanjutkan, pengontrol akan terhubung ke sesi media aplikasi Anda dan mengirimkan MediaItem playlist yang mediaId-nya ditetapkan ke playlistId yang disimpan:

Kotlin

val playlistItem =
  MediaItem.Builder()
    .setMediaId("37i9dQZF1DX76Wlfdnj7AP")
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
        .setIsPlayable(true)
        .build()
    )
    .build()
controller.setMediaItem(playlistItem)
controller.prepare()
controller.play()

Java

MediaItem playlistItem =
    new MediaItem.Builder()
        .setMediaId("37i9dQZF1DX76Wlfdnj7AP")
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
                .setIsPlayable(true)
                .build())
        .build();
controller.setMediaItem(playlistItem);
controller.prepare();
controller.play();

Aplikasi Anda memperluas playlist MediaItem menjadi lagu yang dapat diputar di dalam MediaSession.Callback.onSetMediaItems (atau onAddMediaItems), dan memublikasikan playlistId dan playlistMetadata lagi seperti yang ditunjukkan di bagian sebelumnya.

Mengelola preferensi tombol media

Setiap pengontrol, misalnya UI Sistem, Android Auto, atau Wear OS, dapat membuat keputusannya sendiri tentang tombol mana yang akan ditampilkan kepada pengguna. Untuk menunjukkan kontrol pemutaran yang ingin ditampilkan kepada pengguna, Anda dapat menentukan preferensi tombol media di MediaSession. Preferensi ini terdiri dari daftar instance CommandButton yang diurutkan, yang masing-masing menentukan preferensi untuk tombol di antarmuka pengguna.

Menentukan tombol perintah

Instance CommandButton digunakan untuk menentukan preferensi tombol media. Setiap tombol menentukan tiga aspek elemen UI yang diinginkan:

  1. Ikon, yang menentukan tampilan visual. Ikon harus ditetapkan ke salah satu konstanta yang telah ditentukan saat membuat CommandButton.Builder. Perhatikan bahwa ini bukan resource gambar atau Bitmap yang sebenarnya. Konstanta generik membantu pengontrol memilih resource yang sesuai untuk tampilan dan nuansa yang konsisten dalam UI mereka sendiri. Jika tidak ada konstanta ikon standar yang sesuai dengan kasus penggunaan Anda, Anda dapat menggunakan setCustomIconResId.
  2. Perintah, yang menentukan tindakan yang dipicu saat pengguna berinteraksi dengan tombol. Anda dapat menggunakan setPlayerCommand untuk Player.Command, atau setSessionCommand untuk SessionCommand yang telah ditetapkan atau kustom.
  3. Slot, yang menentukan tempat tombol harus ditempatkan di UI pengontrol. Kolom ini bersifat opsional dan otomatis ditetapkan berdasarkan Ikon dan Perintah. Misalnya, Anda dapat menentukan bahwa tombol harus ditampilkan di area navigasi 'maju' pada UI, bukan di area 'overflow' default.

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

Saat preferensi tombol media diselesaikan, algoritma berikut akan diterapkan:

  1. Untuk setiap CommandButton di preferensi tombol media, tempatkan tombol di slot pertama yang tersedia dan diizinkan.
  2. Jika salah satu slot tengah, maju, dan mundur tidak diisi dengan tombol, tambahkan tombol default untuk slot ini.

Anda dapat menggunakan CommandButton.DisplayConstraints untuk membuat pratinjau tentang cara preferensi tombol media akan di-resolve, bergantung pada batasan tampilan UI.

Menetapkan preferensi tombol media

Cara termudah untuk menetapkan preferensi tombol media adalah dengan menentukan daftar saat membuat MediaSession. Atau, Anda dapat mengganti MediaSession.Callback.onConnectAsync() untuk menyesuaikan preferensi tombol media untuk setiap pengontrol yang terhubung.

Kotlin

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Java

MediaSession mediaSession =
    new MediaSession.Builder(context, player)
        .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
        .build();

Memperbarui preferensi tombol media setelah interaksi pengguna

Setelah menangani interaksi dengan pemutar, Anda mungkin ingin memperbarui tombol yang ditampilkan di UI pengontrol. Contoh umumnya adalah tombol beralih yang mengubah ikon dan tindakannya setelah memicu tindakan yang terkait dengan tombol ini. Untuk memperbarui preferensi tombol media, Anda dapat menggunakan MediaSession.setMediaButtonPreferences untuk memperbarui preferensi bagi semua pengontrol atau pengontrol tertentu:

Kotlin

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton));

Menambahkan perintah kustom dan menyesuaikan perilaku default

Perintah pemutar yang tersedia dapat diperluas dengan perintah kustom dan Anda juga dapat menangkap perintah pemutar dan tombol media yang masuk untuk mengubah perilaku default.

Mendeklarasikan dan menangani perintah kustom

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

Untuk menentukan perintah kustom, Anda harus mengganti MediaSession.Callback.onConnectAsync() guna menetapkan perintah kustom yang tersedia untuk setiap pengontrol yang terhubung.

Kotlin

private class CustomMediaSessionCallback : MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  override fun onConnectAsync(
    session: MediaSession,
    controller: ControllerInfo,
  ): ListenableFuture<ConnectionResult> {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return Futures.immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
}

Java

private static class CustomMediaSessionCallback implements MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  @Override
  public ListenableFuture<ConnectionResult> onConnectAsync(
      MediaSession session, ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return Futures.immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
}

Untuk menerima permintaan perintah kustom dari MediaController, ganti metode onCustomCommand() di Callback.

Kotlin

private class CustomCallback : MediaSession.Callback {
  // ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: 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))
    }
    // ...
    return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
  }
}

Java

private static class CustomCallback 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));
    }
    // ...
    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. Dengan demikian, Anda dapat menyesuaikan perilaku aplikasi sebagai respons terhadap perintah tertentu jika perintah tersebut berasal dari sistem, aplikasi Anda sendiri, atau aplikasi klien lainnya.

Menyesuaikan perintah pemutar default

Semua perintah default dan penanganan status didelegasikan ke Player yang ada di MediaSession. Untuk menyesuaikan perilaku perintah yang ditentukan dalam antarmuka Player, seperti play() atau seekToNext(), gabungkan Player Anda dalam ForwardingSimpleBasePlayer sebelum meneruskannya ke MediaSession:

Kotlin

val forwardingPlayer =
  object : ForwardingSimpleBasePlayer(player) {
    // Customizations
  }

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();

Untuk informasi selengkapnya tentang ForwardingSimpleBasePlayer, lihat panduan ExoPlayer tentang Penyesuaian.

Mengidentifikasi pengontrol yang meminta perintah pemain

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

Kotlin

private class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) {
  private lateinit var session: MediaSession

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Java

private static final class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  private MediaSession session;

  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

Menyesuaikan penanganan tombol media

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

Sebaiknya tangani semua peristiwa tombol media yang masuk dalam metode Player yang sesuai. Untuk kasus penggunaan lanjutan lainnya, peristiwa tombol media dapat dicegat di MediaSession.Callback.onMediaButtonEvent().

Penanganan error dan pelaporan

Ada dua jenis error yang dikeluarkan dan dilaporkan sesi ke pengontrol. Error fatal melaporkan kegagalan pemutaran teknis pemutar sesi yang mengganggu pemutaran. Error fatal dilaporkan ke pengontrol secara otomatis saat terjadi. Error tidak fatal adalah error non-teknis atau kebijakan yang tidak mengganggu pemutaran dan dikirim ke pengontrol oleh aplikasi secara manual.

Error pemutaran fatal

Error pemutaran fatal dilaporkan ke sesi oleh pemutar, lalu dilaporkan ke pengontrol untuk dipanggil melalui Player.Listener.onPlayerError(PlaybackException) dan Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

Dalam kasus seperti ini, status pemutaran ditransisikan ke STATE_IDLE dan MediaController.getPlayerError() menampilkan PlaybackException yang menyebabkan transisi. Pengontrol dapat memeriksa PlaybackException.errorCode untuk mendapatkan informasi tentang alasan error.

Menetapkan error pemutar kustom

Selain error fatal yang dilaporkan oleh pemutar, aplikasi dapat menetapkan PlaybackException kustom di tingkat MediaSession menggunakan MediaSession.setPlaybackException(PlaybackException). Hal ini memungkinkan aplikasi memberi sinyal status error ke pengontrol yang terhubung. Pengecualian dapat ditetapkan untuk semua pengontrol yang terhubung atau untuk ControllerInfo tertentu.

Saat aplikasi menetapkan PlaybackException menggunakan API ini:

  • Instance MediaController yang terhubung akan diberi tahu. Callback Listener.onPlayerError(PlaybackException) dan Listener.onPlayerErrorChanged(@Nullable PlaybackException) pada pengontrol akan dipanggil dengan pengecualian yang diberikan.

  • Metode MediaController.getPlayerError() akan menampilkan PlaybackException yang ditetapkan oleh aplikasi.

  • Status pemutaran untuk pengontrol yang terpengaruh akan berubah menjadi Player.STATE_IDLE.

  • Perintah yang tersedia akan dihapus dan hanya perintah baca seperti COMMAND_GET_TIMELINE yang tersisa jika sudah diberikan. Misalnya, status Timeline dibekukan ke status saat pengecualian diterapkan ke pengontrol. Perintah yang mencoba mengubah status pemutar, seperti COMMAND_PLAY, akan dihapus hingga pengecualian pemutaran untuk pengontrol tertentu dihapus oleh aplikasi.

Untuk menghapus PlaybackException kustom yang ditetapkan sebelumnya dan memulihkan pelaporan status pemutar normal, aplikasi dapat memanggil MediaSession.setPlaybackException(/* playbackException= */ null) atau MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null).

Penyesuaian error fatal

Untuk memberikan informasi yang dilokalkan dan bermakna kepada pengguna, Anda dapat menyesuaikan kode error, pesan error, dan tambahan error dari error pemutaran fatal yang berasal dari pemutar sebenarnya. Untuk melakukannya, gunakan ForwardingSimpleBasePlayer saat membuat sesi:

Kotlin

val session = MediaSession.Builder(context, ErrorForwardingPlayer(context, player)).build()

Java

MediaSession session =
    new MediaSession.Builder(context, new ErrorForwardingPlayer(context, player)).build();

Subclass ForwardingSimpleBasePlayer Anda dapat mengganti getState() untuk mengintersepsi error dan menyesuaikan kode error, pesan, atau tambahan. Dengan cara yang sama, Anda juga dapat menghasilkan error baru yang tidak ada di pemutar asli:

Kotlin

private class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon().setPlayerError(customizePlaybackException(state.playerError!!)).build()
    }
    return state
  }

  private fun customizePlaybackException(error: PlaybackException): PlaybackException {
    val buttonLabel: String
    val errorMessage: String
    when (error.errorCode) {
      PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
        buttonLabel = context.getString(R.string.err_button_label_restart_stream)
        errorMessage = context.getString(R.string.err_msg_behind_live_window)
      }
      else -> {
        buttonLabel = context.getString(R.string.err_button_label_ok)
        errorMessage = context.getString(R.string.err_message_default)
      }
    }
    val extras = Bundle()
    extras.putString("button_label", buttonLabel)
    return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon().setPlayerError(customizePlaybackException(state.playerError)).build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    String buttonLabel;
    String errorMessage;
    switch (error.errorCode) {
      case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
        buttonLabel = context.getString(R.string.err_button_label_restart_stream);
        errorMessage = context.getString(R.string.err_msg_behind_live_window);
        break;
      default:
        buttonLabel = context.getString(R.string.err_button_label_ok);
        errorMessage = context.getString(R.string.err_message_default);
        break;
    }
    Bundle extras = new Bundle();
    extras.putString("button_label", buttonLabel);
    return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
  }
}

Error non-fatal

Error tidak fatal yang tidak berasal dari pengecualian teknis dapat dikirim oleh aplikasi ke semua atau ke pengontrol tertentu:

Kotlin

val sessionError =
  SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired),
  )

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
mediaSession.mediaNotificationControllerInfo?.let { mediaSession.sendError(it, sessionError) }

Java

SessionError sessionError =
    new SessionError(
        SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
        context.getString(R.string.error_message_authentication_expired));

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Saat error non-fatal dikirim ke pengontrol notifikasi media, kode error dan pesan error akan direplikasi ke sesi media platform, sedangkan PlaybackState.state tidak diubah menjadi STATE_ERROR.

Menerima error non-fatal

MediaController menerima error tidak fatal dengan menerapkan MediaController.Listener.onError:

Kotlin

val future =
  MediaController.Builder(context, sessionToken)
    .setListener(
      object : MediaController.Listener {
        override fun onError(controller: MediaController, sessionError: SessionError) {
          // Handle nonfatal error.
        }
      }
    )
    .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });