स्क्रबिंग मोड

जब कोई उपयोगकर्ता फ़्रेम की झलक देखने और वीडियो चलाने की जगह ढूंढने के लिए, टाइमलाइन पर सीक बार थंब को खींचता है, तो यूज़र इंटरफ़ेस (यूआई) कम समय में बार-बार Player.seekTo कॉल भेजता है. Player.seekTo और ExoPlayer.setSeekParameters के साथ स्टैंडर्ड सीकिंग को अलग-अलग जंप के लिए डिज़ाइन किया गया है. इसमें हर नया सीक, इन-फ़्लाइट सीक को रद्द कर देता है और डिकोडर को रीसेट कर देता है. लगातार ड्रैग जेस्चर के दौरान, यह व्यवहार झलक वाले फ़्रेम को रेंडर होने से रोक सकता है. ऐसा तब तक होता है, जब तक उपयोगकर्ता जेस्चर को रोक नहीं देता.

ExoPlayer.setScrubbingModeEnabled, इंटरैक्टिव स्क्रबिंग के लिए प्लेयर को कॉन्फ़िगर करता है, ताकि उपयोगकर्ता के खींचने पर वीडियो की झलक वाले फ़्रेम, रिस्पॉन्सिव तरीके से रेंडर हों.

पहले से बने यूआई कॉम्पोनेंट इस्तेमाल करना

Media3 के पहले से बने यूज़र इंटरफ़ेस (यूआई) मॉड्यूल में, पहले से मौजूद स्क्रबिंग मोड की सुविधा शामिल होती है. ऐसा तब होता है, जब सीधे ExoPlayer इंस्टेंस से कनेक्ट किया जाता है. इसे ForwardingPlayer में रैप नहीं किया जाता या MediaController के ज़रिए ऐक्सेस नहीं किया जाता:

  • Jetpack Compose (media3-ui-compose-material3): Player composable और standalone, दोनों ProgressSlider डिफ़ॉल्ट रूप से स्क्रबिंग मोड का इस्तेमाल करते हैं. जब कोई उपयोगकर्ता स्लाइडर थंब को खींचता है, तो ProgressSlider, खींचने की शुरुआत में setScrubbingModeEnabled(true) को कॉल करता है. साथ ही, खींचने के दौरान seekTo अपडेट भेजता है. इसके अलावा, खींचने की प्रोसेस खत्म होने या कंपोज़ेबल के कंपोज़िशन से बाहर निकलने पर, setScrubbingModeEnabled(false) को कॉल करता है. ज़्यादा जानकारी के लिए, Material3 Compose देखें.
  • व्यू (media3-ui): PlayerView और PlayerControlView में स्क्रबिंग मोड इंटिग्रेशन शामिल है. यह डिफ़ॉल्ट रूप से बंद रहता है. यह सिर्फ़ तब काम करता है, जब उपयोगकर्ता टाइम बार को रिलीज़ करता है. एक्सएमएल में लगातार स्क्रब करने की सुविधा चालू करने के लिए, app:time_bar_scrubbing_enabled="true" को PlayerView पर सेट करें या कोड में setTimeBarScrubbingEnabled(true) को कॉल करें.

ProgressSlider या PlayerView का इस्तेमाल करते समय भी, ExoPlayer इंस्टेंस पर ScrubbingModeParameters को पसंद के मुताबिक बनाया जा सकता है. जैसे, स्क्रबर की चौड़ाई से फ़्रैक्शनल सीक टॉलरेंस सेट करना.

स्क्रबिंग मोड को कब चालू और बंद करना चाहिए

अगर आपने कस्टम सीक बार या स्क्रबर कंट्रोल लागू किया है, तो स्क्रबिंग मोड को सिर्फ़ तब चालू करें, जब उपयोगकर्ता इंटरैक्टिव तरीके से स्क्रबिंग जेस्चर का इस्तेमाल करे:

  • स्क्रबिंग मोड चालू करें (setScrubbingModeEnabled(true)) जब उपयोगकर्ता स्क्रबर थंब को खींचना शुरू करे और खींचने की जगह बदलने पर seekTo को कॉल करें.
  • जैसे ही उपयोगकर्ता जेस्चर को पूरा करे या रद्द करे, वैसे ही स्क्रबिंग मोड (setScrubbingModeEnabled(false)) को बंद करें. उदाहरण के लिए, पॉइंटर रिलीज़ होने पर, जेस्चर रद्द होने पर या यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट के अलग होने या ड्रैग करने के दौरान हटाए जाने पर, इसे बंद करें.
  • सामान्य प्लेबैक और सिंगल सीक के दौरान स्क्रबिंग मोड को बंद रखें, जैसे कि फ़ास्ट-फ़ॉरवर्ड या रिवाइंड बटन दबाना, बिना खींचे सीक बार पर टैप करना या प्रोग्राम के हिसाब से सीक करना.

खींचकर छोड़ने वाले जेस्चर के अलावा, स्क्रबिंग मोड को चालू रखने से अनचाहे खराब असर हो सकते हैं:

  • प्लेबैक बंद है: स्क्रबिंग मोड चालू होने पर, ExoPlayer प्लेबैक बंद होने की वजह को Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING पर सेट करता है. इसलिए, Player.isPlaying, false दिखाता है. setScrubbingModeEnabled(false) को कॉल करने पर, सामान्य प्लेबैक अपने-आप फिर से शुरू हो जाता है. ऐसा तब होता है, जब playWhenReady को true पर सेट किया गया हो.
  • नॉन-विज़ुअल ट्रैक बंद हैं: स्क्रबर का इस्तेमाल करते समय, ऑडियो और मेटाडेटा ट्रैक डिफ़ॉल्ट रूप से बंद रहते हैं.
  • पावर और डिकोडर का ज़्यादा इस्तेमाल: स्क्रबिंग मोड, वीडियो कोडेक के काम करने की स्पीड को बढ़ाता है. अगर यह मोड चालू रहता है, तो हार्डवेयर और बैटरी का ज़्यादा इस्तेमाल होता है.

यहां दिए गए उदाहरण में, कस्टम स्क्रबर जेस्चर कॉलबैक को 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);
}

स्क्रबिंग मोड, वीडियो में आगे या पीछे जाने की सुविधा को कैसे ऑप्टिमाइज़ करता है

ScrubbingModeParameters.DEFAULT की मदद से, ExoPlayer स्क्रबिंग मोड चालू होने पर, कई ऑप्टिमाइज़ेशन लागू करता है:

  • तेज़ी से किए जाने वाले सीक को एक साथ जोड़ता है: ExoPlayer, पॉइंटर के हर मूव पर ऐक्टिव सीक को पहले से रोकने के बजाय, मौजूदा टारगेट फ़्रेम को डिकोड और रेंडर करने की प्रोसेस को पूरा करता है. साथ ही, सूची में मौजूद सबसे हाल की सीक पोज़िशन को होल्ड करके रखता है. सुपरसीड किए गए इंटरमीडिएट सीक को ड्रॉप कर दिया जाता है और इसकी जानकारी AnalyticsListener.onDroppedSeeksWhileScrubbing को दी जाती है.
  • ऑडियो और मेटाडेटा ट्रैक बंद करता है: यह C.TRACK_TYPE_AUDIO और C.TRACK_TYPE_METADATA ट्रैक को कुछ समय के लिए बंद कर देता है, ताकि ऑडियो पॉप और रेंडरर को बार-बार रीसेट करने की ज़रूरत न पड़े.
  • कोडेक थ्रूपुट को बढ़ाता है: वीडियो MediaCodec ऑपरेटिंग रेट को बढ़ाता है और यह पक्का करता है कि डाइनैमिक शेड्यूलिंग चालू हो, ताकि डिकोड किए गए आउटपुट बफ़र तेज़ी से प्रोसेस हों.
  • डिकोडर रीसेट को कम करता है: यह साथ काम करने वाली स्ट्रीम में कीफ़्रेम खोजते समय, ग़ैर-ज़रूरी MediaCodec फ़्लश से बचाता है. साथ ही, इमेज के एक ही ग्रुप (जीओपी) में आगे खोजते समय, कीफ़्रेम पर वापस रीसेट करने की प्रोसेस को स्किप करता है. इसके अलावा, यह Android 14 (एपीआई लेवल 34) और इसके बाद के वर्शन पर MediaCodec.BUFFER_FLAG_DECODE_ONLY का इस्तेमाल करता है, ताकि डिकोडर, कीफ़्रेम और टारगेट फ़्रेम के बीच के इंटरमीडिएट फ़्रेम के लिए आउटपुट बफ़र बनाना स्किप कर सके.

स्क्रबिंग पैरामीटर को पसंद के मुताबिक बनाना

ScrubbingModeParameters.Builder का इस्तेमाल करके, स्क्रबिंग के व्यवहार में बदलाव करें. जैसे, किस तरह के ट्रैक बंद हैं या कोडेक के काम करने की दरें बढ़ाई गई हैं या नहीं. ExoPlayer.setScrubbingModeParameters या ExoPlayer.Builder.setScrubbingModeParameters का इस्तेमाल करके, कॉन्फ़िगरेशन लागू करें. exoPlayer.scrubbingModeParameters.buildUpon() (या ScrubbingModeParameters.DEFAULT.buildUpon()) से बनाएं, ताकि डिफ़ॉल्ट ऑप्टिमाइज़ेशन चालू रहें.

स्क्रबर की चौड़ाई के हिसाब से, फ़्रैक्शनल सीक टॉलरेंस सेट करना

डिफ़ॉल्ट रूप से, ScrubbingModeParameters फ़्रैक्शनल सीक टॉलरेंस को सेट नहीं करता है (null). इसलिए, स्क्रबिंग के दौरान सीक करने पर, प्लेयर के SeekParameters (डिफ़ॉल्ट रूप से SeekParameters.EXACT) का इस्तेमाल किया जाता है. एग्ज़ैक्ट सीक को पिछले कीफ़्रेम से लेकर टारगेट टाइमस्टैंप तक के हर फ़्रेम को डिकोड करना होगा. जब पिछले कीफ़्रेम के बाद कई फ़्रेम होते हैं, जैसे कि ज़्यादा कीफ़्रेम इंटरवल या ज़्यादा फ़्रेम रेट वाली स्ट्रीम में, तो डिकोड करने के इस काम की वजह से, खींचने और छोड़ने के दौरान इंतज़ार का समय बढ़ सकता है.

टच स्क्रीन पर, स्क्रबर बार के साथ 1dp या 2dp के मूवमेंट में, उपयोगकर्ता की उंगली से अंतर नहीं किया जा सकता. widthDp की चौड़ाई वाले स्क्रबर के लिए, किसी भी दिशा में 2dp (कुल 4dp) जैसी छोटी टच टॉलरेंस, मीडिया की अवधि के 2.0 / widthDp के हिस्से से मेल खाती है. यह इस बात पर निर्भर करता है कि वीडियो कितना लंबा है. हमारा सुझाव है कि आप dp-आधारित टॉलरेंस को यूज़र इंटरफ़ेस (यूआई) के स्क्रबर की चौड़ाई से भाग देकर, setFractionalSeekTolerance को पास करें. उदाहरण के लिए, 2dp या स्पीड और सटीक नतीजे के बीच संतुलन बनाने के लिए, 1dp और 4dp के बीच ट्यून करें:

  • लंबे कॉन्टेंट (उदाहरण के लिए, 400dp स्क्रबर पर एक घंटे का वीडियो, जहां 2dp 18 सेकंड तक चलता है) पर, ExoPlayer बीच के फ़्रेम को डिकोड करने के बजाय, सीधे उस टच टॉलरेंस के अंदर मौजूद आस-पास के कीफ़्रेम पर स्नैप कर सकता है.
  • छोटी क्लिप (उदाहरण के लिए, उसी 400dp स्क्रबर पर 10 सेकंड की क्लिप, जहां 2dp 50 मिलीसेकंड तक चलती है) पर, वही फ़्रैक्शन अपने-आप टारगेट फ़्रेम के करीब पहुंच जाता है.

डिसप्ले डेंसिटी का इस्तेमाल करके, अपने टारगेट dp टॉलरेंस को पिक्सल में बदलें, ताकि यह स्क्रबर की मेज़र की गई पिक्सल चौड़ाई से मैच हो. साथ ही, जब भी स्क्रबर की चौड़ाई या डिसप्ले डेंसिटी में बदलाव हो, तब टॉलरेंस को अपडेट करें. जैसे, शुरुआती लेआउट या स्क्रीन रोटेशन पर.

Jetpack Compose में, dp टॉलरेंस को LocalDensity की मदद से पिक्सल में बदलें. इसके बाद, Modifier.onSizeChanged की मदद से अपने कस्टम स्लाइडर को मेज़र करें. साथ ही, प्लेयर, चौड़ाई या डेंसिटी में बदलाव होने पर, scrubbingModeParameters को अपडेट करें. अगर ExoPlayer इंस्टेंस, कंपोज़ेबल से ज़्यादा समय तक बना रहता है, तो डिस्पोज़ल पर टॉलरेंस को हटा दें:

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

व्यू-आधारित यूज़र इंटरफ़ेस (यूआई) में, DisplayMetrics.density का इस्तेमाल करके dp टॉलरेंस को पिक्सल में बदलें. अगर आपका स्क्रबर व्यू या PlayerView पहले से ही लेआउट किया गया है, तो मौजूदा चौड़ाई लागू करें. साथ ही, चौड़ाई बदलने पर scrubbingModeParameters को अपडेट करने के लिए View.addOnLayoutChangeListener का इस्तेमाल करें. यूज़र इंटरफ़ेस (यूआई) को अलग करते समय, लिसनर को हटाएं. साथ ही, अगर ExoPlayer इंस्टेंस, व्यू से ज़्यादा समय तक बना रहता है, तो टॉलरेंस को हटा दें:

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-आधारित टॉलरेंस का इस्तेमाल करते समय, onScrubStop में आखिरी seekTo कॉल के ज़रिए स्क्रबिंग मोड को चालू रखने से, उपयोगकर्ता के उंगली उठाने पर विज़ुअल फ़्रेम जंप होने से बचा जा सकता है. अगर आपके ऐप्लिकेशन को किसी सटीक टाइमस्टैंप पर पहुंचने के लिए, फ़ाइनल रिलीज़ पोज़िशन की ज़रूरत है, तो अपने फ़ाइनल seekTo कॉल से पहले setScrubbingModeEnabled(false) को कॉल करें, ताकि फ़ाइनल सीक, प्लेयर के स्टैंडर्ड SeekParameters का इस्तेमाल करे.

मीडिया और नेटवर्क से जुड़ी ज़रूरी बातें

स्क्रबिंग मोड, लोकल सीक शेड्यूलिंग और हार्डवेयर डिकोडिंग को ऑप्टिमाइज़ करता है. इसलिए, यह लोकल फ़ाइलों, डाउनलोड किए गए मीडिया, कैश मेमोरी में सेव की गई स्ट्रीम या बफ़र की गई अडैप्टिव स्ट्रीम के साथ सबसे ज़्यादा असरदार होता है. इन स्ट्रीम को फ़्रीक्वेंट कीफ़्रेम (छोटे GOP) के साथ एन्कोड किया जाता है. बिना कैश मेमोरी वाले और ज़्यादा इंतज़ार के समय वाले इंटरनेट कनेक्शन पर, बफ़र न की गई पोज़िशन पर बार-बार जाने से, इंटरनेट के दोतरफ़ा यात्रा के समय पर असर पड़ता है.