Modalità di scrubbing

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 composable Player sia l'elemento autonomo ProgressSlider utilizzano la modalità di scrubbing per impostazione predefinita. Quando un utente trascina il cursore, ProgressSlider chiama setScrubbingModeEnabled(true) all'inizio del trascinamento, invia aggiornamenti di seekTo durante il trascinamento e chiama setScrubbingModeEnabled(false) quando il trascinamento termina o il composable esce dalla composizione. Per saperne di più, consulta Material3 Compose.
  • Visualizzazioni (media3-ui): PlayerView e PlayerControlView includono 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 impostando app:time_bar_scrubbing_enabled="true" su PlayerView o nel codice chiamando setTimeBarScrubbingEnabled(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 chiama seekTo quando 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, pertanto Player.isPlaying restituisce false. Quando chiami setScrubbingModeEnabled(false), la riproduzione normale riprende automaticamente se playWhenReady è 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_AUDIO e C.TRACK_TYPE_METADATA per evitare scoppiettii audio e reimpostazioni del renderer non necessarie.
  • Aumenta la velocità effettiva del codec: aumenta la frequenza operativa del video MediaCodec e 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 MediaCodec non 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 utilizza MediaCodec.BUFFER_FLAG_DECODE_ONLY su 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, dove 2dp copre 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, dove 2dp copre 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.