Modo de arrastar

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ável Player e o ProgressSlider independente usam o modo de limpeza por padrão. Quando um usuário arrasta o controle deslizante, ProgressSlider chama setScrubbingModeEnabled(true) no início do arrasto, envia atualizações de seekTo durante o arrasto e chama setScrubbingModeEnabled(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): PlayerView e PlayerControlView incluem 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 definindo app:time_bar_scrubbing_enabled="true" em PlayerView ou no código chamando setTimeBarScrubbingEnabled(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 chame seekTo conforme 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ão Player.isPlaying retorna false. Quando você chama setScrubbingModeEnabled(false), a reprodução normal é retomada automaticamente se playWhenReady for true.
  • 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_AUDIO e C.TRACK_TYPE_METADATA para 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 MediaCodec e 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 MediaCodec ao 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 usa MediaCodec.BUFFER_FLAG_DECODE_ONLY no 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 que 2dp abrange 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 que 2dp abrange 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.