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 mengumumkan 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. Perintah ini juga dapat berasal dari aplikasi klien yang memiliki pengontrol media, seperti memberikan perintah "jeda" ke 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 untuk memutar atau menjeda media atau membuka trek berikutnya atau sebelumnya.
  • Dengan berbicara kepada Asisten Google. Pola umum 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 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 dinonaktifkan atau input diubah, pemutaran akan berhenti di aplikasi).
  • Dan proses eksternal lainnya yang perlu memengaruhi pemutaran.

Hal ini sangat cocok untuk banyak kasus penggunaan. Secara khusus, Anda harus mempertimbangkan penggunaan 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 mem-build aplikasi TV.

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

  • Anda menampilkan konten berdurasi singkat, yang memerlukan engagement dan interaksi pengguna yang penting.
  • 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.
  • 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 memungkinkan aplikasi melanjutkan pemutaran saat pengguna tidak berinteraksi secara aktif dengan konten. Jika jawabannya ya, sebaiknya pilih MediaSession. Jika jawabannya tidak, sebaiknya gunakan Player sebagai gantinya.

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 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 adalah perubahan 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 membuat sesi dengan string kosong sebagai ID sesi. Hal ini cukup jika aplikasi hanya ingin membuat satu instance sesi, yang merupakan kasus paling umum.

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

Jika Anda melihat IllegalStateException yang membuat aplikasi 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 telah dirilis. Untuk menghindari kebocoran sesi 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. Dengan begitu, Anda dapat merutekan perintah dari sumber eksternal ke pemutar yang melakukan tugas memutar media. Sumber ini dapat berupa tombol fisik seperti tombol putar di headset atau remote control TV, atau perintah tidak langsung seperti memberi perintah "jeda" ke Asisten Google. Demikian pula, Anda dapat 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. Lihat contoh penerimaan permintaan koneksi di bagian Mendeklarasikan perintah yang tersedia.

Setelah terhubung, pengontrol dapat mengirim perintah pemutaran ke sesi. Kemudian, sesi mendelegasikan perintah tersebut ke pemutar. Perintah pemutaran dan playlist yang ditentukan di antarmuka Player akan otomatis ditangani 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 berdasarkan 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 menjelaskan media yang diminta:

  • MediaItem.id: 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 Asisten Google.
  • MediaItem.MediaMetadata: Metadata terstruktur seperti 'judul' atau 'artis'.

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

Mengelola tata letak kustom dan perintah kustom

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

Menentukan tata letak kustom sesi

Untuk menunjukkan kepada aplikasi klien kontrol pemutaran yang ingin Anda tampilkan kepada pengguna, tetapkan tata letak sesi kustom saat mem-build MediaSession dalam metode onCreate() 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();
}

Menyatakan pemutar dan perintah kustom yang tersedia

Aplikasi media dapat menentukan perintah kustom yang misalnya dapat digunakan dalam tata letak kustom. Misalnya, Anda mungkin ingin menerapkan 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 dapat melakukannya dengan mengganti MediaSession.Callback.onConnect(). Konfigurasikan dan tampilkan kumpulan 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. Dengan demikian, Anda dapat menyesuaikan perilaku aplikasi sebagai respons terhadap perintah tertentu jika berasal dari sistem, aplikasi Anda sendiri, atau aplikasi klien lainnya.

Memperbarui tata letak kustom setelah interaksi pengguna

Setelah menangani perintah kustom atau interaksi lainnya dengan pemutar, Anda mungkin ingin memperbarui tata letak yang ditampilkan di UI pengontrol. Contoh umum adalah tombol 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 di antarmuka Player, seperti play() atau seekToNext(), gabungkan Player dalam ForwardingSimpleBasePlayer sebelum meneruskannya ke MediaSession.

Kotlin

val player = (logic to build a Player instance)

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

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

Java

ExoPlayer player = (logic to build a Player instance)

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

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

Untuk informasi selengkapnya tentang ForwardingSimpleBasePlayer, lihat panduan ExoPlayer 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

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  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

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  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);
  }
}

Merespons tombol media

Tombol media adalah tombol hardware yang ada di perangkat Android dan perangkat periferal lainnya, seperti tombol putar/jeda di headset Bluetooth. Media3 menangani peristiwa tombol media untuk Anda 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 hal ini, aplikasi dapat/perlu menangani semua spesifikasi API sendiri.

Penanganan dan pelaporan error

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 memanggil melalui Player.Listener.onPlayerError(PlaybackException) dan Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

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

Untuk interoperabilitas, error fatal direplikasi ke PlaybackStateCompat sesi platform dengan mentransisikan statusnya ke STATE_ERROR dan menetapkan kode dan pesan error sesuai dengan PlaybackException.

Penyesuaian error fatal

Untuk memberikan informasi yang dilokalkan dan bermakna kepada pengguna, kode error, pesan error, dan tambahan error dari error pemutaran fatal dapat disesuaikan dengan menggunakan ForwardingPlayer saat mem-build sesi:

Kotlin

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

Java

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

Pemutar penerusan mendaftarkan Player.Listener ke pemutar sebenarnya dan mencegat callback yang melaporkan error. PlaybackException yang disesuaikan kemudian didelegasikan ke pemroses yang terdaftar di pemutar penerusan. Agar hal ini berfungsi, pemutar penerusan akan mengganti Player.addListener dan Player.removeListener agar memiliki akses ke pemroses yang digunakan untuk mengirim kode error, pesan, atau tambahan yang disesuaikan:

Kotlin

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

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    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)
        }
        // Apps can customize further error messages by adding more branches.
        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)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      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;
        // Apps can customize further error messages by adding more case statements.
        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);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Error non-fatal

Error non-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),
)

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

// Interoperability: Sending a nonfatal error to the media notification controller 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));

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

// Interoperability: Sending a nonfatal error to the media notification controller 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);
}

Error non-fatal yang dikirim ke pengontrol notifikasi media direplikasi ke PlaybackStateCompat sesi platform. Dengan demikian, hanya kode error dan pesan error yang ditetapkan ke PlaybackStateCompat, sedangkan PlaybackStateCompat.state tidak diubah menjadi STATE_ERROR.

Menerima error non-fatal

MediaController menerima error non-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.
              }
            });