Gdy użytkownik przeciąga suwak paska przewijania po osi czasu, aby wyświetlić podgląd klatek i znaleźć pozycję odtwarzania, interfejs wysyła częste wywołania Player.seekTo w krótkich odstępach czasu. Standardowe przewijanie za pomocą Player.seekTo i ExoPlayer.setSeekParameters jest przeznaczone do pojedynczych przeskoków, gdzie każde nowe przewinięcie przerywa bieżące przewijanie i resetuje dekoder.
Podczas ciągłego przeciągania może to uniemożliwić renderowanie klatek podglądu, dopóki użytkownik nie przestanie przesuwać.
ExoPlayer.setScrubbingModeEnabled konfiguruje odtwarzacz pod kątem interaktywnego przewijania, dzięki czemu klatki podglądu filmu są renderowane elastycznie, gdy użytkownik przeciąga suwak.
Korzystanie z gotowych komponentów interfejsu
Gotowe moduły interfejsu Media3 obsługują wbudowany tryb przewijania, gdy są połączone bezpośrednio z instancją ExoPlayer (nie są opakowane w ForwardingPlayer ani nie są dostępne przez MediaController):
- Jetpack Compose (
media3-ui-compose-material3): zarówno komponentPlayer, jak i samodzielny komponentProgressSliderdomyślnie używają trybu przewijania. Gdy użytkownik przeciąga suwak,ProgressSliderwywołujesetScrubbingModeEnabled(true)na początku przeciągania, wysyła aktualizacjeseekTopodczas przeciągania i wywołujesetScrubbingModeEnabled(false), gdy przeciąganie się kończy lub gdy element kompozycyjny opuszcza kompozycję. Więcej informacji znajdziesz w artykule Material3 Compose. - Widoki (
media3-ui):PlayerViewiPlayerControlViewobejmują integrację trybu przewijania, która jest domyślnie wyłączona. Przewijanie następuje tylko raz, gdy użytkownik zwolni pasek czasu. Włącz ciągłe przewijanie w XML, ustawiającapp:time_bar_scrubbing_enabled="true"naPlayerView, lub w kodzie, wywołującsetTimeBarScrubbingEnabled(true).
Nawet jeśli używasz ProgressSlider lub PlayerView, możesz dostosować ScrubbingModeParameters w instancji ExoPlayer, np. ustawić ułamkową tolerancję przewijania na podstawie szerokości paska przewijania.
Kiedy włączać i wyłączać tryb czyszczenia
Jeśli implementujesz niestandardowy pasek przewijania lub element sterujący przewijaniem, włącz tryb przewijania tylko na czas interaktywnego gestu przewijania użytkownika:
- Włącz tryb przewijania (
setScrubbingModeEnabled(true)), gdy użytkownik zacznie przeciągać suwak, i wywołajseekTo, gdy zmieni się pozycja przeciągania. - Wyłącz tryb przewijania (
setScrubbingModeEnabled(false)), gdy tylko użytkownik zakończy lub anuluje gest. Możesz na przykład wyłączyć go po zwolnieniu wskaźnika, anulowaniu gestu lub odłączeniu albo usunięciu komponentu interfejsu w trakcie przeciągania. - Tryb przewijania powinien być wyłączony podczas normalnego odtwarzania i pojedynczych operacji przewijania, takich jak naciśnięcie przycisku przewijania do przodu lub do tyłu, kliknięcie paska przewijania bez przeciągania lub przewijanie programowe.
Pozostawienie aktywnego trybu przewijania poza gestem przeciągania ma niezamierzone skutki uboczne:
- Odtwarzanie jest wstrzymane: gdy tryb przewijania jest włączony, ExoPlayer ustawia przyczynę wstrzymania odtwarzania na
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, więcPlayer.isPlayingzwracafalse. Gdy wywołasz funkcjęsetScrubbingModeEnabled(false), normalne odtwarzanie zostanie automatycznie wznowione, jeśliplayWhenReadytotrue. - Ścieżki niewizualne są wyłączone: ścieżki audio i metadanych są domyślnie wyłączone podczas przewijania.
- Zwiększone zużycie energii i dekodera: tryb przewijania zwiększa szybkość działania kodeka wideo, co powoduje dodatkowe zużycie zasobów sprzętowych i baterii, jeśli jest włączony.
W tym przykładzie wywołania zwrotne niestandardowego gestu suwaka są połączone z 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); }
Jak tryb przewijania optymalizuje wyszukiwanie
Dzięki ScrubbingModeParameters.DEFAULT ExoPlayer stosuje kilka optymalizacji, gdy tryb przewijania jest aktywny:
- Łączy szybkie przewijanie: zamiast przerywać aktywne przewijanie przy każdym ruchu wskaźnika, ExoPlayer kończy dekodowanie i renderowanie bieżącej klatki docelowej, zachowując w kolejce tylko ostatnią pozycję przewijania. Zastąpione wyszukiwania pośrednie są odrzucane i zgłaszane do
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Wyłącza ścieżki audio i metadanych: tymczasowo wyłącza ścieżki
C.TRACK_TYPE_AUDIOiC.TRACK_TYPE_METADATA, aby uniknąć trzasków i niepotrzebnych resetów renderera. - Zwiększa przepustowość kodeka: zwiększa szybkość działania
MediaCodecwideo i zapewnia włączenie dynamicznego planowania, dzięki czemu bufory wyjściowe dekodowania są przetwarzane szybciej. - Zmniejsza liczbę resetowań dekodera: unika niepotrzebnych operacji
MediaCodecpodczas przechodzenia do klatki kluczowej w zgodnych strumieniach, pomija resetowanie do klatki kluczowej podczas przechodzenia do przodu w tej samej grupie obrazów (GOP) i używaMediaCodec.BUFFER_FLAG_DECODE_ONLYna Androidzie 14 (poziom interfejsu API 34) i nowszych, dzięki czemu dekoder pomija generowanie buforów wyjściowych dla klatek pośrednich między klatką kluczową a klatką docelową.
Dostosowywanie parametrów przewijania
Użyj ScrubbingModeParameters.Builder, aby dostosować zachowanie podczas przewijania, np. które typy ścieżek są wyłączone lub czy szybkość działania kodeka jest zwiększona. Zastosuj konfigurację za pomocą polecenia ExoPlayer.setScrubbingModeParameters lub ExoPlayer.Builder.setScrubbingModeParameters. Kompiluj z exoPlayer.scrubbingModeParameters.buildUpon() (lub ScrubbingModeParameters.DEFAULT.buildUpon()), aby domyślne optymalizacje pozostały włączone.
Ustawianie ułamkowej tolerancji przewijania na podstawie szerokości paska przewijania
Domyślnie ScrubbingModeParameters pozostawia tolerancje przewijania ułamkowego bez ustawienia (null), więc przewijanie podczas przesuwania wykorzystuje SeekParameters odtwarzacza (domyślnie SeekParameters.EXACT). Dokładne przewijanie musi zdekodować każdą klatkę od poprzedniej klatki kluczowej do docelowej sygnatury czasowej. Jeśli od poprzedniej klatki kluczowej upłynęło wiele klatek, np. w strumieniach z szerokimi odstępami między klatkami kluczowymi lub wysoką liczbą klatek, dekodowanie może powodować zauważalne opóźnienie podczas przeciągania.
Na ekranach dotykowych palec użytkownika nie jest w stanie precyzyjnie rozróżnić ruchu o 1dp lub 2dp wzdłuż paska przewijania. W przypadku suwaka o szerokości widthDp niewielka tolerancja dotyku, np. 2dp w każdym kierunku (łącznie 4dp), odpowiada ułamkowi 2.0 / widthDp czasu trwania multimediów. To, ile czasu odtwarzania obejmuje ten ułamek, zależy od długości filmu. Zalecamy przekazanie do metody setFractionalSeekTolerance tolerancji opartej na dp podzielonej przez szerokość suwaka interfejsu (np. 2dp lub dostosowanej w zakresie od 1dp do 4dp, aby zachować równowagę między szybkością a precyzją):
- W przypadku długich treści (np. godzinnego filmu na pasku przewijania o długości
400dp, gdzie2dpodpowiada 18 sekundom) ExoPlayer może przeskoczyć bezpośrednio do pobliskiej klatki kluczowej w ramach tej tolerancji dotyku zamiast dekodować klatki pośrednie. - W przypadku krótkich klipów (np. 10-sekundowego klipu na tym samym pasku przewijania
400dp, gdzie2dpobejmuje 50 milisekund) ten sam ułamek automatycznie utrzymuje przewijanie blisko klatki docelowej.
Przekonwertuj docelową tolerancję dp na piksele, używając gęstości wyświetlania, aby odpowiadała zmierzonej szerokości suwaka w pikselach. Aktualizuj tolerancję za każdym razem, gdy zmienia się szerokość suwaka lub gęstość wyświetlania, np. przy początkowym układzie lub obracaniu ekranu.
W Jetpack Compose przekonwertuj tolerancję dp na piksele za pomocą LocalDensity,
zmierz niestandardowy suwak za pomocą Modifier.onSizeChanged i zaktualizuj
scrubbingModeParameters, gdy zmieni się odtwarzacz, szerokość lub gęstość. Jeśli instancja ExoPlayer działa dłużej niż funkcja kompozycyjna, wyczyść tolerancję po usunięciu:
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() }
W interfejsie opartym na widoku przekonwertuj tolerancję dp na piksele za pomocą DisplayMetrics.density, zastosuj bieżącą szerokość, jeśli widok suwaka lub PlayerView jest już ułożony, i użyj View.addOnLayoutChangeListener, aby zaktualizować scrubbingModeParameters, gdy zmieni się jego szerokość. Usuń detektor podczas odłączania interfejsu i wyczyść tolerancję, jeśli instancja ExoPlayer działa dłużej niż widok:
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()); }
Jeśli używasz tolerancji opartej na dp, pozostawienie trybu przewijania włączonego podczas ostatniego wywołania seekTo w onScrubStop pozwala uniknąć wizualnego przeskoku klatki, gdy użytkownik podniesie palec. Jeśli aplikacja wymaga, aby ostateczna pozycja odtwarzania była zgodna z dokładną sygnaturą czasową, wywołaj setScrubbingModeEnabled(false) przed ostatnim wywołaniem seekTo, aby ostateczne przewijanie korzystało ze standardowego SeekParameters odtwarzacza.
Kwestie związane z multimediami i siecią
Tryb przewijania optymalizuje lokalne planowanie wyszukiwania i dekodowanie sprzętowe, dzięki czemu jest najbardziej skuteczny w przypadku plików lokalnych, pobranych multimediów, strumieni zapisanych w pamięci podręcznej lub buforowanych strumieni adaptacyjnych zakodowanych z częstymi klatkami kluczowymi (krótkie GOP). W przypadku niebuforowanych połączeń sieciowych o dużym opóźnieniu częste wyszukiwanie niebuforowanych pozycji jest ograniczone przez czas błądzenia sieci.