وقتی کاربر انگشت شست نوار کاوش را روی خط زمان میکشد تا فریمها را پیشنمایش کند و
موقعیت بازپخش را پیدا کند، میانای کاربر تماسهای مکرر 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): هر دوPlayercomposable و مستقل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های کوتاه) رمزگذاری شدهاند دارد. در اتصالات شبکه با تأخیر زیاد و بدون حافظه پنهان، جستجوهای مکرر برای موقعیتهای میانگیرنشده همچنان محدود به زمان رفتوبرگشت شبکه است.