Cuando un usuario arrastra el control deslizante de una barra de búsqueda por una línea de tiempo para obtener una vista previa de los fotogramas y encontrar una posición de reproducción, la IU envía llamadas Player.seekTo frecuentes en una sucesión corta. La búsqueda estándar con Player.seekTo y ExoPlayer.setSeekParameters está diseñada para saltos aislados, en los que cada búsqueda nueva anula cualquier búsqueda en curso y restablece el decodificador.
Durante un gesto de arrastre continuo, ese comportamiento puede impedir que se rendericen los fotogramas de vista previa hasta que el usuario deje de moverse.
ExoPlayer.setScrubbingModeEnabled configura el reproductor para el desplazamiento interactivo, de modo que los fotogramas de vista previa del video se rendericen de forma responsiva a medida que el usuario arrastra el control.
Usa componentes de la IU prediseñados
Los módulos de IU precompilados de Media3 incluyen compatibilidad integrada con el modo de arrastre cuando se conectan directamente a una instancia de ExoPlayer (no unida en un ForwardingPlayer ni a la que se accede a través de un MediaController):
- Jetpack Compose (
media3-ui-compose-material3): Tanto el elemento componiblePlayercomo el elementoProgressSliderindependiente usan el modo de limpieza de forma predeterminada. Cuando un usuario arrastra el control deslizante,ProgressSliderllama asetScrubbingModeEnabled(true)al comienzo del arrastre, envía actualizaciones deseekTodurante el arrastre y llama asetScrubbingModeEnabled(false)cuando finaliza el arrastre o el elemento componible sale de la composición. Para obtener más información, consulta Material3 Compose. - Vistas (
media3-ui):PlayerViewyPlayerControlViewincluyen la integración del modo de arrastre que está inhabilitado de forma predeterminada, que busca solo una vez cuando el usuario suelta la barra de tiempo. Para habilitar el arrastre continuo en XML, configuraapp:time_bar_scrubbing_enabled="true"enPlayerViewo, en el código, llama asetTimeBarScrubbingEnabled(true).
Incluso cuando usas ProgressSlider o PlayerView, puedes personalizar
ScrubbingModeParameters en la instancia de
ExoPlayer, como establecer una tolerancia de búsqueda fraccionaria desde el ancho
del depurador.
Cuándo activar y desactivar el modo de arrastre
Si implementas una barra deslizante de búsqueda o un control de limpieza personalizados, activa el modo de limpieza solo durante el gesto de limpieza interactivo del usuario:
- Activa el modo de limpieza (
setScrubbingModeEnabled(true)) cuando el usuario comience a arrastrar el control deslizante y llama aseekToa medida que cambie la posición de arrastre. - Desactiva el modo de arrastre (
setScrubbingModeEnabled(false)) en cuanto el usuario finalice o cancele el gesto. Por ejemplo, desactívalo cuando se suelte el puntero, cuando se cancele el gesto o si el componente de la IU se desconecta o se elimina durante el arrastre. - Mantén desactivado el modo de arrastre durante la reproducción normal y las búsquedas individuales, como presionar los botones de avance rápido o retroceso, presionar una barra deslizante de búsqueda sin arrastrar o realizar búsquedas de manera programática.
Dejar activo el modo de limpieza fuera de un gesto de arrastre tiene efectos secundarios no deseados:
- La reproducción se suprime: Mientras el modo de limpieza está habilitado, ExoPlayer establece el motivo de la supresión de la reproducción en
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, por lo quePlayer.isPlayingdevuelvefalse. Cuando llamas asetScrubbingModeEnabled(false), la reproducción normal se reanuda automáticamente siplayWhenReadyestrue. - Las pistas no visuales están inhabilitadas: Las pistas de audio y metadatos están inhabilitadas de forma predeterminada durante el barrido.
- Aumento del uso de energía y del decodificador: El modo de limpieza aumenta las tasas de funcionamiento del códec de video, lo que consume recursos adicionales de hardware y batería si se deja habilitado.
En el siguiente ejemplo, se conectan las devoluciones de llamada de gestos de limpieza personalizados 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); }
Cómo el modo de arrastre optimiza la búsqueda
Con ScrubbingModeParameters.DEFAULT, ExoPlayer aplica varias optimizaciones mientras el modo de arrastre está activo:
- Une las búsquedas rápidas: En lugar de interrumpir una búsqueda activa en cada movimiento del puntero, ExoPlayer termina de decodificar y renderizar el fotograma de destino actual mientras mantiene solo la posición de búsqueda más reciente en cola. Las búsquedas intermedias reemplazadas se descartan y se informan a
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Inhabilita las pistas de audio y metadatos: Inhabilita temporalmente las pistas
C.TRACK_TYPE_AUDIOyC.TRACK_TYPE_METADATApara evitar ruidos de audio y restablecimientos innecesarios del renderizador. - Aumenta la capacidad de procesamiento del códec: Aumenta la tasa de funcionamiento de
MediaCodecdel video y garantiza que la programación dinámica esté habilitada para que los búferes de salida decodificados se procesen más rápido. - Reduce los restablecimientos del decodificador: Evita vaciados innecesarios de
MediaCodeccuando se busca un fotograma clave en transmisiones compatibles, omite el restablecimiento al fotograma clave cuando se busca hacia adelante dentro del mismo grupo de imágenes (GOP) y usaMediaCodec.BUFFER_FLAG_DECODE_ONLYen Android 14 (nivel de API 34) y versiones posteriores para que el decodificador omita la producción de búferes de salida para fotogramas intermedios entre un fotograma clave y el fotograma de destino.
Personaliza los parámetros de limpieza
Usa ScrubbingModeParameters.Builder para ajustar el comportamiento de arrastre, como qué tipos de pistas están desactivados o si se aumentan las tasas de funcionamiento de los códecs. Aplica la configuración con
ExoPlayer.setScrubbingModeParameters o
ExoPlayer.Builder.setScrubbingModeParameters. Compila desde
exoPlayer.scrubbingModeParameters.buildUpon() (o
ScrubbingModeParameters.DEFAULT.buildUpon()) para que las optimizaciones predeterminadas permanezcan
habilitadas.
Establece una tolerancia de búsqueda fraccionaria a partir del ancho de la barra de desplazamiento
De forma predeterminada, ScrubbingModeParameters deja sin establecer las tolerancias de búsqueda fraccionaria (null), por lo que las búsquedas durante el arrastre usan el SeekParameters del reproductor (SeekParameters.EXACT de forma predeterminada). Una búsqueda exacta debe decodificar cada fotograma desde el fotograma clave anterior hasta la marca de tiempo de destino. Cuando hay muchos fotogramas desde el fotograma clave anterior, como en las transmisiones con intervalos de fotogramas clave amplios o velocidades de fotogramas altas, este trabajo de decodificación puede agregar una latencia notable mientras se arrastra.
En las pantallas táctiles, el dedo de un usuario no puede distinguir de manera confiable 1dp o 2dp de movimiento a lo largo de una barra de desplazamiento. Para un control de limpieza de ancho widthDp, una tolerancia táctil pequeña, como 2dp en cualquier dirección (4dp en total), corresponde a una fracción de 2.0 / widthDp de la duración del contenido multimedia. La cantidad de tiempo de contenido multimedia que abarca esa fracción depende de la duración del video. Te recomendamos que pases una tolerancia basada en dp dividida por el ancho del control de limpieza de la IU a setFractionalSeekTolerance (por ejemplo, 2dp o ajustada entre 1dp y 4dp para equilibrar la velocidad y la precisión):
- En el contenido largo (por ejemplo, un video de 1 hora en un control deslizante de
400dp, en el que2dpabarca 18 segundos), ExoPlayer puede ajustarse directamente a un fotograma clave cercano dentro de esa tolerancia táctil en lugar de decodificar fotogramas intermedios. - En los clips cortos (por ejemplo, un clip de 10 segundos en el mismo depurador de
400dp, en el que2dpabarca 50 milisegundos), la misma fracción mantiene automáticamente las búsquedas cerca del fotograma objetivo.
Convierte tu tolerancia de dp objetivo en píxeles con la densidad de la pantalla para que coincida con el ancho de píxeles medido del control de limpieza y actualiza la tolerancia cada vez que cambie el ancho del control de limpieza o la densidad de la pantalla, como en el diseño inicial o la rotación de la pantalla.
En Jetpack Compose, convierte la tolerancia de dp en píxeles con LocalDensity, mide el control deslizante personalizado con Modifier.onSizeChanged y actualiza scrubbingModeParameters cuando cambien el reproductor, el ancho o la densidad. Si la
ExoPlayer instancia sobrevive al elemento componible, borra la tolerancia en la eliminación:
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() }
En una IU basada en vistas, convierte la tolerancia de dp en píxeles con DisplayMetrics.density, aplica el ancho actual si tu vista de barra de desplazamiento o PlayerView ya están diseñadas y usa View.addOnLayoutChangeListener para actualizar scrubbingModeParameters cuando cambie su ancho. Quita el objeto de escucha cuando desconectes la IU y borra la tolerancia si la instancia de ExoPlayer sobrevive a la vista:
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()); }
Cuando usas una tolerancia basada en dp, mantener habilitado el modo de arrastre durante la llamada final a seekTo en onScrubStop evita un salto visual de fotogramas cuando el usuario levanta el dedo. Si, en cambio, tu app requiere que la posición de liberación final se ubique en una marca de tiempo exacta, llama a setScrubbingModeEnabled(false) antes de la llamada final a seekTo para que la búsqueda final use el SeekParameters estándar del reproductor.
Consideraciones sobre los medios y la red
El modo de limpieza optimiza la programación de búsqueda local y la decodificación de hardware, por lo que es más eficaz con archivos locales, contenido multimedia descargado, transmisiones almacenadas en caché o transmisiones adaptables almacenadas en búfer codificadas con fotogramas clave frecuentes (GOP cortos). En las conexiones de red de alta latencia sin almacenamiento en caché, las búsquedas frecuentes de posiciones sin almacenamiento en búfer siguen estando limitadas por los tiempos de ida y vuelta de la red.