স্ক্রাবিং মোড

ফ্রেমের প্রিভিউ দেখতে এবং প্লেব্যাক পজিশন খুঁজতে, ব্যবহারকারী টাইমলাইন জুড়ে সিক বার থাম্ব টেনে নিয়ে গেলে, UI ঘন ঘন Player.seekTo কল পাঠায়। Player.seekTo এবং ExoPlayer.setSeekParameters-এর সাথে স্ট্যান্ডার্ড সিকিং আইসোলেটেড জাম্পের জন্য ডিজাইন করা হয়েছে, যেখানে প্রতিটি নতুন সিক যেকোনও ইন-ফ্লাইট সিক বাতিল করে এবং ডিকোডার রিসেট করে। একটানা টেনে নিয়ে যাওয়ার জেসচার চলাকালীন, ব্যবহারকারী টেনে নিয়ে যাওয়া বন্ধ না করা পর্যন্ত এই আচরণ প্রিভিউ ফ্রেম রেন্ডার করা থেকে আটকাতে পারে।

ExoPlayer.setScrubbingModeEnabled ইন্টার‍্যাক্টিভ স্ক্রাবিংয়ের জন্য প্লেয়ার কনফিগার করে যাতে ব্যবহারকারী টেনে নিয়ে যাওয়ার সময় ভিডিও প্রিভিউ ফ্রেম রেসপন্সিভভাবে রেন্ডার করে।

আগে থেকে তৈরি UI কম্পোনেন্ট ব্যবহার করুন

Media3-এর আগে থেকে তৈরি UI মডিউলে বিল্ট-ইন স্ক্রাবিং মোড সাপোর্ট অন্তর্ভুক্ত থাকে, যখন সরাসরি একটি ExoPlayer ইনস্ট্যান্সের সাথে কানেক্ট করা হয় (একটি ForwardingPlayer-এ মোড়ানো হয় না বা একটি MediaController-এর মাধ্যমে অ্যাক্সেস করা হয়):

  • Jetpack Compose (media3-ui-compose-material3): Player কম্পোজেবল ও স্ট্যান্ডঅ্যালোন ProgressSlider উভয়ই ডিফল্ট হিসেবে স্ক্রাবিং মোড ব্যবহার করে। ব্যবহারকারী স্লাইডার থাম্ব ড্র্যাগ করলে, ড্র্যাগ শুরু হওয়ার সময় ProgressSlider কল setScrubbingModeEnabled(true), ড্র্যাগ করার সময় seekTo আপডেট ডিসপ্যাচ করে এবং ড্র্যাগ শেষ হলে বা কম্পোজেবল কম্পোজিশন ছেড়ে দিলে setScrubbingModeEnabled(false) কল করে। আরও তথ্যের জন্য, Material3 Compose দেখুন।
  • ভিউ (media3-ui): PlayerView এবং PlayerControlView-এ স্ক্রাবিং মোড ইন্টিগ্রেশন অন্তর্ভুক্ত থাকে যা ডিফল্ট হিসেবে বন্ধ করা থাকে, ব্যবহারকারী টাইম বার রিলিজ করলে শুধুমাত্র একবারই খোঁজা হয়। XML-এ ক্রমাগত স্ক্রাবিং চালু করতে app:time_bar_scrubbing_enabled="true"-কে PlayerView-এ সেট করুন অথবা কোডে setTimeBarScrubbingEnabled(true) কল করুন।

এমনকি আপনি ProgressSlider বা PlayerView ব্যবহার করলেও, ExoPlayer ইনস্ট্যান্সে ScrubbingModeParameters কাস্টমাইজ করতে পারবেন, যেমন স্ক্রাবার প্রস্থ থেকে ফ্র্যাকশনাল সিক টলারেন্স সেট করা।

স্ক্রাবিং মোড কখন চালু এবং বন্ধ করতে হবে

আপনি কাস্টম সিক বার বা স্ক্রাবার কন্ট্রোল প্রয়োগ করলে, শুধুমাত্র ইন্টার‍্যাক্টিভ ব্যবহারকারীর স্ক্রাবিং জেসচারের সময়সীমার জন্য স্ক্রাবিং মোড চালু করুন:

  • ব্যবহারকারী স্ক্রাবার থাম্ব টেনে আনা শুরু করলে, স্ক্রাবিং মোড চালু করুন (setScrubbingModeEnabled(true)) এবং টেনে আনার পজিশন পরিবর্তন হলে seekTo কল করুন।
  • ব্যবহারকারী জেসচার শেষ বা বাতিল করার সাথে সাথেই স্ক্রাবিং মোড বন্ধ করুন (setScrubbingModeEnabled(false))। যেমন, পয়েন্টার রিলিজ, জেসচার বাতিল করার সময় অথবা ড্র্যাগ করার সময় UI কম্পোনেন্ট আলাদা বা ডিসপোজ হয়ে গেলে এটি বন্ধ করে দিন।
  • সাধারণ প্লেব্যাক এবং একবার সিক করার সময় স্ক্রাবিং মোড বন্ধ রাখুন, যেমন ফাস্ট-ফরওয়ার্ড বা রিওয়াইন্ড বোতাম প্রেস করা, টেনে না এনে সিক বারে ট্যাপ করা অথবা প্রোগ্রাম্যাটিকভাবে সিক করা।

ড্র্যাগ জেসচারের বাইরে স্ক্রাবিং মোড চালু রাখলে অনিচ্ছাকৃত পার্শ্বপ্রতিক্রিয়া হতে পারে:

  • প্লেব্যাক সাপ্রেস করা হয়েছে: স্ক্রাবিং মোড চালু থাকাকালীন, 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)
}

জাভা

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 বর্তমান টার্গেট ফ্রেম ডিকোড ও রেন্ডার করার প্রসেস সম্পূর্ণ করে, তবে শুধুমাত্র সবচেয়ে সাম্প্রতিক খোঁজার পজিশন সারিতে রাখে। Superseded intermediate seeks are dropped and reported to AnalyticsListener.onDroppedSeeksWhileScrubbing.
  • অডিও এবং মেটাডেটা ট্র্যাক বন্ধ করে: অডিও পপ এবং অপ্রয়োজনীয় রেন্ডারার রিসেট এড়াতে সাময়িকভাবে C.TRACK_TYPE_AUDIO এবং C.TRACK_TYPE_METADATA ট্র্যাক বন্ধ করে।
  • কোডেক থ্রুপুট বাড়ায়: ভিডিও MediaCodec অপারেটিং রেট বাড়ায় এবং ডায়নামিক শিডিউলিং চালু আছে কিনা তা নিশ্চিত করে যাতে ডিকোড করা আউটপুট বাফার আরও দ্রুত প্রসেস করা যায়।
  • ডিকোডার রিসেট করার সংখ্যা কমায়: মানানসই স্ট্রিমে কোনও কীফ্রেম খোঁজার সময় অপ্রয়োজনীয় MediaCodec ফ্লাশ করা এড়ায়, একই গ্রুপ অফ পিকচার (GOP)-এর মধ্যে সামনের দিকে খোঁজার সময় কীফ্রেমে ফিরে গিয়ে রিসেট করা এড়ায় এবং Android 14 (API লেভেল 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-ভিত্তিক টলারেন্সকে আপনার UI স্ক্রাবারের প্রস্থ দিয়ে ভাগ করে setFractionalSeekTolerance-এ পাস করুন (যেমন, 2dp, অথবা স্পিড ও নির্ভুলতার মধ্যে ভারসাম্য বজায় রাখতে 1dp ও 4dp-এর মধ্যে টিউন করা):

  • দীর্ঘ কন্টেন্টের (যেমন, 400dp স্ক্রাবার সহ ১ ঘণ্টার ভিডিও, যেখানে 2dp ১৮ সেকেন্ডের জন্য থাকে) ক্ষেত্রে, ExoPlayer মধ্যবর্তী ফ্রেম ডিকোড করার পরিবর্তে সেই টাচ টলারেন্সের মধ্যে সরাসরি কাছাকাছি কোনও কীফ্রেমে স্ন্যাপ করতে পারে।
  • ছোট ক্লিপের (যেমন, একই 400dp স্ক্রাবারে ১০ সেকেন্ডের ক্লিপ, যেখানে 2dp ৫০ মিলিসেকেন্ডের জন্য থাকে) ক্ষেত্রে, একই ভগ্নাংশ অটোমেটিক টার্গেট ফ্রেমের কাছাকাছি সিক করে।

ডিসপ্লে ডেনসিটি ব্যবহার করে আপনার টার্গেট 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()
}

ভিউ-ভিত্তিক UI-তে, DisplayMetrics.density ব্যবহার করে dp টলারেন্সকে পিক্সেলে কনভার্ট করুন, আপনার স্ক্রাবার ভিউ বা PlayerView আগে থেকেই লে-আউট করা থাকলে বর্তমান প্রস্থ প্রয়োগ করুন এবং scrubbingModeParameters-এর প্রস্থ পরিবর্তন হলে তা আপডেট করতে View.addOnLayoutChangeListener ব্যবহার করুন। UI বিচ্ছিন্ন করার সময় লিসেনার সরিয়ে দিন এবং 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()
}

জাভা

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) সহ এনকোড করা বাফার করা অ্যাডাপ্টিভ স্ট্রিমের ক্ষেত্রে সবচেয়ে কার্যকর। আনক্যাশড, হাই-লেটেন্সি নেটওয়ার্ক কানেকশনে, আনবাফার্ড পজিশনে ঘন ঘন সিক করা নেটওয়ার্ক রাউন্ড-ট্রিপ টাইমের দ্বারা সীমাবদ্ধ থাকে।