Khi người dùng kéo thanh tua trên dòng thời gian để xem trước khung hình và tìm vị trí phát, giao diện người dùng sẽ gửi các lệnh gọi Player.seekTo thường xuyên liên tiếp trong thời gian ngắn. Tính năng tua tiêu chuẩn bằng Player.seekTo và ExoPlayer.setSeekParameters được thiết kế cho các bước nhảy riêng biệt, trong đó mỗi lần tua mới sẽ huỷ mọi lần tua đang diễn ra và đặt lại bộ giải mã.
Trong một cử chỉ kéo liên tục, hành vi đó có thể ngăn khung xem trước hiển thị cho đến khi người dùng ngừng di chuyển.
ExoPlayer.setScrubbingModeEnabled định cấu hình trình phát để kéo để tua tương tác, nhờ đó các khung hình xem trước video sẽ hiển thị nhanh chóng khi người dùng kéo.
Sử dụng các thành phần giao diện người dùng tạo sẵn
Các mô-đun giao diện người dùng tạo sẵn của Media3 bao gồm tính năng hỗ trợ chế độ kéo để tua tích hợp sẵn khi kết nối trực tiếp với một thực thể ExoPlayer (không được gói trong ForwardingPlayer hoặc được truy cập thông qua MediaController):
- Jetpack Compose (
media3-ui-compose-material3): Cả thành phần kết hợpPlayervàProgressSliderđộc lập đều sử dụng chế độ kéo để tua theo mặc định. Khi người dùng kéo thanh trượt,ProgressSlidersẽ gọisetScrubbingModeEnabled(true)khi bắt đầu kéo, gửi thông tin cập nhậtseekTotrong khi kéo và gọisetScrubbingModeEnabled(false)khi kết thúc thao tác kéo hoặc khi composable rời khỏi composition. Để biết thêm thông tin, hãy xem bài viết Material3 Compose. - Khung hiển thị (
media3-ui):PlayerViewvàPlayerControlViewbao gồm tính năng tích hợp chế độ kéo để tua bị tắt theo mặc định, chỉ tua một lần khi người dùng thả thanh thời gian. Bật tính năng kéo để tua liên tục trong XML bằng cách đặtapp:time_bar_scrubbing_enabled="true"trênPlayerViewhoặc trong mã bằng cách gọisetTimeBarScrubbingEnabled(true).
Ngay cả khi sử dụng ProgressSlider hoặc PlayerView, bạn vẫn có thể tuỳ chỉnh
ScrubbingModeParameters trên thực thể ExoPlayer, chẳng hạn như đặt dung sai tua phân số từ chiều rộng của thanh tua.
Trường hợp nên bật và tắt chế độ kéo để tua
Nếu bạn triển khai thanh tua hoặc chế độ điều khiển tua tuỳ chỉnh, hãy chỉ bật chế độ tua trong thời gian người dùng thực hiện cử chỉ tua tương tác:
- Bật chế độ kéo để tua (
setScrubbingModeEnabled(true)) khi người dùng bắt đầu kéo thanh trượt tua và gọiseekTokhi vị trí kéo thay đổi. - Tắt chế độ kéo để tua (
setScrubbingModeEnabled(false)) ngay khi người dùng hoàn tất hoặc huỷ cử chỉ. Ví dụ: tắt tính năng này khi thả con trỏ, khi huỷ cử chỉ hoặc nếu thành phần giao diện người dùng bị tách ra hoặc bị loại bỏ giữa chừng. - Tắt chế độ kéo để tua trong khi phát bình thường và tua một lần, chẳng hạn như nhấn nút tua đi hoặc tua lại, nhấn vào thanh tua mà không kéo hoặc tua theo lập trình.
Việc để chế độ kéo để tua hoạt động bên ngoài cử chỉ kéo sẽ gây ra các tác dụng phụ không mong muốn:
- Quá trình phát bị chặn: Khi chế độ tua nhanh được bật, ExoPlayer sẽ đặt lý do chặn phát thành
Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING, vì vậy,Player.isPlayingsẽ trả vềfalse. Khi bạn gọisetScrubbingModeEnabled(false), quá trình phát bình thường sẽ tự động tiếp tục nếuplayWhenReadylàtrue. - Các bản âm thanh không phải hình ảnh bị tắt: Các bản âm thanh và siêu dữ liệu bị tắt theo mặc định trong khi kéo để tua.
- Tăng mức sử dụng điện năng và bộ giải mã: Chế độ kéo để tua tăng tốc độ hoạt động của bộ mã hoá và giải mã video, từ đó tiêu tốn thêm tài nguyên phần cứng và pin nếu bạn bật chế độ này.
Ví dụ sau đây kết nối các lệnh gọi lại cử chỉ thanh tua tuỳ chỉnh với 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); }
Cách chế độ tua nhanh tối ưu hoá việc tua video
Với ScrubbingModeParameters.DEFAULT, ExoPlayer áp dụng một số tính năng tối ưu hoá khi chế độ kéo để tua đang hoạt động:
- Kết hợp các thao tác tua nhanh: Thay vì ưu tiên thao tác tua đang hoạt động trên mỗi lần di chuyển con trỏ, ExoPlayer sẽ hoàn tất việc giải mã và kết xuất khung hình mục tiêu hiện tại trong khi chỉ giữ lại vị trí tua gần đây nhất trong hàng đợi. Các lượt tìm kiếm trung gian bị thay thế sẽ bị loại bỏ và báo cáo cho
AnalyticsListener.onDroppedSeeksWhileScrubbing. - Tắt các bản âm thanh và siêu dữ liệu: Tạm thời tắt các bản âm thanh
C.TRACK_TYPE_AUDIOvàC.TRACK_TYPE_METADATAđể tránh tiếng nổ âm thanh và việc đặt lại trình kết xuất không cần thiết. - Tăng thông lượng bộ mã hoá và giải mã: Tăng tốc độ hoạt động của video
MediaCodecvà đảm bảo bật tính năng lập lịch động để bộ đệm đầu ra đã giải mã được xử lý nhanh hơn. - Giảm số lần đặt lại bộ giải mã: Tránh các lần xả dữ liệu
MediaCodeckhông cần thiết khi tua đến một khung hình chính trong các luồng tương thích, bỏ qua việc đặt lại về khung hình chính khi tua đi trong cùng một nhóm ảnh (GOP) và sử dụngMediaCodec.BUFFER_FLAG_DECODE_ONLYtrên Android 14 (cấp độ API 34) trở lên để bộ giải mã bỏ qua việc tạo vùng đệm đầu ra cho các khung hình trung gian giữa khung hình chính và khung hình mục tiêu.
Tuỳ chỉnh các tham số kéo để tua
Sử dụng ScrubbingModeParameters.Builder để điều chỉnh hành vi tua, chẳng hạn như loại bản nhạc nào bị tắt hoặc liệu tốc độ hoạt động của bộ mã hoá và giải mã có được tăng cường hay không. Áp dụng cấu hình bằng ExoPlayer.setScrubbingModeParameters hoặc ExoPlayer.Builder.setScrubbingModeParameters. Tạo từ exoPlayer.scrubbingModeParameters.buildUpon() (hoặc ScrubbingModeParameters.DEFAULT.buildUpon()) để các tính năng tối ưu hoá mặc định vẫn được bật.
Đặt dung sai tua phân số từ chiều rộng của thanh tua
Theo mặc định, ScrubbingModeParameters không đặt dung sai tua phân số (null), vì vậy, thao tác kéo để tua trong khi tua nhanh sẽ sử dụng SeekParameters của trình phát (SeekParameters.EXACT theo mặc định). Thao tác tua chính xác phải giải mã mọi khung hình từ khung hình chính trước đó cho đến dấu thời gian mục tiêu. Khi có nhiều khung hình kể từ khung hình chính trước đó, chẳng hạn như trong các luồng có khoảng thời gian khung hình chính rộng hoặc tốc độ khung hình cao, thì công việc giải mã này có thể làm tăng độ trễ đáng kể trong khi kéo.
Trên màn hình cảm ứng, ngón tay của người dùng không thể phân biệt chính xác chuyển động 1dp hoặc 2dp dọc theo thanh tua. Đối với thanh tua có chiều rộng widthDp, dung sai chạm nhỏ như 2dp theo một trong hai hướng (tổng cộng 4dp) tương ứng với một phần 2.0 / widthDp của thời lượng nội dung nghe nhìn. Thời lượng của phần đó phụ thuộc vào thời lượng của video. Bạn nên truyền một dung sai dựa trên dp chia cho chiều rộng của thanh tua giao diện người dùng đến setFractionalSeekTolerance (ví dụ: 2dp hoặc được điều chỉnh trong khoảng từ 1dp đến 4dp để cân bằng giữa tốc độ và độ chính xác):
- Trên nội dung dài (ví dụ: video dài 1 giờ trên thanh tua
400dp, trong đó2dpkéo dài 18 giây), ExoPlayer có thể chuyển trực tiếp đến một khung hình chính gần đó trong phạm vi dung sai chạm thay vì giải mã các khung hình trung gian. - Trên đoạn video ngắn (ví dụ: đoạn video dài 10 giây trên cùng một thanh tua
400dp, trong đó2dpkéo dài 50 mili giây), cùng một phân số sẽ tự động giữ cho thao tác tua gần với khung hình mục tiêu.
Chuyển đổi dung sai dp mục tiêu thành pixel bằng mật độ hiển thị để khớp với chiều rộng pixel đo được của thanh tua, đồng thời cập nhật dung sai bất cứ khi nào chiều rộng hoặc mật độ hiển thị của thanh tua thay đổi, chẳng hạn như khi bố cục ban đầu hoặc xoay màn hình.
Trong Jetpack Compose, hãy chuyển đổi dung sai dp thành pixel bằng LocalDensity,
đo lường thanh trượt tuỳ chỉnh bằng Modifier.onSizeChanged và cập nhật
scrubbingModeParameters khi trình phát, chiều rộng hoặc mật độ thay đổi. Nếu thực thể ExoPlayer tồn tại lâu hơn thành phần kết hợp, hãy xoá dung sai khi xử lý:
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() }
Trong giao diện người dùng dựa trên Khung hiển thị, hãy chuyển đổi dung sai dp thành pixel bằng DisplayMetrics.density, áp dụng chiều rộng hiện tại nếu khung hiển thị thanh tua hoặc PlayerView đã được bố trí và sử dụng View.addOnLayoutChangeListener để cập nhật scrubbingModeParameters khi chiều rộng thay đổi. Xoá trình nghe khi tách giao diện người dùng và xoá dung sai nếu thực thể ExoPlayer tồn tại lâu hơn khung hiển thị:
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()); }
Khi bạn sử dụng dung sai dựa trên dp, việc duy trì chế độ kéo để tua thông qua lệnh gọi seekTo cuối cùng trong onScrubStop sẽ tránh được hiện tượng nhảy khung hình khi người dùng nhấc ngón tay lên. Nếu ứng dụng của bạn yêu cầu vị trí phát hành cuối cùng phải nằm ở một dấu thời gian chính xác, hãy gọi setScrubbingModeEnabled(false) trước lệnh gọi seekTo cuối cùng để thao tác tua cuối cùng sử dụng SeekParameters tiêu chuẩn của trình phát.
Những điểm cần cân nhắc về nội dung nghe nhìn và mạng
Chế độ kéo để tua tối ưu hoá việc lập lịch tìm kiếm cục bộ và giải mã phần cứng, vì vậy, chế độ này hiệu quả nhất với các tệp cục bộ, nội dung nghe nhìn đã tải xuống, luồng đã lưu vào bộ nhớ đệm hoặc luồng thích ứng được lưu vào vùng đệm được mã hoá bằng các khung hình chính thường xuyên (GOP ngắn). Đối với các kết nối mạng có độ trễ cao, không được lưu vào bộ nhớ đệm, thì việc thường xuyên tua đến các vị trí không được lưu vào vùng đệm vẫn bị ràng buộc bởi thời gian khứ hồi của mạng.