حالت پاک‌سازی

وقتی کاربر انگشت شست نوار کاوش را روی خط زمان می‌کشد تا فریم‌ها را پیش‌نمایش کند و موقعیت بازپخش را پیدا کند، میانای کاربر تماس‌های مکرر Player.seekTo را در توالی‌های کوتاه ارسال می‌کند. جستجوی استاندارد با Player.seekTo و ExoPlayer.setSeekParameters برای پرش‌های مجزا طراحی شده است، که در آن هر جستجوی جدید هر جستجوی درحال انجام را لغو می‌کند و رمزگشا را بازنشانی می‌کند. درطول اشاره کشیدن پیوسته، این رفتار می‌تواند از رندر شدن فریم‌های پیش‌نمایش تا زمانی که کاربر حرکت را متوقف کند جلوگیری کند.

ExoPlayer.setScrubbingModeEnabled پخش‌کننده را برای پاک‌سازی تعاملی پیکربندی می‌کند تا قاب‌های پیش‌نمایش ویدیو به‌طور واکنشی هنگام کشیدن کاربر رندر شوند.

استفاده از مؤلفه‌های واسط کاربر ازپیش‌ساخته

Media3's prebuilt UI modules include built-in scrubbing mode support when connected directly to an ExoPlayer instance (not wrapped in a ForwardingPlayer or accessed through a MediaController):

  • Jetpack Compose (media3-ui-compose-material3): هر دو Player composable و مستقل ProgressSlider به‌طور پیش‌فرض از حالت پیمایش سریع استفاده می‌کنند. وقتی کاربر زبانه لغزنده را می‌کشد، ProgressSlider در شروع کشیدن setScrubbingModeEnabled(true) را فراخوانی می‌کند، seekTo را درطول کشیدن ارسال می‌کند، و وقتی کشیدن تمام می‌شود یا ترکیب‌شدنی ترکیب‌بندی را ترک می‌کند، setScrubbingModeEnabled(false) را فراخوانی می‌کند. برای اطلاعات بیشتر، Material3 Compose را ببینید.
  • نماها (media3-ui): PlayerView و PlayerControlView شامل ادغام حالت پاک‌سازی است که به‌طور پیش‌فرض غیرفعال است، و فقط یک‌بار وقتی کاربر نوار زمان را رها می‌کند، جستجو می‌کند. با تنظیم کردن app:time_bar_scrubbing_enabled="true" روی PlayerView در XML، یا با فراخوانی setTimeBarScrubbingEnabled(true) در کد، پاک‌سازی مداوم را در XML فعال کنید.

حتی وقتی از ProgressSlider یا PlayerView استفاده می‌کنید، می‌توانید ScrubbingModeParameters را در نمونه ExoPlayer سفارشی‌سازی کنید، مثلاً تنظیم کردن تلورانس جستجوی کسری از عرض پاک‌کننده.

چه زمانی حالت پاک‌سازی را روشن و خاموش کنید

اگر نوار کاوش یا کنترل لغزنده سفارشی پیاده‌سازی می‌کنید، حالت لغزاندن را فقط برای مدت اشاره لغزاندن تعاملی کاربر روشن کنید:

  • وقتی کاربر شروع به کشیدن دکمه لغزنده می‌کند، حالت لغزاندن را روشن کنید (setScrubbingModeEnabled(true)) و با تغییر موقعیت کشیدن، seekTo را فراخوانی کنید.
  • حالت پاک‌سازی را خاموش کنید (setScrubbingModeEnabled(false)) به‌محض اینکه کاربر اشاره را تمام یا لغو کرد. برای مثال، آن را در رها کردن اشاره‌گر، در لغو اشاره، یا اگر مؤلفه واسط کاربر شما در اواسط کشیدن جدا یا دور انداخته شده است، خاموش کنید.
  • حالت پیمایش را درطول بازپخش عادی و کاوش‌های تکی خاموش نگه دارید، مثلاً فشردن دکمه‌های جلوبر سریع یا عقب‌بر، تک‌ضرب زدن روی نوار کاوش بدون کشیدن، یا کاوش به‌صورت برنامه‌نویسی‌شده.

فعال نگه داشتن حالت پیمایش سریع خارج از اشاره کشیدن، عوارض جانبی ناخواسته‌ای دارد:

  • بازپخش متوقف می‌شود: وقتی حالت پیمایش فعال است، ExoPlayer دلیل توقف بازپخش را روی Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING تنظیم می‌کند، بنابراین Player.isPlaying مقدار false را برمی‌گرداند. وقتی setScrubbingModeEnabled(false) را فراخوانی می‌کنید، اگر playWhenReady برابر با true باشد، بازپخش عادی به‌طور خودکار ازسر گرفته می‌شود.
  • قطعه‌های غیردیداری غیرفعال هستند: قطعه‌های صوتی و فراداده به‌طور پیش‌فرض درحین پیمایش غیرفعال هستند.
  • افزایش مصرف برق و رمزگشا: حالت «مرور سریع» نرخ عملکرد کدک ویدیو را افزایش می‌دهد که درصورت فعال ماندن، منابع سخت‌افزاری و باتری بیشتری مصرف می‌کند.

مثال زیر فراخوانی‌های برگشت اشاره پاک‌کننده سفارشی را به ExoPlayer متصل می‌کند:

کاتلین

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) صرف‌نظر می‌کند، و از MediaCodec.BUFFER_FLAG_DECODE_ONLY در Android 14 (سطح API‏ ۳۴) و بالاتر استفاده می‌کند تا رمزگشا از تولید میان‌گیرهای خروجی برای قاب‌های میانی بین قاب کلیدی و قاب هدف صرف‌نظر کند.

سفارشی‌سازی پارامترهای پاک‌سازی

از 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 ۱۸ ثانیه را پوشش می‌دهد)، ExoPlayer می‌تواند مستقیماً به یک فریم کلیدی نزدیک در آن محدوده لمس بپرد، به‌جای اینکه فریم‌های میانی را رمزگشایی کند.
  • در کلیپ‌های کوتاه (برای مثال، کلیپ ۱۰ ثانیه‌ای در همان 400dp اسکرابر، که 2dp ۵۰ میلی‌ثانیه طول می‌کشد)، همان کسر به‌طور خودکار جستجوها را نزدیک به قاب هدف نگه می‌دارد.

بااستفاده از تراکم نمایش، تلورانس dp هدف را به پیکسل تبدیل کنید تا با پهنای پیکسل اندازه‌گیری‌شده لغزنده مطابقت داشته باشد و هرگاه پهنای لغزنده یا تراکم نمایش تغییر کرد، مثلاً در چیدمان اولیه یا چرخش صفحه‌نمایش، تلورانس را به‌روز کنید.

در Jetpack Compose، تلورانس dp را با LocalDensity به پیکسل تبدیل کنید، لغزنده سفارشی‌تان را با Modifier.onSizeChanged اندازه بگیرید، و scrubbingModeParameters را هنگام تغییر پخش‌کننده، عرض، یا تراکم به‌روز کنید. اگر نمونه ExoPlayer بیشتر از composable دوام آورد، تلورانس را در disposal پاک کنید:

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

در واسط کاربر مبتنی بر «نما»، تلورانس dp را بااستفاده از DisplayMetrics.density به پیکسل تبدیل کنید، اگر نمای لغزنده یا PlayerView شما ازقبل چیده شده است، عرض فعلی را اعمال کنید، و از View.addOnLayoutChangeListener برای به‌روزرسانی scrubbingModeParameters هنگام تغییر عرض آن استفاده کنید. Remove the listener when detaching the UI, and clear the tolerance if the ExoPlayer instance outlives the view:

کاتلین

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 استفاده می‌کنید، فعال نگه داشتن حالت پیمایش درطول آخرین فراخوانی seekTo در onScrubStop از پرش قاب بصری هنگام برداشتن انگشت کاربر جلوگیری می‌کند. اگر برنامه شما به‌جای آن نیاز دارد که موقعیت انتشار نهایی روی مُهر زمان دقیقی قرار بگیرد، setScrubbingModeEnabled(false) را قبل‌از تماس نهایی seekTo فراخوانی کنید تا جستجوی نهایی از SeekParameters استاندارد پخش‌کننده استفاده کند.

ملاحظات رسانه و شبکه

حالت «مرور سریع» زمان‌بندی جستجوی محلی و رمزگشایی سخت‌افزاری را بهینه می‌کند، بنابراین بیشترین تأثیر را بر فایل‌های محلی، رسانه‌های بارگیری‌شده، جاری‌سازی‌های ذخیره‌شده در حافظه نهان، یا جاری‌سازی‌های تطبیقی میان‌گیرشده که با قاب‌های کلیدی مکرر (GOPهای کوتاه) رمزگذاری شده‌اند دارد. در اتصالات شبکه با تأخیر زیاد و بدون حافظه پنهان، جستجوهای مکرر برای موقعیت‌های میان‌گیرنشده همچنان محدود به زمان رفت‌وبرگشت شبکه است.