Quando un utente trascina il cursore di una barra di scorrimento su una sequenza temporale per visualizzare l'anteprima dei frame e trovare una posizione di riproduzione, l'interfaccia utente invia frequenti chiamate Player.seekTo in rapida successione. La ricerca standard con Player.seekTo e
ExoPlayer.setSeekParameters è progettata per salti
isolati, in cui ogni nuova ricerca interrompe qualsiasi ricerca in corso e reimposta il decoder.
Durante un gesto di trascinamento continuo, questo comportamento può impedire il rendering dei frame di anteprima finché l'utente non smette di muoversi.
ExoPlayer.setScrubbingModeEnabled configura il player per lo scrubbing interattivo in modo che i frame di anteprima del video vengano visualizzati in modo adattabile mentre l'utente trascina.
Utilizza componenti UI predefiniti
I moduli UI predefiniti di Media3 includono il supporto integrato della modalità di scrubbing quando sono collegati direttamente a un'istanza ExoPlayer (non aggregata in un ForwardingPlayer o accessibile tramite un MediaController):
- Jetpack Compose (
media3-ui-compose-material3): sia il composablePlayersia l'elemento autonomoProgressSliderutilizzano la modalità di scrubbing per impostazione predefinita. Quando un utente trascina il cursore,ProgressSliderchiamasetScrubbingModeEnabled(true)all'inizio del trascinamento, invia aggiornamenti diseekTodurante il trascinamento e chiamasetScrubbingModeEnabled(false)quando il trascinamento termina o il composable esce dalla composizione. Per saperne di più, consulta Material3 Compose. - Visualizzazioni (
media3-ui):PlayerViewePlayerControlViewincludono l'integrazione della modalità di scrubbing, che è disattivata per impostazione predefinita, e la ricerca viene eseguita una sola volta quando l'utente rilascia la barra del tempo. Attiva lo scrubbing continuo in XML impostandoapp:time_bar_scrubbing_enabled="true"suPlayerViewo nel codice chiamandosetTimeBarScrubbingEnabled(true).
Anche quando utilizzi ProgressSlider o PlayerView, puoi personalizzare
ScrubbingModeParameters nell'istanza
ExoPlayer, ad esempio impostando una tolleranza di ricerca frazionaria dalla larghezza
dello scrubber.
Quando attivare e disattivare la modalità di scrubbing
Se implementi una barra di scorrimento o un controllo di scorrimento personalizzati, attiva la modalità di scorrimento solo per la durata di un gesto di scorrimento interattivo dell'utente:
- Attiva la modalità di scrubbing (
setScrubbingModeEnabled(true)) quando l'utente inizia a trascinare il cursore di scrubbing e chiamaseekToquando la posizione di trascinamento cambia. - Disattiva la modalità di scrubbing (
setScrubbingModeEnabled(false)) non appena l'utente termina o annulla il gesto. Ad esempio, disattivalo al rilascio del puntatore, all'annullamento del gesto o se il componente della UI viene scollegato o eliminato durante il trascinamento. - Mantieni la modalità di scrubbing disattivata durante la riproduzione normale e le singole ricerche, ad esempio premendo i pulsanti di avanzamento rapido o riavvolgimento, toccando una barra di scorrimento senza trascinarla o eseguendo una ricerca in modo programmatico.
Se la modalità di scorrimento rimane attiva al di fuori di un gesto di trascinamento, si verificano effetti collaterali indesiderati:
- La riproduzione è soppressa: quando la modalità di scrubbing è attiva, ExoPlayer imposta il motivo della soppressione della riproduzione su
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, pertantoPlayer.isPlayingrestituiscefalse. Quando chiamisetScrubbingModeEnabled(false), la riproduzione normale riprende automaticamente seplayWhenReadyètrue. - Le tracce non visive sono disattivate: le tracce audio e di metadati sono disattivate per impostazione predefinita durante lo scrubbing.
- Aumento del consumo energetico e dell'utilizzo del decodificatore: la modalità di scrubbing aumenta la velocità di funzionamento del codec video, il che consuma risorse hardware e della batteria aggiuntive se lasciata attiva.
L'esempio seguente connette i callback dei gesti di scorrimento personalizzati a 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); }
In che modo la modalità di scrubbing ottimizza la ricerca
Con ScrubbingModeParameters.DEFAULT, ExoPlayer applica
diverse ottimizzazioni quando la modalità di scrubbing è attiva:
- Unisce le ricerche rapide: anziché interrompere una ricerca attiva a ogni movimento del puntatore, ExoPlayer completa la decodifica e il rendering del frame target corrente, mantenendo in coda solo la posizione di ricerca più recente. Le ricerche intermedie sostituite vengono ignorate e segnalate a
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Disattiva le tracce audio e dei metadati: disattiva temporaneamente le tracce
C.TRACK_TYPE_AUDIOeC.TRACK_TYPE_METADATAper evitare scoppiettii audio e reimpostazioni del renderer non necessarie. - Aumenta la velocità effettiva del codec: aumenta la frequenza operativa del video
MediaCodece garantisce l'attivazione della pianificazione dinamica in modo che i buffer di output decodificati vengano elaborati più velocemente. - Riduce i reset del decoder: evita svuotamenti
MediaCodecnon necessari quando si cerca un keyframe in stream compatibili, salta il reset al keyframe quando si cerca in avanti nello stesso gruppo di immagini (GOP) e utilizzaMediaCodec.BUFFER_FLAG_DECODE_ONLYsu Android 14 (livello API 34) e versioni successive in modo che il decoder salti la produzione di buffer di output per i frame intermedi tra un keyframe e il frame target.
Personalizza i parametri di scrubbing
Utilizza ScrubbingModeParameters.Builder per regolare il comportamento di scrubbing, ad esempio quali tipi di tracce sono disattivati o se le frequenze operative dei codec sono aumentate. Applica la configurazione con ExoPlayer.setScrubbingModeParameters o ExoPlayer.Builder.setScrubbingModeParameters. Esegui la build da exoPlayer.scrubbingModeParameters.buildUpon() (o ScrubbingModeParameters.DEFAULT.buildUpon()) in modo che le ottimizzazioni predefinite rimangano attive.
Imposta una tolleranza di ricerca frazionaria dalla larghezza dello scrubber
Per impostazione predefinita, ScrubbingModeParameters lascia non impostate le tolleranze di ricerca frazionaria (null), pertanto le ricerche durante lo scrubbing utilizzano il valore SeekParameters del player (SeekParameters.EXACT per impostazione predefinita). Una ricerca esatta deve decodificare ogni frame dal keyframe precedente fino al timestamp target. Quando sono presenti molti fotogrammi dal keyframe precedente, ad esempio negli stream con ampi intervalli di keyframe o frequenze fotogrammi elevate, questo lavoro di decodifica può aggiungere una latenza evidente durante il trascinamento.
Sui touch screen, il dito di un utente non può distinguere in modo affidabile 1dp o 2dp di movimento lungo una barra di scorrimento. Per uno scrubber di larghezza widthDp, una tolleranza al tocco ridotta, ad esempio 2dp in entrambe le direzioni (4dp in totale), corrisponde a una frazione di 2.0 / widthDp della durata dei contenuti multimediali. La durata del tempo di riproduzione dei contenuti multimediali coperta da questa frazione dipende dalla durata del video. Ti consigliamo di passare una tolleranza basata su dp divisa per la larghezza dello scrubber della UI a setFractionalSeekTolerance (ad esempio 2dp o ottimizzata tra 1dp e 4dp per bilanciare velocità e precisione):
- Sui contenuti lunghi (ad esempio, un video di 1 ora su uno scrubber
400dp, dove2dpcopre 18 secondi), ExoPlayer può agganciarsi direttamente a un keyframe vicino all'interno di questa tolleranza di tocco, anziché decodificare i frame intermedi. - Nei clip brevi (ad esempio, un clip di 10 secondi sullo stesso scrubber
400dp, dove2dpcopre 50 millisecondi), la stessa frazione mantiene automaticamente le ricerche vicine al frame target.
Converti la tolleranza target di dp in pixel utilizzando la densità del display in modo che corrisponda alla larghezza in pixel misurata dello scrubber e aggiorna la tolleranza ogni volta che la larghezza dello scrubber o la densità del display cambiano, ad esempio durante il layout iniziale o la rotazione dello schermo.
In Jetpack Compose, converti la tolleranza di dp in pixel con LocalDensity,
misura lo slider personalizzato con Modifier.onSizeChanged e aggiorna
scrubbingModeParameters quando il player, la larghezza o la densità cambiano. Se l'istanza ExoPlayer sopravvive al composable, cancella la tolleranza al momento dell'eliminazione:
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 un'UI basata su View, converti la tolleranza dp in pixel utilizzando
DisplayMetrics.density, applica la larghezza corrente se la visualizzazione dello scrubber o
PlayerView è già stata disposta e utilizza View.addOnLayoutChangeListener per
aggiornare scrubbingModeParameters quando la larghezza cambia. Rimuovi il listener quando scolleghi l'interfaccia utente e cancella la tolleranza se l'istanza ExoPlayer sopravvive alla visualizzazione:
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()); }
Quando utilizzi una tolleranza basata su dp, mantenere attiva la modalità di scrubbing durante la chiamata seekTo finale in onScrubStop evita un salto visivo del frame quando l'utente solleva il dito. Se invece la tua app richiede che la posizione di rilascio finale arrivi su un timestamp esatto, chiama setScrubbingModeEnabled(false) prima della chiamata seekTo finale, in modo che la ricerca finale utilizzi il SeekParameters standard del player.
Considerazioni su contenuti multimediali e rete
La modalità di scrubbing ottimizza la pianificazione della ricerca locale e la decodifica hardware, quindi è più efficace con i file locali, i media scaricati, gli stream memorizzati nella cache o gli stream adattivi memorizzati nel buffer codificati con keyframe frequenti (GOP brevi). In caso di connessioni di rete non memorizzate nella cache e con latenza elevata, le ricerche frequenti di posizioni non memorizzate nel buffer rimangono vincolate ai tempi di round trip della rete.