Wenn ein Nutzer den Schieberegler einer Steuerleiste über eine Zeitachse zieht, um eine Vorschau der Frames zu sehen und eine Wiedergabeposition zu finden, sendet die Benutzeroberfläche in kurzen Abständen häufige Player.seekTo-Aufrufe. Die Standardsuche mit Player.seekTo und ExoPlayer.setSeekParameters ist für isolierte Sprünge konzipiert, bei denen jede neue Suche alle laufenden Suchvorgänge abbricht und den Decoder zurücksetzt.
Bei einer kontinuierlichen Ziehgeste kann dieses Verhalten verhindern, dass Vorschauframes gerendert werden, bis der Nutzer die Bewegung beendet.
ExoPlayer.setScrubbingModeEnabled konfiguriert den Player für das interaktive Scrubbing, sodass die Frames der Videovorschau beim Ziehen durch den Nutzer responsiv gerendert werden.
Vordefinierte UI-Komponenten verwenden
Die vordefinierten UI-Module von Media3 unterstützen den Scrubbing-Modus, wenn sie direkt mit einer ExoPlayer-Instanz verbunden sind (nicht in einer ForwardingPlayer verpackt oder über eine MediaController aufgerufen):
- Jetpack Compose (
media3-ui-compose-material3): Sowohl die zusammensetzbare FunktionPlayerals auch die eigenständige FunktionProgressSliderverwenden standardmäßig den Scrubbing-Modus. Wenn ein Nutzer den Schieberegler zieht, ruftProgressSliderzu Beginn des ZiehenssetScrubbingModeEnabled(true)auf, sendet während des ZiehensseekTo-Updates und ruftsetScrubbingModeEnabled(false)auf, wenn das Ziehen beendet wird oder die komponierbare Funktion die Komposition verlässt. Weitere Informationen finden Sie unter Material3 Compose. - Ansichten (
media3-ui):PlayerViewundPlayerControlViewenthalten eine Integration des Scrubbing-Modus, die standardmäßig deaktiviert ist. Dabei wird nur einmal gesucht, wenn der Nutzer die Zeitleiste loslässt. Aktiviere das kontinuierliche Scrubbing in XML, indem duapp:time_bar_scrubbing_enabled="true"fürPlayerViewfestlegst, oder im Code, indem dusetTimeBarScrubbingEnabled(true)aufrufst.
Auch wenn Sie ProgressSlider oder PlayerView verwenden, können Sie ScrubbingModeParameters auf der ExoPlayer-Instanz anpassen, z. B. eine Toleranz für die Suche als Bruchteil der Breite des Schiebereglers festlegen.
Wann der Scrubbing-Modus aktiviert und deaktiviert werden sollte
Wenn du eine benutzerdefinierte Steuerleiste oder einen benutzerdefinierten Schieberegler implementierst, aktiviere den Scrubbing-Modus nur für die Dauer einer interaktiven Scrubbing-Geste des Nutzers:
- Aktiviere den Scrubbing-Modus (
setScrubbingModeEnabled(true)), wenn der Nutzer beginnt, den Scrubbing-Schieberegler zu ziehen, und rufeseekToauf, wenn sich die Ziehposition ändert. - Deaktiviere den Scrubbing-Modus (
setScrubbingModeEnabled(false)), sobald der Nutzer die Geste beendet oder abbricht. Deaktivieren Sie sie beispielsweise, wenn der Zeiger losgelassen wird, wenn die Geste abgebrochen wird oder wenn die UI-Komponente während des Ziehens getrennt oder verworfen wird. - Deaktiviere den Scrubbing-Modus während der normalen Wiedergabe und bei einzelnen Suchvorgängen, z. B. beim Drücken der Tasten für den schnellen Vor- oder Rücklauf, beim Tippen auf eine Steuerleiste ohne Ziehen oder beim programmgesteuerten Suchen.
Wenn der Scrubbing-Modus außerhalb einer Zieh-Geste aktiv bleibt, hat das unbeabsichtigte Nebenwirkungen:
- Wiedergabe wird unterdrückt: Wenn der Scrubbing-Modus aktiviert ist, setzt ExoPlayer den Grund für die Unterdrückung der Wiedergabe auf
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, sodassPlayer.isPlayingden Wertfalsezurückgibt. Wenn dusetScrubbingModeEnabled(false)aufrufst, wird die normale Wiedergabe automatisch fortgesetzt, wennplayWhenReadyden Werttruehat. - Nicht visuelle Tracks sind deaktiviert: Audio- und Metadaten-Tracks sind beim Scrubbing standardmäßig deaktiviert.
- Erhöhter Strom- und Decoderverbrauch: Im Scrubbing-Modus werden die Betriebsraten des Videocodecs erhöht. Wenn der Modus aktiviert bleibt, werden zusätzliche Hardware- und Akkuressourcen verbraucht.
Im folgenden Beispiel werden benutzerdefinierte Callbacks für die Scrubbing-Geste mit ExoPlayer verbunden:
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); }
Wie der Scrubbing-Modus das Suchen optimiert
Mit ScrubbingModeParameters.DEFAULT wendet ExoPlayer mehrere Optimierungen an, während der Scrubbing-Modus aktiv ist:
- Schnelle Suchvorgänge zusammenfassen: Anstatt bei jeder Zeigerbewegung einen aktiven Suchvorgang vorzeitig zu beenden, schließt ExoPlayer die Decodierung und das Rendering des aktuellen Zielframes ab und hält nur die letzte Suchposition in der Warteschlange. Ersetzte Zwischensuchvorgänge werden verworfen und an
AnalyticsListener.onDroppedSeeksWhileScrubbinggemeldet. - Deaktiviert Audio- und Metadatentracks: Deaktiviert vorübergehend
C.TRACK_TYPE_AUDIO- undC.TRACK_TYPE_METADATA-Tracks, um Audioknacken und unnötige Renderer-Zurücksetzungen zu vermeiden. - Erhöht den Codec-Durchsatz: Erhöht die Betriebsrate des Videos
MediaCodecund sorgt dafür, dass die dynamische Planung aktiviert ist, damit decodierte Ausgabepuffer schneller verarbeitet werden. - Reduziert Decoder-Zurücksetzungen: Vermeidet unnötige
MediaCodec-Leerungen, wenn in kompatiblen Streams zu einem Keyframe gesprungen wird. Überspringt das Zurücksetzen auf den Keyframe, wenn innerhalb derselben Bildgruppe (Group of Pictures, GOP) vorwärts gesprungen wird. VerwendetMediaCodec.BUFFER_FLAG_DECODE_ONLYunter Android 14 (API-Level 34) und höher, damit der Decoder keine Ausgabepuffer für Zwischenframes zwischen einem Keyframe und dem Zielframe erzeugt.
Bereinigungsparameter anpassen
Mit ScrubbingModeParameters.Builder können Sie das Scrubbing-Verhalten anpassen, z. B. welche Tracktypen deaktiviert sind oder ob die Codec-Betriebsraten erhöht werden. Wenden Sie die Konfiguration mit ExoPlayer.setScrubbingModeParameters oder ExoPlayer.Builder.setScrubbingModeParameters an. Erstellen Sie das Image aus exoPlayer.scrubbingModeParameters.buildUpon() (oder ScrubbingModeParameters.DEFAULT.buildUpon()), damit die Standardoptimierungen aktiviert bleiben.
Suchtoleranz als Bruchteil der Breite des Schiebereglers festlegen
Standardmäßig lässt ScrubbingModeParameters die Toleranzen für die Suche nach Bruchteilen ungesetzt (null), sodass bei der Suche während des Scrubbings die SeekParameters des Players verwendet wird (standardmäßig SeekParameters.EXACT). Bei einer genauen Suche muss jeder Frame vom vorherigen Keyframe bis zum Zielzeitstempel decodiert werden. Wenn seit dem vorherigen Keyframe viele Frames vorhanden sind, z. B. in Streams mit großen Keyframe-Intervallen oder hohen Frameraten, kann diese Decodierung beim Ziehen zu einer spürbaren Latenz führen.
Auf Touchscreens kann der Finger eines Nutzers Bewegungen von 1dp oder 2dp entlang einer Bildlaufleiste nicht zuverlässig unterscheiden. Bei einem Schieberegler mit einer Breite von widthDp entspricht eine kleine Berührungstoleranz wie 2dp in beide Richtungen (4dp insgesamt) einem Bruchteil von 2.0 / widthDp der Mediendauer. Wie viel Medienzeit dieser Bruchteil umfasst, hängt von der Länge des Videos ab. Wir empfehlen, eine dp-basierte Toleranz, geteilt durch die Breite des UI-Scrubbers, an setFractionalSeekTolerance zu übergeben (z. B. 2dp oder zwischen 1dp und 4dp, um Geschwindigkeit und Präzision auszugleichen):
- Bei langen Inhalten (z. B. einem einstündigen Video auf einem
400dp-Scrubber, bei dem2dp18 Sekunden umfasst) kann ExoPlayer direkt zu einem nahegelegenen Keyframe innerhalb dieser Berührungstoleranz springen, anstatt Zwischenframes zu decodieren. - Bei kurzen Clips (z. B. einem 10-Sekunden-Clip auf demselben
400dp-Scrubber, bei dem2dp50 Millisekunden umfasst) sorgt derselbe Bruchteil automatisch dafür, dass die Suchvorgänge nahe am Zielframe bleiben.
Konvertieren Sie die Zieltoleranz von dp in Pixel, indem Sie die Anzeigedichte verwenden, damit sie der gemessenen Pixelbreite des Scrubbers entspricht. Aktualisieren Sie die Toleranz, wenn sich die Breite des Scrubbers oder die Anzeigedichte ändert, z. B. beim ersten Layout oder bei der Bildschirmdrehung.
In Jetpack Compose kannst du die Toleranz von dp mit LocalDensity in Pixel umwandeln, deinen benutzerdefinierten Schieberegler mit Modifier.onSizeChanged messen und scrubbingModeParameters aktualisieren, wenn sich der Player, die Breite oder die Dichte ändert. Wenn die ExoPlayer-Instanz die zusammensetzbare Funktion überlebt, löschen Sie die Toleranz beim Verwerfen:
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() }
In einer ansichtsbasierten Benutzeroberfläche konvertieren Sie die dp-Toleranz mit DisplayMetrics.density in Pixel, wenden die aktuelle Breite an, wenn Ihre Scrubber-Ansicht oder PlayerView bereits angeordnet ist, und verwenden View.addOnLayoutChangeListener, um scrubbingModeParameters zu aktualisieren, wenn sich die Breite ändert. Entfernen Sie den Listener, wenn Sie die Benutzeroberfläche trennen, und löschen Sie die Toleranz, wenn die ExoPlayer-Instanz die Ansicht überdauert:
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()); }
Wenn du eine dp-basierte Toleranz verwendest, solltest du den Scrubbing-Modus bis zum letzten seekTo-Aufruf in onScrubStop aktiviert lassen, um einen visuellen Frame-Sprung zu vermeiden, wenn der Nutzer den Finger hebt. Wenn Ihre App stattdessen erfordert, dass die endgültige Wiedergabeposition auf einem genauen Zeitstempel landet, rufen Sie setScrubbingModeEnabled(false) vor dem letzten seekTo-Aufruf auf, damit für die endgültige Suche die Standard-SeekParameters des Players verwendet wird.
Überlegungen zu Medien und Netzwerk
Der Scrubbing-Modus optimiert die lokale Suchplanung und die Hardwaredecodierung. Er ist daher am effektivsten bei lokalen Dateien, heruntergeladenen Medien, im Cache gespeicherten Streams oder gepufferten adaptiven Streams, die mit häufigen Keyframes (kurzen GOPs) codiert sind. Bei nicht im Cache gespeicherten Netzwerkverbindungen mit hoher Latenz sind häufige Suchvorgänge an nicht gepufferten Positionen weiterhin an die Umlaufzeiten des Netzwerks gebunden.