Tin tức về sản phẩm

Nâng cao trải nghiệm phát nội dung nghe nhìn: Tìm hiểu sâu về PreloadManager của Media3 – Phần 2

9 phút đọc
Xem hồ sơ của Mayuri Khinvasara Khabya
Mayuri Khinvasara Khabya Kỹ sư quan hệ nhà phát triển

Chào mừng bạn đến với phần thứ hai trong loạt bài gồm ba phần về việc tải trước nội dung nghe nhìn bằng Media3. Loạt bài này được thiết kế để hướng dẫn bạn quy trình xây dựng trải nghiệm nghe nhìn có độ trễ thấp và độ thích ứng cao trong ứng dụng Android.

  • Phần 1: Giới thiệu về tính năng tải trước bằng Media3 đã đề cập đến các nguyên tắc cơ bản. Chúng ta đã khám phá sự khác biệt giữa PreloadConfiguration cho danh sách phát đơn giản và DefaultPreloadManager mạnh mẽ hơn cho giao diện người dùng động. Bạn đã tìm hiểu cách triển khai vòng đời API cơ bản: thêm nội dung nghe nhìn bằng add(), truy xuất MediaSource đã chuẩn bị bằng getMediaSource(), quản lý mức độ ưu tiên bằng setCurrentPlayingIndex() và invalidate(), đồng thời giải phóng tài nguyên bằng remove() và release().
  • Phần 2 (Bài đăng này): Trong blog này, chúng ta sẽ tìm hiểu các chức năng nâng cao của DefaultPreloadManager. Chúng ta sẽ tìm hiểu cách thu thập thông tin chi tiết bằng PreloadManagerListener, triển khai các phương pháp hay nhất sẵn sàng cho môi trường thực tế (chẳng hạn như chia sẻ các thành phần cốt lõi với ExoPlayer) và nắm vững mẫu cửa sổ trượt để quản lý bộ nhớ một cách hiệu quả.
  • Phần 3: Phần cuối của loạt bài này sẽ đi sâu vào việc tích hợp PreloadManager với bộ nhớ đệm của ổ đĩa lưu trữ dài lâu, giúp bạn giảm mức sử dụng dữ liệu bằng tính năng quản lý tài nguyên và mang lại trải nghiệm liền mạch.

Nếu bạn mới làm quen với tính năng tải trước trong Media3, bạn nên đọc Phần 1 trước khi tiếp tục. Đối với những người đã sẵn sàng vượt qua những kiến thức cơ bản, hãy cùng khám phá cách nâng cao khả năng triển khai tính năng phát nội dung nghe nhìn.

Lắng nghe: Tìm nạp số liệu phân tích bằng PreloadManagerListener

Khi muốn ra mắt một tính năng trong phiên bản chính thức, nhà phát triển ứng dụng cũng muốn tìm hiểu và nắm bắt số liệu phân tích đằng sau tính năng đó. Làm cách nào để chắc chắn rằng chiến lược tải trước của bạn có hiệu quả trong môi trường thực tế? Để trả lời câu hỏi này, bạn cần có dữ liệu về tỷ lệ thành công, tỷ lệ thất bại và hiệu suất. Giao diện PreloadManagerListener là cơ chế chính để thu thập dữ liệu này.

PreloadManagerListener cung cấp hai lệnh gọi lại thiết yếu, cung cấp thông tin chi tiết quan trọng về quy trình và trạng thái tải trước.

  • onCompleted(MediaItem mediaItem): Lệnh gọi lại này được gọi khi yêu cầu tải trước hoàn tất thành công, theo định nghĩa của TargetPreloadStatusControl.
  • onError(PreloadException error): Lệnh gọi lại này có thể hữu ích cho việc gỡ lỗi và giám sát. Hàm này được gọi khi không tải trước được, cung cấp ngoại lệ liên quan.

Bạn có thể đăng ký trình nghe bằng một lệnh gọi phương thức duy nhất như trong mã ví dụ sau:

val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

Trích xuất thông tin chi tiết từ người nghe 

Bạn có thể kết nối các lệnh gọi lại của trình nghe này với quy trình phân tích. Bằng cách chuyển tiếp các sự kiện này đến công cụ phân tích, bạn có thể trả lời các câu hỏi quan trọng như:

  • Tỷ lệ tải trước thành công của chúng tôi là bao nhiêu? (tỷ lệ giữa số sự kiện onCompleted và tổng số lần tải trước)
  • CDN hoặc định dạng video nào có tỷ lệ lỗi cao nhất? (Bằng cách phân tích cú pháp các ngoại lệ từ onError)
  • Tỷ lệ lỗi tải trước của chúng ta là bao nhiêu? (tỷ lệ giữa số sự kiện onError và tổng số lần thử tải trước)

Dữ liệu này có thể cung cấp cho bạn ý kiến phản hồi định lượng về chiến lược tải trước, cho phép thử nghiệm A/B và cải thiện trải nghiệm người dùng dựa trên dữ liệu. Dữ liệu này có thể giúp bạn tinh chỉnh một cách thông minh thời lượng tải trước và số lượng video bạn muốn tải trước cũng như vùng đệm mà bạn phân bổ.

Ngoài việc gỡ lỗi: Sử dụng onError để dự phòng giao diện người dùng một cách linh hoạt

Tải trước không thành công là dấu hiệu rõ ràng cho thấy người dùng sắp gặp phải sự kiện tải vào vùng đệm. Lệnh gọi lại onError cho phép bạn phản hồi một cách linh hoạt. Thay vì chỉ ghi lại lỗi, bạn có thể điều chỉnh giao diện người dùng. Ví dụ: nếu video sắp tới không tải trước được, thì ứng dụng của bạn có thể tắt tính năng tự động phát cho lần vuốt tiếp theo, yêu cầu người dùng nhấn để bắt đầu phát.

Ngoài ra, bằng cách kiểm tra loại PreloadException, bạn có thể xác định chiến lược thử lại thông minh hơn. Ứng dụng có thể chọn xoá ngay một nguồn không thành công khỏi trình quản lý dựa trên thông báo lỗi hoặc mã trạng thái HTTP. Mục này cần được xoá khỏi luồng giao diện người dùng để không làm rò rỉ các vấn đề về tải vào trải nghiệm người dùng. Bạn cũng có thể lấy dữ liệu chi tiết hơn từ PreloadException như HttpDataSourceException để tìm hiểu thêm về các lỗi. Đọc thêm về cách khắc phục sự cố ExoPlayer.

Hệ thống đồng hành: Tại sao cần chia sẻ các thành phần với ExoPlayer?

DefaultPreloadManager và ExoPlayer được thiết kế để hoạt động cùng nhau. Để đảm bảo tính ổn định và hiệu quả, các ứng dụng này phải dùng chung một số thành phần cốt lõi. Nếu các thành phần này hoạt động riêng biệt và không phối hợp với nhau, thì điều này có thể ảnh hưởng đến độ an toàn của luồng và khả năng hữu dụng của các bản nhạc được tải trước trên trình phát, vì chúng ta cần đảm bảo rằng các bản nhạc được tải trước sẽ phát trên đúng trình phát. Các thành phần riêng biệt cũng có thể cạnh tranh để giành lấy tài nguyên hạn chế như băng thông mạng và bộ nhớ, điều này có thể dẫn đến suy giảm hiệu suất. Một phần quan trọng trong vòng đời là xử lý việc loại bỏ thích hợp, thứ tự loại bỏ được đề xuất là giải phóng PreloadManager trước, sau đó là ExoPlayer.

DefaultPreloadManager.Builder được thiết kế để hỗ trợ việc chia sẻ này và có các API để tạo thực thể cho cả PreloadManager và một thực thể trình phát được liên kết. Hãy cùng tìm hiểu lý do tại sao các thành phần như BandwidthMeter, LoadControl, TrackSelector, Looper phải được chia sẻ. Hãy xem hình ảnh minh hoạ về cách các thành phần này tương tác với tính năng Phát của ExoPlayer.

preloadManager2.png

Ngăn chặn xung đột băng thông bằng BandwidthMeter dùng chung

BandwidthMeter cung cấp thông tin ước tính về băng thông mạng hiện có dựa trên tốc độ truyền dữ liệu trước đây. Nếu PreloadManager và trình phát sử dụng các thực thể riêng biệt, thì chúng sẽ không biết về hoạt động mạng của nhau, điều này có thể dẫn đến các trường hợp không thành công. Ví dụ: hãy xem xét trường hợp người dùng đang xem video, kết nối mạng của họ bị suy giảm và MediaSource tải trước đồng thời bắt đầu tải xuống mạnh mẽ cho một video trong tương lai. Hoạt động của MediaSource tải trước sẽ tiêu tốn băng thông mà trình phát đang hoạt động cần, khiến video hiện tại bị gián đoạn. Tình trạng bị treo trong khi phát là một lỗi nghiêm trọng về trải nghiệm người dùng.

Bằng cách chia sẻ một BandwidthMeter, TrackSelector có thể chọn các bản nhạc có chất lượng cao nhất dựa trên điều kiện mạng hiện tại và trạng thái của vùng đệm, trong quá trình tải trước hoặc phát. Sau đó, ứng dụng có thể đưa ra quyết định thông minh để bảo vệ phiên phát đang hoạt động và đảm bảo trải nghiệm mượt mà.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Đảm bảo tính nhất quán với các thành phần LoadControl, TrackSelector, Trình kết xuất dùng chung của ExoPlayer

  • LoadControl: Thành phần này quy định chính sách lưu vào vùng đệm, chẳng hạn như lượng dữ liệu cần lưu vào vùng đệm trước khi bắt đầu phát và thời điểm bắt đầu hoặc dừng tải thêm dữ liệu. Việc chia sẻ LoadControl đảm bảo rằng mức tiêu thụ bộ nhớ của trình phát và PreloadManager được hướng dẫn bằng một chiến lược lưu vào vùng đệm duy nhất, phối hợp trên cả nội dung nghe nhìn được tải trước và đang phát, ngăn chặn xung đột tài nguyên. Bạn sẽ phải phân bổ dung lượng bộ nhớ đệm một cách thông minh, phối hợp với số lượng mục bạn đang tải trước và thời lượng để đảm bảo tính nhất quán. Trong thời gian tranh chấp, trình phát sẽ ưu tiên phát mục hiện đang hiển thị trên màn hình. Với LoadControl dùng chung, trình quản lý tải trước sẽ tiếp tục tải trước miễn là số byte vùng đệm mục tiêu được phân bổ cho việc tải trước chưa đạt đến giới hạn trên, trình quản lý này không đợi cho đến khi quá trình tải để phát hoàn tất.

Lưu ý: Việc chia sẻ LoadControl trong phiên bản mới nhất của Media3 (1.8) đảm bảo rằng Allocator của LoadControl có thể được chia sẻ chính xác với PreloadManager và trình phát. Sử dụng LoadControl để kiểm soát hiệu quả việc tải trước là một tính năng sẽ có trong bản phát hành Media3 1.9 sắp tới.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: Thành phần này chịu trách nhiệm chọn bản nhạc (ví dụ: video có độ phân giải nhất định, âm thanh bằng một ngôn ngữ cụ thể) để tải và phát. Việc chia sẻ đảm bảo rằng các bản nhạc được chọn trong quá trình tải trước cũng chính là các bản nhạc mà trình phát sẽ sử dụng. Điều này giúp tránh trường hợp lãng phí khi một bản video 480p được tải trước, chỉ để trình phát loại bỏ ngay lập tức và tìm nạp bản 720p khi phát.< br /> Trình quản lý tải trước KHÔNG được chia sẻ cùng một thực thể của TrackSelector với trình phát. Thay vào đó, họ nên sử dụng thực thể TrackSelector khác nhưng có cùng cách triển khai. Đó là lý do chúng ta đặt TrackSelectorFactory thay vì TrackSelector trong DefaultPreloadManager.Builder.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Trình kết xuất: Thành phần này chịu trách nhiệm tìm hiểu các chức năng của trình phát mà không cần tạo trình kết xuất đầy đủ. Trình phát sẽ kiểm tra bản thiết kế này để xem định dạng video, âm thanh và văn bản nào sẽ được hỗ trợ. Nhờ vậy, trình phát có thể chọn và chỉ tải xuống bản âm thanh tương thích một cách thông minh, đồng thời tránh lãng phí băng thông cho nội dung mà trình phát không thể phát.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Tìm hiểu thêm về các thành phần của ExoPlayer.

Quy tắc vàng: Một Playback Looper chung để quản lý tất cả

Bạn có thể chỉ định rõ luồng mà một thực thể ExoPlayer có thể truy cập bằng cách truyền một Looper khi tạo trình phát. Bạn có thể truy vấn Looper của luồng mà người chơi phải truy cập bằng cách sử dụng Player.getApplicationLooper. Bằng cách duy trì một Looper dùng chung giữa trình phát và PreloadManager, tất cả các thao tác trên các đối tượng nội dung nghe nhìn dùng chung này đều được chuyển đổi tuần tự vào hàng đợi thông báo của một luồng. Điều này có thể làm giảm lỗi đồng thời.

Tất cả hoạt động tương tác giữa PreloadManager và trình phát với các nguồn nội dung nghe nhìn cần tải hoặc tải trước đều phải diễn ra trên cùng một luồng phát. Bạn bắt buộc phải chia sẻ Looper để đảm bảo an toàn cho luồng, do đó, chúng ta phải chia sẻ PlaybackLooper giữa PreloadManager và trình phát.

PreloadManager chuẩn bị một đối tượng MediaSource có trạng thái ở chế độ nền. Khi mã giao diện người dùng gọi player.setMediaSource(mediaSource), bạn đang thực hiện chuyển giao đối tượng có trạng thái phức tạp này từ MediaSource tải trước sang trình phát. Trong trường hợp này, toàn bộ PreloadMediaSource sẽ được chuyển từ trình quản lý sang trình phát. Tất cả các hoạt động tương tác và chuyển giao này phải diễn ra trên cùng một PlaybackLooper.

Nếu PreloadManager và ExoPlayer hoạt động trên các luồng khác nhau, thì tình huống tương tranh có thể xảy ra. Luồng của PreloadManager có thể đang sửa đổi trạng thái nội bộ của MediaSource (ví dụ: ghi dữ liệu mới vào vùng đệm) tại thời điểm chính xác mà luồng của trình phát đang cố gắng đọc từ đó. Điều này dẫn đến hành vi không thể đoán trước, IllegalStateException khó gỡ lỗi.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Hãy xem cách bạn có thể chia sẻ tất cả các thành phần nêu trên giữa ExoPlayer và DefaultPreloadManager trong chính quá trình thiết lập.

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

Lưu ý: Nếu sử dụng các thành phần Mặc định trong ExoPlayer như DefaultLoadControl, v.v., bạn không cần phải chia sẻ rõ ràng các thành phần đó với DefaultPreloadManager. Khi bạn tạo thực thể ExoPlayer thông qua buildExoPlayer của DefaultPreloadManager.Builder, các thành phần này sẽ tự động được tham chiếu với nhau nếu bạn sử dụng các phương thức triển khai mặc định với cấu hình mặc định. Tuy nhiên, nếu sử dụng các thành phần tuỳ chỉnh hoặc cấu hình tuỳ chỉnh, bạn nên thông báo rõ ràng cho DefaultPreloadManager về các thành phần hoặc cấu hình đó thông qua các API nêu trên.

Tải trước sẵn sàng cho môi trường thực tế: Mẫu cửa sổ trượt

Trong một nguồn cấp dữ liệu động, người dùng có thể cuộn qua một lượng nội dung gần như vô hạn. Nếu liên tục thêm video vào DefaultPreloadManager mà không có chiến lược xoá tương ứng, bạn chắc chắn sẽ gây ra OutOfMemoryError. Mỗi MediaSource được tải trước sẽ giữ một SampleQueue, giúp phân bổ vùng đệm bộ nhớ. Khi các đối tượng này tích luỹ, chúng có thể làm cạn kiệt không gian vùng nhớ khối xếp của ứng dụng. Giải pháp là một thuật toán mà bạn có thể đã quen thuộc, được gọi là cửa sổ trượt. Mẫu cửa sổ trượt duy trì một tập hợp nhỏ các mục có thể quản lý trong bộ nhớ, nằm liền kề một cách logic với vị trí hiện tại của người dùng trong nguồn cấp dữ liệu. Khi người dùng cuộn, "cửa sổ" của các mục được quản lý này sẽ trượt theo, thêm các mục mới xuất hiện và cũng xoá các mục hiện đã ở xa.

slidingwindow.png

Triển khai mẫu cửa sổ trượt

Bạn cần hiểu rằng PreloadManager không cung cấp phương thức setWindowSize() tích hợp sẵn. Cửa sổ trượt là một mẫu thiết kế mà bạn (nhà phát triển) có trách nhiệm triển khai bằng cách sử dụng các phương thức add() và remove() gốc. Logic ứng dụng của bạn phải kết nối các sự kiện trên giao diện người dùng (chẳng hạn như thao tác cuộn hoặc thay đổi trang) với các lệnh gọi API này. Nếu bạn muốn tham khảo mã cho việc này, chúng tôi đã triển khai mẫu cửa sổ trượt này trong mẫu socialite, trong đó cũng có PreloadManagerWrapper mô phỏng cửa sổ trượt.

Đừng quên thêm preloadManager.remove(mediaItem) vào quá trình triển khai khi mục đó không còn khả năng xuất hiện sớm trong chế độ xem của người dùng. Việc không xoá được các mục không còn gần với người dùng là nguyên nhân chính gây ra các vấn đề về bộ nhớ trong quá trình triển khai tải trước. Lệnh gọi remove() đảm bảo các tài nguyên được giải phóng, giúp bạn duy trì mức sử dụng bộ nhớ của ứng dụng ở mức ổn định và có giới hạn.

Tinh chỉnh chiến lược tải trước được phân loại bằng TargetPreloadStatusControl

Bây giờ, sau khi đã xác định nội dung cần tải trước (các mục trong cửa sổ), chúng ta có thể áp dụng một chiến lược được xác định rõ ràng về lượng nội dung cần tải trước cho mỗi mục. Chúng ta đã biết cách đạt được mức độ chi tiết này bằng cách thiết lập TargetPreloadStatusControl trong Phần 1.

Để nhắc lại, một mục ở vị trí +/- 1 có thể có xác suất được phát cao hơn một mục ở vị trí +/- 4. Bạn có thể phân bổ thêm tài nguyên (mạng, CPU, bộ nhớ) cho các mục mà người dùng có nhiều khả năng sẽ xem tiếp theo. Điều này tạo ra một chiến lược "tải trước" dựa trên khoảng cách, đây là chìa khoá để cân bằng giữa việc phát ngay lập tức và sử dụng tài nguyên hiệu quả.

Bạn có thể sử dụng dữ liệu phân tích thông qua PreloadManagerListener như đã thảo luận trong các phần trước để quyết định chiến lược về thời lượng tải trước.

Kết luận và các bước tiếp theo

Giờ đây, bạn đã có kiến thức nâng cao để xây dựng nguồn cấp dữ liệu đa phương tiện nhanh, ổn định và tiết kiệm tài nguyên bằng DefaultPreloadManager của Media3.

Hãy cùng tóm tắt những điểm chính cần ghi nhớ:

  • Sử dụng PreloadManagerListener để thu thập thông tin chi tiết về số liệu phân tích và triển khai biện pháp xử lý lỗi hiệu quả.
  • Luôn sử dụng một DefaultPreloadManager.Builder để tạo cả phiên bản trình quản lý và trình phát nhằm đảm bảo các thành phần quan trọng được chia sẻ.
  • Triển khai mẫu cửa sổ trượt bằng cách chủ động quản lý các lệnh gọi add() và remove() để ngăn chặn OutOfMemoryError.
  • Sử dụng TargetPreloadStatusControl để tạo chiến lược tải trước thông minh, theo tầng, giúp cân bằng hiệu suất và mức tiêu thụ tài nguyên.

Nội dung tiếp theo trong Phần 3: Lưu vào bộ nhớ đệm bằng nội dung nghe nhìn được tải trước

Việc tải trước dữ liệu vào bộ nhớ mang lại lợi ích tức thì về hiệu suất, nhưng có thể phải đánh đổi. Sau khi ứng dụng đóng hoặc nội dung nghe nhìn được tải trước bị xoá khỏi trình quản lý, dữ liệu sẽ biến mất. Để đạt được mức độ tối ưu hoá ổn định hơn, chúng ta có thể kết hợp tính năng tải trước với bộ nhớ đệm của ổ đĩa. Tính năng này đang trong quá trình phát triển và sẽ sớm ra mắt trong vài tháng tới.

Bạn có ý kiến phản hồi nào muốn chia sẻ không? Chúng tôi rất mong nhận được ý kiến của bạn.

Hãy theo dõi và tăng tốc độ phát video của bạn! 🚀

Tác giả:
Tiếp tục đọc