Quando um usuário arrasta o controle deslizante de uma barra de busca em uma linha do tempo para visualizar frames e encontrar
uma posição de reprodução, a IU envia chamadas Player.seekTo frequentes em uma sucessão
curta. A busca padrão com Player.seekTo e
ExoPlayer.setSeekParameters foi projetada para saltos
isolados, em que cada nova busca cancela qualquer busca em andamento e redefine o decodificador.
Durante um gesto de arrastar contínuo, esse comportamento pode impedir que os frames de visualização sejam
renderizados até que o usuário pare de se mover.
ExoPlayer.setScrubbingModeEnabled configura o
player para limpeza interativa, de modo que os frames de prévia do vídeo sejam renderizados de forma responsiva conforme
o usuário arrasta.
Usar componentes de interface pré-criados
Os módulos de interface pré-criados do Media3 incluem suporte integrado ao modo de arrastar quando
conectados diretamente a uma instância ExoPlayer (não encapsulada em um
ForwardingPlayer ou acessada por um
MediaController):
- Jetpack Compose (
media3-ui-compose-material3): o elemento combinávelPlayere oProgressSliderindependente usam o modo de limpeza por padrão. Quando um usuário arrasta o controle deslizante,ProgressSliderchamasetScrubbingModeEnabled(true)no início do arrasto, envia atualizações deseekTodurante o arrasto e chamasetScrubbingModeEnabled(false)quando o arrasto termina ou o elemento que pode ser composto sai da composição. Para mais informações, consulte Material3 Compose. - Visualizações (
media3-ui):PlayerViewePlayerControlViewincluem a integração do modo de arrastar, que é desativada por padrão, buscando apenas uma vez quando o usuário solta a barra de tempo. Ative a depuração contínua em XML definindoapp:time_bar_scrubbing_enabled="true"emPlayerViewou no código chamandosetTimeBarScrubbingEnabled(true).
Mesmo ao usar ProgressSlider ou PlayerView, é possível personalizar
ScrubbingModeParameters na instância
ExoPlayer, como definir uma tolerância de busca fracionária da largura do
depurador.
Quando ativar e desativar o modo de arrastar
Se você implementar uma barra de busca personalizada ou um controle de barra de progressão, ative o modo de arrastar apenas durante um gesto de arrastar interativo do usuário:
- Ative o modo de limpeza (
setScrubbingModeEnabled(true)) quando o usuário começar a arrastar o controle deslizante e chameseekToconforme a posição de arrastar mudar. - Desative o modo de arrastar (
setScrubbingModeEnabled(false)) assim que o usuário terminar ou cancelar o gesto. Por exemplo, desative-o ao soltar o ponteiro, ao cancelar o gesto ou se o componente da IU for desanexado ou descartado no meio do arrasto. - Mantenha o modo de arrastar desativado durante a reprodução normal e buscas únicas, como pressionar os botões de avanço rápido ou retrocesso, tocar em uma barra de busca sem arrastar ou buscar de forma programática.
Deixar o modo de limpeza ativo fora de um gesto de arrastar tem efeitos colaterais indesejados:
- A reprodução é suprimida: enquanto o modo de limpeza está ativado, o ExoPlayer define
o motivo da supressão de reprodução como
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, entãoPlayer.isPlayingretornafalse. Quando você chamasetScrubbingModeEnabled(false), a reprodução normal é retomada automaticamente seplayWhenReadyfortrue. - As faixas não visuais estão desativadas: as faixas de áudio e metadados estão desativadas por padrão durante a limpeza.
- Aumento do uso de energia e do decodificador: o modo de arrastar aumenta as taxas de operação do codec de vídeo, o que consome mais recursos de hardware e bateria se permanecer ativado.
O exemplo a seguir conecta callbacks de gestos de barra de rolagem 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); }
Como o modo de arrastar otimiza a busca
Com ScrubbingModeParameters.DEFAULT, o ExoPlayer aplica várias otimizações enquanto o modo de arrastar está ativo:
- Agrupa buscas rápidas: em vez de interromper uma busca ativa em cada
movimento do ponteiro, o ExoPlayer conclui a decodificação e a renderização do frame de destino
atual, mantendo apenas a posição de busca mais recente na fila. As buscas intermediárias substituídas são descartadas e informadas ao
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Desativa as faixas de áudio e metadados: desativa temporariamente as faixas
C.TRACK_TYPE_AUDIOeC.TRACK_TYPE_METADATApara evitar estalos de áudio e redefinições desnecessárias do renderizador. - Aumenta a capacidade do codec: aumenta a taxa de operação do vídeo
MediaCodece garante que a programação dinâmica esteja ativada para que os buffers de saída decodificados sejam processados mais rapidamente. - Reduz as redefinições do decodificador: evita limpezas desnecessárias de
MediaCodecao buscar um frame-chave em streams compatíveis, ignora a redefinição para o frame-chave ao buscar para frente no mesmo grupo de imagens (GOP, na sigla em inglês) e usaMediaCodec.BUFFER_FLAG_DECODE_ONLYno Android 14 (nível 34 da API) e versões mais recentes para que o decodificador pule a produção de buffers de saída para frames intermediários entre um frame-chave e o frame de destino.
Personalizar parâmetros de limpeza
Use ScrubbingModeParameters.Builder para ajustar o comportamento
de limpeza, como quais tipos de faixa estão desativados ou se as taxas de operação do codec
são aumentadas. Aplique a configuração com
ExoPlayer.setScrubbingModeParameters ou
ExoPlayer.Builder.setScrubbingModeParameters. Crie com base em
exoPlayer.scrubbingModeParameters.buildUpon() (ou
ScrubbingModeParameters.DEFAULT.buildUpon()) para que as otimizações padrão permaneçam
ativadas.
Defina uma tolerância de busca fracionária com base na largura da barra de rolagem
Por padrão, o ScrubbingModeParameters deixa as tolerâncias de busca fracionária sem definição
(null). Assim, as buscas durante a arrastar usam o
SeekParameters do player (SeekParameters.EXACT por padrão). Uma
busca exata precisa decodificar todos os frames do frame-chave anterior até o carimbo de data/hora
de destino. Quando há muitos frames desde o frame-chave anterior, como em
streams com intervalos amplos de frames-chave ou frame rates altos, esse trabalho de decodificação
pode adicionar latência perceptível ao arrastar.
Em touch screens, o dedo de um usuário não consegue distinguir de forma confiável 1dp ou 2dp de
movimento ao longo de uma barra de rolagem. Para um controle de largura widthDp, uma pequena tolerância
ao toque, como 2dp em qualquer direção (4dp no total), corresponde a uma
fração de 2.0 / widthDp da duração da mídia. O tempo de mídia que essa fração abrange depende da duração do vídeo. Recomendamos transmitir uma tolerância baseada em dp dividida pela largura do controle de limpeza da IU para setFractionalSeekTolerance (por exemplo, 2dp ou ajustada entre 1dp e 4dp para equilibrar velocidade e precisão):
- Em conteúdos longos (por exemplo, um vídeo de 1 hora em um controle de
400dp, em que2dpabrange 18 segundos), o ExoPlayer pode se ajustar diretamente a um frame-chave próximo dentro dessa tolerância de toque, em vez de decodificar frames intermediários. - Em clipes curtos (por exemplo, um clipe de 10 segundos no mesmo controle de limpeza
400dp, em que2dpabrange 50 milissegundos), a mesma fração mantém automaticamente as buscas próximas ao frame de destino.
Converta a tolerância de dp de destino em pixels usando a densidade da tela para que ela
corresponda à largura de pixel medida do controle deslizante e atualize a tolerância sempre que
a largura do controle deslizante ou a densidade da tela mudar, como no layout inicial ou
na rotação da tela.
No Jetpack Compose, converta a tolerância de dp em pixels com LocalDensity,
meça o controle deslizante personalizado com Modifier.onSizeChanged e atualize
scrubbingModeParameters quando o player, a largura ou a densidade mudarem. Se a
instância ExoPlayer durar mais que o elemento combinável, limpe a tolerância na eliminação:
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() }
Em uma interface baseada em visualização, converta a tolerância de dp em pixels usando
DisplayMetrics.density, aplique a largura atual se a visualização do controle deslizante ou
PlayerView já estiver definida e use View.addOnLayoutChangeListener para
atualizar scrubbingModeParameters quando a largura mudar. Remova o listener
ao desanexar a interface e limpe a tolerância se a instância ExoPlayer
durar mais que a visualização:
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()); }
Ao usar uma tolerância baseada em dp, manter o modo de arrastar ativado até a chamada final de seekTo em onScrubStop evita um salto visual de frame quando o usuário levanta o dedo. Se o app exigir que a posição de liberação final
chegue a um carimbo de data/hora exato, chame setScrubbingModeEnabled(false) antes da
chamada final de seekTo para que a busca final use o SeekParameters
padrão do player.
Considerações sobre mídia e rede
O modo de arrastar otimiza a programação de busca local e a decodificação de hardware, por isso é mais eficaz com arquivos locais, mídia baixada, streams em cache ou streams adaptáveis em buffer codificados com frames-chave frequentes (GOPs curtos). Em conexões de rede de alta latência sem armazenamento em cache, as buscas frequentes de posições sem buffer permanecem limitadas pelos tempos de retorno da rede.