Tin tức về sản phẩm
Nâng cao trải nghiệm phát nội dung nghe nhìn: Phân tích sâu về PreloadManager của Media3 – Phần 2
Đọc trong 9 phút
Chào mừng bạn đến với phần thứ hai trong loạt bài gồm 3 phần về tính năng 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 tạo trải nghiệm đa phương tiện có độ trễ thấp và có khả năng phản hồi cao trong các ứ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 những kiến thức cơ bản. Chúng ta đã tìm hiểu 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ẽ khám phá 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 hoạt động sản xuất 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ùng 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 liên tục, cho phép bạn giảm mức tiêu thụ dữ liệu bằng cách quản lý tài nguyên và mang đến trải nghiệm liền mạch.
Nếu mới bắt đầu sử dụng 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 việc triển khai tính năng phát nội dung nghe nhìn.
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 bản phát hành công khai, là nhà phát triển ứng dụng, bạn cũng muốn hiểu và nắm bắt số liệu phân tích đằng sau tính năng đó. Làm thế nào để bạn 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, thất bại và hiệu quả. Giao diện PreloadManagerListener là cơ chế chính để thu thập dữ liệu này.
PreloadManagerListener cung cấp 2 lệnh gọi lại thiết yếu, mang đến 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 một 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 quá trình tải trước không thành công, cung cấp ngoại lệ liên kết.
Bạn có thể đăng ký một trình nghe bằng một lệnh gọi phương thức duy nhất như trong đoạn 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ể liên kết các lệnh gọi lại của trình nghe này với quy trình phân tích của mình. Bằng cách chuyển tiếp những 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ệ thành công khi tải trước là bao nhiêu? (tỷ lệ số sự kiện onCompleted trên tổng số lượt thử 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 trường hợp ngoại lệ từ onError)
- Tỷ lệ lỗi tải trước của chúng tôi là bao nhiêu? (tỷ lệ sự kiện onError so với tổng số lần thử tải trước)
Dữ liệu này có thể cung cấp cho bạn thông tin phản hồi định lượng về chiến lược tải trước, cho phép bạn thực hiện 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 điều chỉnh trước một cách thông minh thời lượng và số lượng video bạn muốn tải trước cũng như các vùng đệ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 hiệu quả
Việc tải trước không thành công là một dấu hiệu rõ ràng cho thấy người dùng sắp gặp phải sự kiện đệm. Lệnh gọi lại onError cho phép bạn phản hồi một cách chủ động. 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 phát không tải trước được, ứ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 một chiến lược thử lại thông minh hơn. Một ứ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. Bạn cần xoá mục đó khỏi luồng giao diện người dùng cho phù hợp để các vấn đề về tải không ảnh hưởng đến trải nghiệm người dùng. Bạn cũng có thể nhận được dữ liệu chi tiết hơn từ PreloadException, chẳng hạn 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 hỗ trợ: 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 tính an toàn của luồng và khả năng sử dụng các bản nhạc được tải sẵn 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 sẵn phải được phát trên trình phát phù hợp. Các thành phần riêng biệt cũng có thể cạnh tranh để giành những tài nguyên có hạn như băng thông mạng và bộ nhớ, điều này có thể dẫn đến hiệu suất giảm sút. Một phần quan trọng của vòng đời là xử lý việc thải bỏ thích hợp. Thứ tự thải bỏ được đề xuất là trước tiên hãy giải phóng PreloadManager, sau đó là ExoPlayer.
DefaultPreloadManager.Builder được thiết kế để tạo điều kiện cho việc chia sẻ này và có các API để khởi tạo cả PreloadManager và một phiên bản trình phát được liên kết. Hãy xem lý do bạn phải chia sẻ các thành phần như BandwidthMeter, LoadControl, TrackSelector, Looper. Xem hình ảnh minh hoạ 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.
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 có sẵn dựa trên tốc độ truyền dữ liệu trong quá khứ. Nếu PreloadManager và trình phát sử dụng các phiên bản riêng biệt, thì chúng sẽ không biết hoạt động mạng của nhau, điều này có thể dẫn đến các trường hợp thất bại. 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ột video trong tương lai. Hoạt động MediaSource tải trước sẽ tiêu thụ băng thông mà trình phát đang hoạt động cần, khiến video hiện tại bị tạm dừng. Tình trạng giật hình trong quá trình 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 duy nhất, TrackSelector có thể chọn các bản nhạc có chất lượng cao nhất dựa trên tình trạng mạng hiện tại và trạng thái của bộ nhớ đệ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, Renderer dùng chung của ExoPlayer
- LoadControl: Thành phần này quy định chính sách lưu vào bộ nhớ đệm, chẳng hạn như lượng dữ liệu cần lưu vào bộ nhớ đệ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 đệm phối hợp duy nhất trên cả nội dung nghe nhìn được tải trước và đang phát, ngăn chặn tình trạng tranh chấp 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 trường hợp có xung đột, trình phát sẽ ưu tiên phát mục hiện tại đ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 đệ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 sẽ 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 những 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 phụ đề được chọn trong quá trình tải trước sẽ giống với các bản phụ đề 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 bản đó và tìm nạp bản 720p khi phát.< br /> Trình quản lý tải trước KHÔNG được dùng 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 phiên bản TrackSelector khác nhưng có cùng cách triển khai. Đó là lý do chúng tôi đặ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 đủ. Công cụ này kiểm tra bản thiết kế để xem trình phát cuối cùng sẽ hỗ trợ những định dạng video, âm thanh và văn bản nào. Nhờ đó, trình phát có thể chọn và tải xuống một cách thông minh chỉ bản âm thanh/phụ đề tương thích, đồng thời ngăn 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 để điều khiển tất cả
Bạn có thể chỉ định rõ ràng 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 trình lặp dùng chung giữa trình phát và PreloadManager, bạn có thể đảm bảo rằng tất cả các thao tác trên những đối tượng đa phương tiện dùng chung này đều được chuyển đổi tuần tự thành hàng đợi tin nhắn của một luồng duy nhất. Điều này có thể làm giảm các lỗi đồng thời.
Tất cả các hoạt động tương tác giữa PreloadManager và trình phát có nguồn nội dung nghe nhìn cần được tải hoặc tải trước đều phải diễn ra trên cùng một luồng phát. Việc chia sẻ Looper là điều bắt buộc để đả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 của bạn gọi player.setMediaSource(mediaSource), bạn đang thực hiện một thao tác chuyển giao đối tượng phức tạp, có trạng thái 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 di chuyển từ trình quản lý sang trình phát. Tất cả các lượt tương tác và chuyển đổi 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ì có thể xảy ra tình huống tương tranh. 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) ngay tại thời điểm luồng của trình phát đang cố gắng đọc dữ liệu từ đó. Điều này dẫn đến hành vi khó đoán, 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 trên giữa ExoPlayer và DefaultPreloadManager trong chính chế độ 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 này với DefaultPreloadManager. Khi bạn tạo phiên bản ExoPlayer thông qua buildExoPlayer của DefaultPreloadManager.Builder, các thành phần này sẽ tự động tham chiếu lẫn 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. Nhưng 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 nêu rõ cho DefaultPreloadManager biết về các thành phần và cấu hình đó thông qua các API ở trên.
Tải trước sẵn sàng cho sản xuất: Mẫu cửa sổ trượt
Trong 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ặp phải lỗi OutOfMemoryError. Mỗi MediaSource được tải trước sẽ giữ lại một SampleQueue, giúp phân bổ các vùng đệm bộ nhớ. Khi tích luỹ, những đối tượng này có thể làm cạn kiệt không gian heap của ứng dụng. Giải pháp là một thuật toán mà có thể bạn đã biết, đó là cửa sổ trượt. Mẫu cửa sổ trượt duy trì một nhóm nhỏ, dễ quản lý các mục trong bộ nhớ, theo logic là các mục này nằm cạnh 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ổ" này của các mục được quản lý sẽ trượt theo họ, thêm các mục mới xuất hiện và cũng xoá các mục hiện ở xa.
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. Cửa sổ trượt là một mẫu thiết kế mà bạn (nhà phát triển) chịu trách nhiệm triển khai bằng cách sử dụng các phương thức nguyên thuỷ add() và remove(). Logic ứng dụng của bạn phải kết nối các sự kiệ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 có một bản tham chiếu 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. Mẫu này cũng bao gồm một 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 xuất hiện trong quá trình xem của người dùng nữa. Không xoá được các mục không còn gần 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ính năng 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ớ ổn định và có giới hạn cho ứng dụng.
Tinh chỉnh chiến lược tải trước được phân loại bằng TargetPreloadStatusControl
Sau khi xác định những gì 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õ về số lượng cần tải trước cho từng mục. Chúng ta đã thấy cách đạt được độ chi tiết này bằng 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 so với 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 những 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 việc phát ngay với việc 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 để tạo 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 hữu ích.
- Luôn sử dụng một DefaultPreloadManager.Builder duy nhất để 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 lỗi OutOfMemoryError.
- Sử dụng TargetPreloadStatusControl để tạo một chiến lược tải trước thông minh, theo cấp bậc, cân bằng giữa 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 sẵn
Việc tải trước dữ liệu vào bộ nhớ mang lại lợi ích ngay lập tức về hiệu suất, nhưng có thể đi kèm với những điểm hạn chế. Khi ứng dụng bị đó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 việc tải trước với bộ nhớ đệm trên đĩa. Tính năng này đang được phát triển và sẽ 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! 🚀
Tiếp tục đọc
-
Tin tức về sản phẩm
Trong các ứng dụng tập trung vào nội dung nghe nhìn hiện nay, việc mang đến trải nghiệm phát mượt mà và không bị gián đoạn là yếu tố then chốt để mang lại trải nghiệm người dùng thú vị. Người dùng mong muốn video của họ bắt đầu phát ngay lập tức và phát liên tục mà không bị tạm dừng.
Mayuri Khinvasara Khabya • Đọc trong 8 phút
-
Tin tức về sản phẩm
Giờ đây, việc kiểm thử các lượt tương tác trên nhiều thiết bị trở nên dễ dàng hơn bao giờ hết nhờ Trình mô phỏng Android.
Steven Jenkins • Đọc trong 2 phút
-
Tin tức về sản phẩm
Mỗi nhà phát triển đều có quy trình làm việc và nhu cầu riêng về AI. Do đó, bạn cần có thể chọn cách AI hỗ trợ quá trình phát triển của mình. Vào tháng 1, chúng tôi đã giới thiệu khả năng chọn bất kỳ mô hình AI cục bộ hoặc từ xa nào để hỗ trợ chức năng AI trong Android Studio
Matthew Warner • Đọc trong 2 phút
Nhận thông tin cập nhật
Nhận thông tin chi tiết mới nhất về hoạt động phát triển trên Android trong hộp thư đến của bạn mỗi tuần.