İleri geri oynatma modu

Kullanıcı, kareleri önizlemek ve oynatma konumu bulmak için arama çubuğu tutma yerini zaman çizelgesi üzerinde sürüklediğinde kullanıcı arayüzü, kısa aralıklarla sık sık Player.seekTo çağrıları gönderir. Player.seekTo ve ExoPlayer.setSeekParameters ile standart arama, her yeni aramanın devam eden aramaları iptal ettiği ve kod çözücüyü sıfırladığı bağımsız atlamalar için tasarlanmıştır. Sürekli sürükleme hareketi sırasında bu davranış, kullanıcı hareketi durdurana kadar önizleme karelerinin oluşturulmasını engelleyebilir.

ExoPlayer.setScrubbingModeEnabled, oynatıcıyı etkileşimli ileri geri oynatma için yapılandırır. Böylece video önizleme kareleri, kullanıcı sürükledikçe duyarlı bir şekilde oluşturulur.

Önceden oluşturulmuş kullanıcı arayüzü bileşenlerini kullanma

Media3'ün önceden oluşturulmuş kullanıcı arayüzü modülleri, doğrudan bir ExoPlayer örneğine bağlandığında (ForwardingPlayer içinde sarmalanmamış veya bir MediaController üzerinden erişilmemiş) yerleşik ileri geri oynatma modu desteği içerir:

  • Jetpack Compose (media3-ui-compose-material3): Hem Player composable hem de bağımsız ProgressSlider, varsayılan olarak ileri geri oynatma modunu kullanır. Kullanıcı kaydırma çubuğunu sürüklediğinde ProgressSlider, sürükleme işleminin başında setScrubbingModeEnabled(true) işlevini çağırır, sürükleme sırasında seekTo güncellemelerini gönderir ve sürükleme işlemi sona erdiğinde veya composable, oluşturma işleminden ayrıldığında setScrubbingModeEnabled(false) işlevini çağırır. Daha fazla bilgi için Material3 Compose sayfasına bakın.
  • Görünümler (media3-ui): PlayerView ve PlayerControlView, varsayılan olarak devre dışı bırakılan ve yalnızca kullanıcı zaman çubuğunu bıraktığında bir kez arama yapan ileri geri oynatma modu entegrasyonunu içerir. XML'de PlayerView üzerinde app:time_bar_scrubbing_enabled="true" ayarını yaparak veya kodda setTimeBarScrubbingEnabled(true) yöntemini çağırarak sürekli ileri geri oynatmayı etkinleştirin.

ProgressSlider veya PlayerView kullansanız bile ExoPlayer örneğinde ScrubbingModeParameters öğesini özelleştirebilirsiniz (ör. kaydırma çubuğu genişliğinden kesirli bir arama toleransı ayarlama).

İleri geri oynatma modunu açma ve kapatma zamanı

Özel bir sarma çubuğu veya ilerleme noktası kontrolü uyguluyorsanız ileri geri oynatma modunu yalnızca etkileşimli bir kullanıcı ileri geri oynatma hareketi süresince etkinleştirin:

  • Kullanıcı ilerleme noktası sürüklemeye başladığında ileri geri oynatma modunu etkinleştirin (setScrubbingModeEnabled(true)) ve sürükleme konumu değiştikçe seekTo yöntemini çağırın.
  • Kullanıcı hareketi tamamladığında veya iptal ettiğinde ileri geri oynatma modunu kapatın (setScrubbingModeEnabled(false)). Örneğin, işaretçi bırakıldığında, hareket iptal edildiğinde veya kullanıcı arayüzü bileşeniniz sürükleme sırasında ayrıldığında ya da silindiğinde bu özelliği devre dışı bırakın.
  • Normal oynatma ve tekli arama sırasında ileri geri oynatma modunu kapalı tutun. Örneğin, ileri sarma veya geri sarma düğmelerine basma, sarma çubuğuna sürüklemeden dokunma veya programatik olarak arama yapma.

Sürükleme hareketi dışında ileri geri oynatma modunu etkin bırakmanın istenmeyen yan etkileri vardır:

  • Oynatma engellenir: Sarma modu etkinken ExoPlayer, oynatmanın engellenme nedenini Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING olarak ayarlar. Bu nedenle Player.isPlaying, false değerini döndürür. setScrubbingModeEnabled(false) yöntemini çağırdığınızda playWhenReady, true ise normal oynatma otomatik olarak devam eder.
  • Görsel olmayan parçalar devre dışı bırakılır: Ses ve meta veri parçaları, ileri geri oynatma sırasında varsayılan olarak devre dışı bırakılır.
  • Güç ve kod çözücü kullanımı artar: İleri geri oynatma modu, video codec'inin çalışma hızını artırır. Bu mod etkin bırakılırsa ek donanım ve pil kaynakları tüketilir.

Aşağıdaki örnekte, özel kaydırma çubuğu hareketi geri çağırmaları ExoPlayer öğesine bağlanmaktadır:

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);
}

Sarma modunun ileri geri oynatmayı optimize etme şekli

ScrubbingModeParameters.DEFAULT ile ExoPlayer, ileri geri oynatma modu etkinken çeşitli optimizasyonlar uygular:

  • Hızlı aramaları birleştirir: ExoPlayer, her işaretçi hareketinde etkin bir aramayı önceden almak yerine, yalnızca sıraya alınan en son arama konumunu tutarken mevcut hedef kareyi çözmeyi ve oluşturmayı tamamlar. Yerine yenisi gelen ara aramalar bırakılır ve AnalyticsListener.onDroppedSeeksWhileScrubbing'e bildirilir.
  • Ses ve meta veri parçalarını devre dışı bırakır: Ses patlamalarını ve gereksiz oluşturucu sıfırlamalarını önlemek için C.TRACK_TYPE_AUDIO ve C.TRACK_TYPE_METADATA parçalarını geçici olarak devre dışı bırakır.
  • Codec işleme kapasitesini artırır: Video MediaCodec çalışma hızını artırır ve dinamik planlamanın etkinleştirilmesini sağlayarak kodu çözülmüş çıkış arabelleklerinin daha hızlı işlenmesini sağlar.
  • Kod çözücü sıfırlama işlemlerini azaltır: Uyumlu akışlarda bir ana kareye giderken gereksiz MediaCodec temizleme işlemlerini önler, aynı resim grubu (GOP) içinde ileri doğru giderken ana kareye sıfırlama işlemini atlar ve Android 14 (API düzeyi 34) ve sonraki sürümlerde MediaCodec.BUFFER_FLAG_DECODE_ONLY kullanarak kod çözücünün bir ana kare ile hedef kare arasındaki ara kareler için çıkış arabellekleri oluşturmayı atlamasını sağlar.

Temizleme parametrelerini özelleştirme

Hangi parça türlerinin devre dışı bırakılacağı veya codec çalışma hızlarının artırılıp artırılmayacağı gibi ileri geri oynatma davranışını ayarlamak için ScrubbingModeParameters.Builder kullanın. Yapılandırmayı ExoPlayer.setScrubbingModeParameters veya ExoPlayer.Builder.setScrubbingModeParameters ile uygulayın. Varsayılan optimizasyonların etkin kalması için exoPlayer.scrubbingModeParameters.buildUpon() (veya ScrubbingModeParameters.DEFAULT.buildUpon()) ile derleyin.

Kaydırıcı genişliğinden kesirli bir arama toleransı ayarlayın

Varsayılan olarak ScrubbingModeParameters, kesirli arama toleranslarını ayarlanmamış olarak bırakır (null). Bu nedenle, ileri geri oynatma sırasında aramalar oynatıcının SeekParameters değerini (varsayılan olarak SeekParameters.EXACT) kullanır. Tam arama, önceki ana kareden hedef zaman damgasına kadar her kareyi çözmelidir. Önceki kare anahtarından bu yana çok sayıda kare olduğunda (ör. geniş kare anahtarı aralıkları veya yüksek kare hızları olan akışlarda) bu kod çözme işlemi, sürükleme sırasında fark edilir bir gecikmeye neden olabilir.

Dokunmatik ekranlarda, kullanıcının parmağı kaydırma çubuğu boyunca 1dp veya 2dp hareketi güvenilir bir şekilde ayırt edemez. widthDp genişliğindeki bir kaydırma çubuğu için her iki yönde 2dp (toplam 4dp) gibi küçük bir dokunma toleransı, medya süresinin 2.0 / widthDp kadarına karşılık gelir. Bu kesrin kapsadığı medya süresi, videonun uzunluğuna bağlıdır. setFractionalSeekTolerance'ye, kullanıcı arayüzü kaydırıcınızın genişliğine bölünmüş dp tabanlı bir tolerans (ör. 2dp veya hız ile hassasiyeti dengelemek için 1dp ile 4dp arasında ayarlanmış) iletmenizi öneririz:

  • Uzun içeriklerde (ör. 400dp kaydırma çubuğunda 1 saatlik bir video, 2dp 18 saniyeyi kapsar) ExoPlayer, ara karelerin kodunu çözmek yerine doğrudan dokunma toleransı dahilindeki yakındaki bir kareye geçebilir.
  • Kısa kliplerde (örneğin, aynı 400dp kaydırıcıda 10 saniyelik bir klip, burada 2dp 50 milisaniyeyi kapsar), aynı kesir, aramaları otomatik olarak hedef kareye yakın tutar.

Hedef dp toleransınızı ekran yoğunluğunu kullanarak piksele dönüştürün. Böylece, tolerans, kaydırıcının ölçülen piksel genişliğiyle eşleşir. Kaydırıcının genişliği veya ekran yoğunluğu her değiştiğinde (ör. ilk düzende veya ekran döndürüldüğünde) toleransı güncelleyin.

Jetpack Compose'da dp toleransını LocalDensity ile piksele dönüştürün, özel kaydırma çubuğunuzu Modifier.onSizeChanged ile ölçün ve oynatıcı, genişlik veya yoğunluk değiştiğinde scrubbingModeParameters değerini güncelleyin. ExoPlayer örneği, composable'dan daha uzun süre etkin kalırsa imha sırasında toleransı temizleyin:

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()
}

Görünüm tabanlı bir kullanıcı arayüzünde, DisplayMetrics.density kullanarak dp toleransını piksele dönüştürün, kaydırma çubuğu görünümünüz veya PlayerView zaten düzenlenmişse mevcut genişliği uygulayın ve genişliği değiştiğinde scrubbingModeParameters değerini güncellemek için View.addOnLayoutChangeListener kullanın. Kullanıcı arayüzünü ayırırken dinleyiciyi kaldırın ve ExoPlayer örneği görünümden daha uzun süre kalırsa toleransı temizleyin:

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());
}

dp tabanlı bir tolerans kullandığınızda, onScrubStop içindeki son seekTo çağrısı boyunca ileri geri oynatma modunu etkin tutmak, kullanıcı parmağını kaldırdığında görsel bir kare atlamasını önler. Uygulamanız, son bırakma konumunun tam bir zaman damgasına denk gelmesini gerektiriyorsa son seekTo çağrınızdan önce setScrubbingModeEnabled(false) çağrısı yapın. Böylece son arama, oynatıcının standart SeekParameters değerini kullanır.

Medya ve ağ ile ilgili dikkat edilmesi gerekenler

Sarma modu, yerel arama planlamasını ve donanım kod çözme işlemini optimize eder. Bu nedenle, yerel dosyalar, indirilen medya, önbelleğe alınmış akışlar veya sık ana karelerle (kısa GOP'ler) kodlanmış arabelleğe alınmış uyarlanabilir akışlarda en etkilidir. Önbelleğe alınmamış, yüksek gecikmeli ağ bağlantılarında, arabelleğe alınmamış konumlara sık sık yapılan aramalar ağ gidiş-dönüş süreleriyle sınırlı kalır.