Saat pengguna menarik thumb bilah geser di sepanjang linimasa untuk melihat pratinjau frame dan menemukan
posisi pemutaran, UI akan sering mengirimkan panggilan Player.seekTo dalam waktu
singkat. Pencarian standar dengan Player.seekTo dan
ExoPlayer.setSeekParameters dirancang untuk lompatan
terisolasi, di mana setiap pencarian baru membatalkan pencarian yang sedang berlangsung dan mereset dekoder.
Selama gestur tarik berkelanjutan, perilaku tersebut dapat mencegah frame pratinjau
dirender hingga pengguna berhenti bergerak.
ExoPlayer.setScrubbingModeEnabled mengonfigurasi
pemutar untuk scrubbing interaktif sehingga frame pratinjau video dirender secara responsif saat
pengguna menariknya.
Menggunakan komponen UI bawaan
Modul UI bawaan Media3 mencakup dukungan mode scrubbing bawaan saat
terhubung langsung ke instance ExoPlayer (tidak digabungkan dalam
ForwardingPlayer atau diakses melalui
MediaController):
- Jetpack Compose (
media3-ui-compose-material3): ComposablePlayerdanProgressSlidermandiri menggunakan mode scrubbing secara default. Saat pengguna menarik thumb penggeser,ProgressSliderakan memanggilsetScrubbingModeEnabled(true)di awal penarikan, mengirimkan updateseekToselama penarikan, dan memanggilsetScrubbingModeEnabled(false)saat penarikan berakhir atau composable keluar dari komposisi. Untuk informasi selengkapnya, lihat Compose Material3. - Tampilan (
media3-ui):PlayerViewdanPlayerControlViewmenyertakan integrasi mode scrubbing yang dinonaktifkan secara default, hanya mencari sekali saat pengguna melepaskan panel waktu. Aktifkan scrubbing berkelanjutan di XML dengan menetapkanapp:time_bar_scrubbing_enabled="true"diPlayerView, atau dalam kode dengan memanggilsetTimeBarScrubbingEnabled(true).
Meskipun menggunakan ProgressSlider atau PlayerView, Anda dapat menyesuaikan
ScrubbingModeParameters pada instance
ExoPlayer, seperti menetapkan toleransi pencarian pecahan dari lebar
scrubber.
Kapan harus mengaktifkan dan menonaktifkan mode scrubbing
Jika Anda menerapkan kontrol scrubber atau bilah geser kustom, aktifkan mode scrubbing hanya selama durasi gestur scrubbing pengguna interaktif:
- Aktifkan mode scrubbing (
setScrubbingModeEnabled(true)) saat pengguna mulai menarik thumb scrubber, dan panggilseekTosaat posisi tarik berubah. - Nonaktifkan mode scrubbing (
setScrubbingModeEnabled(false)) segera setelah pengguna menyelesaikan atau membatalkan gestur. Misalnya, nonaktifkan saat pointer dilepaskan, saat gestur dibatalkan, atau jika komponen UI Anda dilepaskan atau dibuang di tengah proses tarik. - Nonaktifkan mode scrubbing selama pemutaran normal dan pencarian tunggal, seperti menekan tombol maju cepat atau mundur, mengetuk bilah geser tanpa menyeret, atau mencari secara terprogram.
Membiarkan mode scrubbing aktif di luar gestur tarik memiliki efek samping yang tidak diinginkan:
- Pemutaran dihentikan: Saat mode scrubbing diaktifkan, ExoPlayer menetapkan
alasan penghentian pemutaran ke
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, sehinggaPlayer.isPlayingmenampilkanfalse. Saat Anda memanggilsetScrubbingModeEnabled(false), pemutaran normal akan otomatis dilanjutkan jikaplayWhenReadyadalahtrue. - Trek non-visual dinonaktifkan: Trek audio dan metadata dinonaktifkan secara default saat scrubbing.
- Peningkatan penggunaan daya dan decoder: Mode scrubbing meningkatkan kecepatan operasi codec video, yang menggunakan resource hardware dan baterai tambahan jika dibiarkan aktif.
Contoh berikut menghubungkan callback gestur scrubber kustom ke ExoPlayer:
Kotlin
fun onScrubStart(exoPlayer: ExoPlayer, positionMs: Long) { exoPlayer.setScrubbingModeEnabled(true) exoPlayer.seekTo(positionMs) } fun onScrubMove(exoPlayer: ExoPlayer, positionMs: Long) { exoPlayer.seekTo(positionMs) } fun onScrubStop(exoPlayer: ExoPlayer, positionMs: Long, canceled: Boolean) { if (!canceled) { exoPlayer.seekTo(positionMs) } exoPlayer.setScrubbingModeEnabled(false) }
Java
public static void onScrubStart(ExoPlayer exoPlayer, long positionMs) { exoPlayer.setScrubbingModeEnabled(true); exoPlayer.seekTo(positionMs); } public static void onScrubMove(ExoPlayer exoPlayer, long positionMs) { exoPlayer.seekTo(positionMs); } public static void onScrubStop(ExoPlayer exoPlayer, long positionMs, boolean canceled) { if (!canceled) { exoPlayer.seekTo(positionMs); } exoPlayer.setScrubbingModeEnabled(false); }
Cara mode scrubbing mengoptimalkan pencarian
Dengan ScrubbingModeParameters.DEFAULT, ExoPlayer menerapkan
beberapa pengoptimalan saat mode scrubbing aktif:
- Menggabungkan pencarian cepat: Daripada mendahului pencarian aktif pada setiap
gerakan pointer, ExoPlayer menyelesaikan decoding dan rendering frame target
saat ini sambil hanya menahan posisi pencarian terbaru dalam antrean. Pencarian perantara yang digantikan akan dihapus dan dilaporkan ke
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Menonaktifkan trek audio dan metadata: Menonaktifkan sementara trek
C.TRACK_TYPE_AUDIOdanC.TRACK_TYPE_METADATAuntuk menghindari letupan audio dan reset perender yang tidak perlu. - Meningkatkan throughput codec: Meningkatkan kecepatan operasi
MediaCodecvideo dan memastikan penjadwalan dinamis diaktifkan sehingga buffering output yang didekode diproses lebih cepat. - Mengurangi reset decoder: Menghindari flush
MediaCodecyang tidak perlu saat mencari keyframe dalam streaming yang kompatibel, melewati reset kembali ke keyframe saat mencari ke depan dalam grup gambar (GOP) yang sama, dan menggunakanMediaCodec.BUFFER_FLAG_DECODE_ONLYdi Android 14 (API level 34) dan yang lebih tinggi sehingga decoder melewati pembuatan buffering output untuk frame perantara antara keyframe dan frame target.
Menyesuaikan parameter scrubbing
Gunakan ScrubbingModeParameters.Builder untuk menyesuaikan perilaku
scrubbing, seperti jenis trek yang dinonaktifkan atau apakah kecepatan operasi
codec ditingkatkan. Terapkan konfigurasi dengan
ExoPlayer.setScrubbingModeParameters atau
ExoPlayer.Builder.setScrubbingModeParameters. Bangun dari
exoPlayer.scrubbingModeParameters.buildUpon() (atau
ScrubbingModeParameters.DEFAULT.buildUpon()) agar pengoptimalan default tetap
diaktifkan.
Menetapkan toleransi pencarian pecahan dari lebar scrubber
Secara default, ScrubbingModeParameters membiarkan toleransi pencarian pecahan tidak ditetapkan
(null), sehingga pencarian selama scrubbing menggunakan
SeekParameters pemutar (SeekParameters.EXACT secara default). Penelusuran
yang tepat harus mendekode setiap frame dari keyframe sebelumnya hingga stempel waktu
target. Jika ada banyak frame sejak keyframe sebelumnya, seperti dalam
streaming dengan interval keyframe yang lebar atau kecepatan frame yang tinggi, pekerjaan decoding ini
dapat menambahkan latensi yang terlihat saat menarik.
Di layar sentuh, jari pengguna tidak dapat membedakan gerakan 1dp atau 2dp
dengan andal di sepanjang panel scrubber. Untuk scrubber dengan lebar widthDp, toleransi sentuh kecil seperti 2dp di kedua arah (total 4dp) sesuai dengan pecahan 2.0 / widthDp dari durasi media. Durasi waktu media yang dicakup oleh pecahan tersebut bergantung pada durasi video. Sebaiknya teruskan toleransi berbasis dp yang dibagi dengan lebar scrubber UI Anda ke setFractionalSeekTolerance (misalnya, 2dp, atau disesuaikan antara 1dp dan 4dp untuk menyeimbangkan kecepatan dan presisi):
- Pada konten panjang (misalnya, video berdurasi 1 jam pada scrubber
400dp, dengan2dpberdurasi 18 detik), ExoPlayer dapat langsung beralih ke keyframe terdekat dalam toleransi sentuh tersebut, bukan mendekode frame perantara. - Pada klip pendek (misalnya, klip 10 detik pada scrubber
400dpyang sama, dengan2dpberdurasi 50 milidetik), pecahan yang sama secara otomatis membuat pencarian tetap dekat dengan frame target.
Konversikan toleransi dp target Anda ke piksel menggunakan kepadatan tampilan agar
sesuai dengan lebar piksel scrubber yang diukur, dan perbarui toleransi setiap kali
lebar scrubber atau kepadatan tampilan berubah, seperti pada tata letak awal atau
rotasi layar.
Di Jetpack Compose, konversikan toleransi dp ke piksel dengan LocalDensity,
ukur penggeser kustom Anda dengan Modifier.onSizeChanged, dan perbarui
scrubbingModeParameters saat pemutar, lebar, atau kepadatan berubah. Jika
instance ExoPlayer bertahan lebih lama dari composable, hapus toleransi saat pembuangan:
private val SCRUBBING_TOLERANCE = 2.dp @Composable fun CustomScrubbingSlider(exoPlayer: ExoPlayer?, modifier: Modifier = Modifier) { var sliderWidthPx by remember { mutableIntStateOf(0) } val tolerancePx = with(LocalDensity.current) { SCRUBBING_TOLERANCE.toPx() } LaunchedEffect(exoPlayer, sliderWidthPx, tolerancePx) { if (exoPlayer != null && sliderWidthPx > 0) { val tolerance = (tolerancePx.toDouble() / sliderWidthPx).coerceIn(0.0, 1.0) exoPlayer.setFractionalSeekTolerance(tolerance) } } DisposableEffect(exoPlayer) { onDispose { exoPlayer?.setFractionalSeekTolerance(null) } } CustomSlider( player = exoPlayer, modifier = modifier.onSizeChanged { size -> sliderWidthPx = size.width }, ) } private fun ExoPlayer.setFractionalSeekTolerance(tolerance: Double?) { scrubbingModeParameters = scrubbingModeParameters .buildUpon() .setFractionalSeekTolerance( /* toleranceBefore= */ tolerance, /* toleranceAfter= */ tolerance, ) .build() }
Dalam UI berbasis View, konversikan toleransi dp ke piksel menggunakan
DisplayMetrics.density, terapkan lebar saat ini jika tampilan scrubber atau
PlayerView sudah ditata letak, dan gunakan View.addOnLayoutChangeListener untuk
memperbarui scrubbingModeParameters saat lebarnya berubah. Hapus pemroses
saat melepaskan UI, dan hapus toleransi jika instance ExoPlayer
bertahan lebih lama dari tampilan:
Kotlin
private const val SCRUBBING_TOLERANCE_DP = 2.0 fun configureViewFractionalSeekTolerance( exoPlayer: ExoPlayer, scrubberView: View, ): View.OnLayoutChangeListener { updateFractionalSeekTolerance(exoPlayer, scrubberView, scrubberView.width) val listener = View.OnLayoutChangeListener { v, left, _, right, _, oldLeft, _, oldRight, _ -> val widthPx = right - left if (widthPx != oldRight - oldLeft) { updateFractionalSeekTolerance(exoPlayer, v, widthPx) } } scrubberView.addOnLayoutChangeListener(listener) return listener } fun clearViewFractionalSeekTolerance( exoPlayer: ExoPlayer, scrubberView: View, listener: View.OnLayoutChangeListener, ) { scrubberView.removeOnLayoutChangeListener(listener) setFractionalSeekTolerance(exoPlayer, tolerance = null) } private fun updateFractionalSeekTolerance( exoPlayer: ExoPlayer, scrubberView: View, widthPx: Int, ) { val density = scrubberView.resources.displayMetrics.density if (widthPx > 0 && density > 0f) { val tolerancePx = SCRUBBING_TOLERANCE_DP * density setFractionalSeekTolerance(exoPlayer, (tolerancePx / widthPx).coerceIn(0.0, 1.0)) } } private fun setFractionalSeekTolerance(exoPlayer: ExoPlayer, tolerance: Double?) { exoPlayer.scrubbingModeParameters = exoPlayer.scrubbingModeParameters .buildUpon() .setFractionalSeekTolerance( /* toleranceBefore= */ tolerance, /* toleranceAfter= */ tolerance, ) .build() }
Java
private static final double SCRUBBING_TOLERANCE_DP = 2.0; public static View.OnLayoutChangeListener configureViewFractionalSeekTolerance( ExoPlayer exoPlayer, View scrubberView) { updateFractionalSeekTolerance(exoPlayer, scrubberView, scrubberView.getWidth()); View.OnLayoutChangeListener listener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { int widthPx = right - left; if (widthPx != oldRight - oldLeft) { updateFractionalSeekTolerance(exoPlayer, v, widthPx); } }; scrubberView.addOnLayoutChangeListener(listener); return listener; } public static void clearViewFractionalSeekTolerance( ExoPlayer exoPlayer, View scrubberView, View.OnLayoutChangeListener listener) { scrubberView.removeOnLayoutChangeListener(listener); setFractionalSeekTolerance(exoPlayer, /* tolerance= */ null); } private static void updateFractionalSeekTolerance( ExoPlayer exoPlayer, View scrubberView, int widthPx) { float density = scrubberView.getResources().getDisplayMetrics().density; if (widthPx > 0 && density > 0f) { double tolerancePx = SCRUBBING_TOLERANCE_DP * density; setFractionalSeekTolerance(exoPlayer, Math.min(1.0, tolerancePx / widthPx)); } } private static void setFractionalSeekTolerance(ExoPlayer exoPlayer, @Nullable Double tolerance) { exoPlayer.setScrubbingModeParameters( exoPlayer .getScrubbingModeParameters() .buildUpon() .setFractionalSeekTolerance( /* toleranceBefore= */ tolerance, /* toleranceAfter= */ tolerance) .build()); }
Saat Anda menggunakan toleransi berbasis dp, mempertahankan mode scrubbing tetap aktif melalui
panggilan seekTo terakhir di onScrubStop akan menghindari lompatan frame visual saat pengguna
mengangkat jari. Jika aplikasi Anda memerlukan posisi rilis akhir untuk
mendarat pada stempel waktu yang tepat, panggil setScrubbingModeEnabled(false) sebelum
panggilan seekTo terakhir Anda sehingga pencarian akhir menggunakan
SeekParameters standar pemutar.
Pertimbangan media dan jaringan
Mode scrubbing mengoptimalkan penjadwalan pencarian lokal dan decoding hardware, sehingga paling efektif dengan file lokal, media yang didownload, streaming yang di-cache, atau streaming adaptif yang di-buffer yang dienkode dengan keyframe yang sering (GOP pendek). Melalui koneksi jaringan tanpa cache dan latensi tinggi, pencarian yang sering ke posisi yang tidak di-buffer tetap terikat oleh waktu round-trip jaringan.