Lorsqu'un utilisateur fait glisser le curseur d'une barre de recherche sur une chronologie pour prévisualiser des images et trouver une position de lecture, l'UI envoie fréquemment des appels Player.seekTo à intervalles rapprochés. La recherche standard avec Player.seekTo et ExoPlayer.setSeekParameters est conçue pour les sauts isolés, où chaque nouvelle recherche annule toute recherche en cours et réinitialise le décodeur.
Lors d'un geste de déplacement continu, ce comportement peut empêcher l'affichage des images d'aperçu jusqu'à ce que l'utilisateur arrête de bouger.
ExoPlayer.setScrubbingModeEnabled configure le lecteur pour le scrubbing interactif afin que les images d'aperçu de la vidéo s'affichent de manière responsive lorsque l'utilisateur fait glisser le curseur.
Utiliser des composants d'interface utilisateur prédéfinis
Les modules d'interface utilisateur prédéfinis de Media3 incluent la prise en charge intégrée du mode de défilement lorsqu'ils sont connectés directement à une instance ExoPlayer (non encapsulée dans un ForwardingPlayer ou accessible via un MediaController) :
- Jetpack Compose (
media3-ui-compose-material3) : le composablePlayeret le composant autonomeProgressSliderutilisent le mode de balayage par défaut. Lorsqu'un utilisateur fait glisser le curseur,ProgressSliderappellesetScrubbingModeEnabled(true)au début du déplacement, distribue les mises à jour deseekTopendant le déplacement et appellesetScrubbingModeEnabled(false)lorsque le déplacement se termine ou que le composable quitte la composition. Pour en savoir plus, consultez Material3 Compose. - Vues (
media3-ui) :PlayerViewetPlayerControlViewincluent l'intégration du mode d'utilisation de la barre de lecture, qui est désactivé par défaut, et ne recherchent qu'une seule fois lorsque l'utilisateur relâche la barre de temps. Activez l'utilisation continue de la barre de lecture en XML en définissantapp:time_bar_scrubbing_enabled="true"surPlayerView, ou dans le code en appelantsetTimeBarScrubbingEnabled(true).
Même lorsque vous utilisez ProgressSlider ou PlayerView, vous pouvez personnaliser ScrubbingModeParameters sur l'instance ExoPlayer, par exemple en définissant une tolérance de recherche fractionnaire à partir de la largeur de la barre de lecture.
Quand activer et désactiver le mode de nettoyage ?
Si vous implémentez une barre de recherche ou un curseur de défilement personnalisé, n'activez le mode de défilement que pendant la durée d'un geste de défilement interactif de l'utilisateur :
- Activez le mode de défilement (
setScrubbingModeEnabled(true)) lorsque l'utilisateur commence à faire glisser le curseur de défilement, puis appelezseekTolorsque la position de déplacement change. - Désactivez le mode d'utilisation de la barre de lecture (
setScrubbingModeEnabled(false)) dès que l'utilisateur termine ou annule le geste. Par exemple, désactivez-le lorsque le pointeur est relâché, lorsque le geste est annulé, ou si votre composant d'interface utilisateur est détaché ou supprimé en cours de déplacement. - Désactivez le mode de balayage pendant la lecture normale et les recherches uniques, par exemple lorsque l'utilisateur appuie sur les boutons d'avance rapide ou de retour arrière, appuie sur une barre de recherche sans la faire glisser, ou effectue une recherche de manière programmatique.
Laisser le mode d'utilisation de la barre de lecture actif en dehors d'un geste de déplacement entraîne des effets secondaires indésirables :
- La lecture est supprimée : lorsque le mode de balayage est activé, ExoPlayer définit la raison de la suppression de la lecture sur
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING. Par conséquent,Player.isPlayingrenvoiefalse. Lorsque vous appelezsetScrubbingModeEnabled(false), la lecture normale reprend automatiquement siplayWhenReadyest défini surtrue. - Les pistes non visuelles sont désactivées : les pistes audio et de métadonnées sont désactivées par défaut pendant le scrubbing.
- Augmentation de la consommation d'énergie et de l'utilisation du décodeur : le mode d'utilisation de la barre de lecture augmente les taux de fonctionnement du codec vidéo, ce qui consomme des ressources matérielles et de batterie supplémentaires s'il reste activé.
L'exemple suivant associe des rappels de gestes de défilement personnalisés à 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); }
Comment le mode de défilement rapide optimise la recherche
Avec ScrubbingModeParameters.DEFAULT, ExoPlayer applique plusieurs optimisations lorsque le mode d'utilisation de la barre de lecture est actif :
- Fusionne les recherches rapides : au lieu de préempter une recherche active à chaque déplacement du pointeur, ExoPlayer termine le décodage et l'affichage de l'image cible actuelle tout en ne conservant que la position de recherche la plus récente dans la file d'attente. Les recherches intermédiaires remplacées sont supprimées et signalées à
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Désactive les pistes audio et de métadonnées : désactive temporairement les pistes
C.TRACK_TYPE_AUDIOetC.TRACK_TYPE_METADATApour éviter les bruits audio et les réinitialisations inutiles du moteur de rendu. - Augmente le débit du codec : augmente le taux de fonctionnement de
MediaCodecde la vidéo et s'assure que la programmation dynamique est activée afin que les tampons de sortie décodés soient traités plus rapidement. - Réduction des réinitialisations du décodeur : évite les vidages
MediaCodecinutiles lors de la recherche d'une image clé dans les flux compatibles, ignore la réinitialisation de l'image clé lors de la recherche vers l'avant dans le même groupe d'images (GOP) et utiliseMediaCodec.BUFFER_FLAG_DECODE_ONLYsur Android 14 (niveau d'API 34) ou version ultérieure afin que le décodeur ignore la production de tampons de sortie pour les images intermédiaires entre une image clé et l'image cible.
Personnaliser les paramètres de nettoyage
Utilisez ScrubbingModeParameters.Builder pour ajuster le comportement d'utilisation de la barre de lecture, par exemple les types de pistes désactivés ou l'augmentation des taux de fonctionnement des codecs. Appliquez la configuration à l'aide de la commande ExoPlayer.setScrubbingModeParameters ou ExoPlayer.Builder.setScrubbingModeParameters. Créez à partir de exoPlayer.scrubbingModeParameters.buildUpon() (ou ScrubbingModeParameters.DEFAULT.buildUpon()) afin que les optimisations par défaut restent activées.
Définir une tolérance de recherche fractionnaire à partir de la largeur de la barre de défilement
Par défaut, ScrubbingModeParameters ne définit pas les tolérances de recherche fractionnaire (null). Par conséquent, les recherches pendant le scrubbing utilisent la valeur SeekParameters du lecteur (SeekParameters.EXACT par défaut). Une recherche exacte doit décoder chaque frame, de l'image clé précédente au code temporel cible. Lorsqu'il y a de nombreuses images depuis l'image clé précédente, par exemple dans les flux avec de grands intervalles d'images clés ou des fréquences d'images élevées, ce travail de décodage peut ajouter une latence notable lors du déplacement.
Sur les écrans tactiles, le doigt d'un utilisateur ne peut pas distinguer de manière fiable un mouvement de 1dp ou 2dp le long d'une barre de défilement. Pour un curseur de largeur widthDp, une petite tolérance tactile telle que 2dp dans l'une ou l'autre direction (4dp au total) correspond à une fraction de 2.0 / widthDp de la durée du contenu multimédia. La durée de lecture de cette fraction dépend de la durée de la vidéo. Nous vous recommandons de transmettre une tolérance basée sur dp divisée par la largeur de votre barre de défilement d'UI à setFractionalSeekTolerance (par exemple, 2dp, ou ajustée entre 1dp et 4dp pour équilibrer vitesse et précision) :
- Sur un contenu long (par exemple, une vidéo d'une heure sur un curseur de
400dp, où2dps'étend sur 18 secondes), ExoPlayer peut s'aligner directement sur une image clé à proximité dans cette tolérance tactile au lieu de décoder les images intermédiaires. - Sur les clips courts (par exemple, un clip de 10 secondes sur la même barre de défilement
400dp, où2dps'étend sur 50 millisecondes), la même fraction maintient automatiquement les recherches à proximité de l'image cible.
Convertissez la tolérance dp de votre cible en pixels à l'aide de la densité d'affichage afin qu'elle corresponde à la largeur en pixels mesurée de la barre de lecture, puis mettez à jour la tolérance chaque fois que la largeur de la barre de lecture ou la densité d'affichage change, par exemple lors de la mise en page initiale ou de la rotation de l'écran.
Dans Jetpack Compose, convertissez la tolérance dp en pixels avec LocalDensity, mesurez votre curseur personnalisé avec Modifier.onSizeChanged et mettez à jour scrubbingModeParameters lorsque le lecteur, la largeur ou la densité changent. Si l'instance ExoPlayer survit au composable, effacez la tolérance lors de la suppression :
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() }
Dans une UI basée sur les vues, convertissez la tolérance dp en pixels à l'aide de DisplayMetrics.density, appliquez la largeur actuelle si votre vue de défilement ou PlayerView est déjà mise en page, et utilisez View.addOnLayoutChangeListener pour mettre à jour scrubbingModeParameters lorsque sa largeur change. Supprimez l'écouteur lorsque vous dissociez l'UI, puis effacez la tolérance si l'instance ExoPlayer survit à la vue :
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()); }
Lorsque vous utilisez une tolérance basée sur dp, le fait de laisser le mode d'utilisation de la barre de lecture activé lors de l'appel final de seekTo dans onScrubStop évite un saut d'image visuel lorsque l'utilisateur lève le doigt. Si votre application nécessite que la position de relâchement finale se trouve sur un code temporel exact, appelez setScrubbingModeEnabled(false) avant votre appel seekTo final afin que la recherche finale utilise le SeekParameters standard du lecteur.
Remarques sur les médias et le réseau
Le mode de balayage optimise la planification de la recherche locale et le décodage matériel. Il est donc plus efficace avec les fichiers locaux, les contenus multimédias téléchargés, les flux mis en cache ou les flux adaptatifs mis en mémoire tampon encodés avec des images clés fréquentes (GOP courts). Sur les connexions réseau non mises en cache et à latence élevée, les recherches fréquentes de positions non mises en mémoire tampon restent limitées par les délais aller-retour du réseau.