欢迎观看我们关于使用 Media3 进行媒体预加载的三部分系列视频的第二部分。本系列旨在引导您在 Android 应用中构建高响应速度、低延迟的媒体体验。
- 第 1 部分:介绍如何使用 Media3 进行预加载介绍了基本知识。我们探讨了简单播放列表的 PreloadConfiguration 与动态用户界面的更强大的 DefaultPreloadManager 之间的区别。您学习了如何实现基本的 API 生命周期:使用 add() 添加媒体、使用 getMediaSource() 检索准备好的 MediaSource、使用 setCurrentPlayingIndex() 和 invalidate() 管理优先级,以及使用 remove() 和 release() 释放资源。
- 第 2 部分(本博文):在本博文中,我们将探讨 DefaultPreloadManager 的高级功能。我们将介绍如何使用 PreloadManagerListener 获取分析洞见,如何实现可用于生产用途的最佳实践(例如与 ExoPlayer 共享核心组件),以及如何掌握滑动窗口模式以有效管理内存。
- 第 3 部分:本系列的最后一部分将深入探讨如何将 PreloadManager 与持久性磁盘缓存集成,从而让您能够通过资源管理来减少数据消耗并提供顺畅的体验。
如果您刚开始接触 Media3 中的预加载,我们强烈建议您先阅读第 1 部分,然后再继续。如果您已准备好学习更高级的知识,不妨探索如何提升媒体播放实现。
监听:使用 PreloadManagerListener 获取分析数据
作为应用开发者,您在正式版中发布某项功能时,也希望了解并捕获该功能背后的分析数据。如何确定您的预加载策略在实际环境中是否有效?回答此问题需要有关成功率、失败率和性能的数据。PreloadManagerListener 接口是收集此数据的主要机制。
PreloadManagerListener 提供两个基本回调,可提供有关预加载过程和状态的关键信息。
- onCompleted(MediaItem mediaItem):当预加载请求成功完成时(由您的 TargetPreloadStatusControl 定义),系统会调用此回调。
- onError(PreloadException error):此回调可用于调试和监控。当预加载失败时,系统会调用此方法,并提供关联的异常。
您可以通过一次方法调用来注册监听器,如以下示例代码所示:
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)
从监听器中提取数据洞见
这些监听器回调可以挂接到您的分析流水线。通过将这些事件转发到分析引擎,您可以回答以下关键问题:
- 我们的预加载成功率是多少?(onCompleted 事件与预加载尝试总数的比率)
- 哪些 CDN 或视频格式的错误率最高?(通过解析来自 onError 的异常)
- 我们的预加载错误率是多少?(onError 事件与预加载尝试总数的比率)
这些数据可为您提供有关预加载策略的量化反馈,从而支持 A/B 测试和数据驱动型用户体验改进。这些数据还可以进一步帮助您智能微调预加载时长、要预加载的视频数量以及您分配的缓冲区。
除了调试之外:使用 onError 实现优雅的界面回退
预加载失败是用户即将遇到缓冲事件的有力信号。通过 onError 回调,您可以做出被动响应。您可以调整界面,而不仅仅是记录错误。例如,如果即将播放的视频预加载失败,您的应用可以针对下一次滑动操作停用自动播放功能,要求用户点按才能开始播放。
此外,通过检查 PreloadException 类型,您可以定义更智能的重试策略。应用可以选择根据错误消息或 HTTP 状态代码立即从管理器中移除失败的来源。需要从界面流中移除相应商品,以免加载问题影响用户体验。您还可以从 PreloadException(例如 HttpDataSourceException)获取更精细的数据,以进一步探究错误。详细了解 ExoPlayer 问题排查。
伙伴系统:为什么需要与 ExoPlayer 共享组件?
DefaultPreloadManager 和 ExoPlayer 旨在协同工作。为确保稳定性和效率,它们必须共享多个核心组件。如果它们使用单独的、不协调的组件运行,可能会影响播放器上预加载轨道的线程安全性和可用性,因为我们需要确保预加载的轨道应在正确的播放器上播放。各个单独的组件也可能会争用有限的资源(如网络带宽和内存),从而导致性能下降。生命周期中的一个重要环节是处理适当的处置,建议的处置顺序是先释放 PreloadManager,然后再释放 ExoPlayer。
DefaultPreloadManager.Builder 旨在促进这种共享,并提供用于实例化 PreloadManager 和关联的播放器实例的 API。让我们来看看为什么必须共享 BandwidthMeter、LoadControl、TrackSelector、Looper 等组件。查看这些组件如何与 ExoPlayer 播放互动的直观表示。
通过共享 BandwidthMeter 防止带宽冲突
BandwidthMeter 可根据历史传输速率估算可用网络带宽。如果 PreloadManager 和播放器使用单独的实例,它们不会感知彼此的网络活动,这可能会导致失败情况。例如,假设用户正在观看视频,但网络连接变差,并且预加载 MediaSource 同时开始积极下载未来的视频。预加载 MediaSource 的 activity 会占用活跃播放器所需的带宽,导致当前视频停滞。播放期间出现停顿是严重的用户体验失败。
通过共享单个 BandwidthMeter,TrackSelector 能够在预加载或播放期间,根据当前的网络状况和缓冲区状态选择最高质量的轨道。然后,它可以做出智能决策,以保护正在进行的播放会话并确保流畅的体验。
preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)
确保与 ExoPlayer 的共享 LoadControl、TrackSelector、Renderer 组件保持一致
- LoadControl:此组件用于指定缓冲政策,例如在开始播放之前要缓冲多少数据,以及何时开始或停止加载更多数据。共享 LoadControl 可确保播放器和 PreloadManager 的内存消耗受到统一的协调缓冲策略的指导,该策略适用于预加载的媒体和正在播放的媒体,从而防止资源争用。您需要根据预加载的项目数量和时长合理分配缓冲区空间,以确保一致性。在发生冲突时,播放器会优先播放屏幕上显示的当前内容。使用共享的 LoadControl 时,只要为预加载分配的目标缓冲区字节数未达到上限,预加载管理器就会继续预加载,而不会等到播放加载完成。
注意:在最新版 Media3 (1.8) 中共享 LoadControl 可确保其分配器能够正确地与 PreloadManager 和播放器共享。使用 LoadControl 有效控制预加载是一项功能,将在即将发布的 Media3 1.9 版本中提供。
preloadManagerBuilder.setLoadControl(customLoadControl)
- TrackSelector:此组件负责选择要加载和播放的轨道(例如,特定分辨率的视频、特定语言的音频)。共享可确保预加载期间选择的轨道与播放器将使用的轨道相同。这样可以避免出现以下浪费的情况:预加载 480p 视频轨道,但播放器在播放时立即将其舍弃并提取 720p 轨道。预加载管理器不应与播放器共享 TrackSelector 的同一实例。而应使用不同的 TrackSelector 实例,但实现相同。因此,我们在 DefaultPreloadManager.Builder 中设置了 TrackSelectorFactory,而不是 TrackSelector。
preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)
- 渲染器:此组件负责了解播放器的功能,而无需创建完整的渲染器。它会检查此蓝图,以确定最终播放器将支持哪些视频、音频和文本格式。这样一来,播放器就可以智能地选择并下载兼容的媒体轨道,避免在播放器无法实际播放的内容上浪费带宽。
preloadManagerBuilder.setRenderersFactory(customRenderersFactory)
详细了解 Exoplayer 组件。
黄金法则:一个通用的 Playback Looper 即可掌控一切
在创建播放器时,可以通过传递 Looper 来明确指定可访问 ExoPlayer 实例的线程。可以使用 Player.getApplicationLooper 查询必须从中访问播放器的线程的 Looper。通过在播放器和 PreloadManager 之间保持共享的 Looper,可以保证对这些共享媒体对象的所有操作都序列化到单个线程的消息队列中。这可以减少并发 bug。
PreloadManager 与播放器之间所有涉及要加载或预加载的媒体源的互动都需要在同一播放线程上进行。共享 Looper 对于线程安全至关重要,因此我们必须在 PreloadManager 和播放器之间共享 PlaybackLooper。
PreloadManager 在后台准备有状态的 MediaSource 对象。当界面代码调用 player.setMediaSource(mediaSource) 时,您正在将此复杂的有状态对象从预加载的 MediaSource 切换到播放器。在此场景中,整个 PreloadMediaSource 从管理器移至播放器。所有这些互动和切换都应在同一 PlaybackLooper 上进行。
如果 PreloadManager 和 ExoPlayer 在不同的线程上运行,可能会发生竞态条件。在播放器线程尝试从 MediaSource 读取数据的确切时刻,PreloadManager 的线程可能会修改 MediaSource 的内部状态(例如,将新数据写入缓冲区)。这会导致难以调试的不可预测的行为和 IllegalStateException。
preloadManagerBuilder.setPreloadLooper(playbackLooper)
让我们看看如何在设置本身中,在 ExoPlayer 和 DefaultPreloadManager 之间共享上述所有组件。
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()
提示:如果您在 ExoPlayer 中使用默认组件(例如 DefaultLoadControl 等),则无需明确与 DefaultPreloadManager 共享这些组件。当您通过 DefaultPreloadManager.Builder 的 buildExoPlayer 构建 ExoPlayer 实例时,如果您使用默认配置的默认实现,这些组件会自动相互引用。但如果您使用自定义组件或自定义配置,则应通过上述 API 明确告知 DefaultPreloadManager。
可用于生产用途的预加载:滑动窗口模式
在动态 Feed 中,用户可以滚动浏览几乎无限量的内容。如果您不断向 DefaultPreloadManager 添加视频,但没有相应的移除策略,则必然会导致 OutOfMemoryError。每个预加载的 MediaSource 都会保留一个分配内存缓冲区的 SampleQueue。随着这些对象的累积,它们可能会耗尽应用的堆空间。该解决方案是一种您可能已经熟悉的算法,称为滑动窗口。滑动窗口模式会在内存中维护一小部分可管理的项,这些项在逻辑上与用户在 Feed 中的当前位置相邻。随着用户滚动,这个受管理的商品“窗口”也会随之滑动,添加进入视图的新商品,同时移除现在距离较远的商品。
实现滑动窗口模式
请务必了解,PreloadManager 不提供内置的 setWindowSize() 方法。滑动窗口是一种设计模式,需要由开发者负责使用原始的 add() 和 remove() 方法来实现。您的应用逻辑必须将界面事件(例如滚动或网页更改)与这些 API 调用相关联。如果您需要相关代码参考,我们已在 socialite 示例中实现了此滑动窗口模式,该示例还包含模仿滑动窗口的 PreloadManagerWrapper。
当媒体项不太可能很快出现在用户的观看内容中时,请务必在实现中添加 preloadManager.remove(mediaItem)。未能移除不再靠近用户的商品是预加载实现中出现内存问题的主要原因。remove() 调用可确保释放资源,从而帮助您将应用的内存使用量保持在一定范围内并保持稳定。
使用 TargetPreloadStatusControl 微调分类预加载策略
现在,我们已经定义了要预加载的内容(窗口中的项),接下来可以针对每个项应用明确定义的预加载量策略。在第 1 部分中,我们已经了解了如何通过 TargetPreloadStatusControl 设置实现这种精细度。
回想一下,位置为 +/- 1 的推荐项比位置为 +/- 4 的推荐项更有可能被播放。您可以为用户最有可能接下来观看的内容分配更多资源(网络、CPU、内存)。这会创建一个基于邻近度的“预加载”策略,这是在即时播放与高效资源使用之间取得平衡的关键。
您可以按照前面部分所述,通过 PreloadManagerListener 使用分析数据来确定预加载时长策略。
总结和后续步骤
现在,您已掌握相关高级知识,可以使用 Media3 的 DefaultPreloadManager 构建快速、稳定且资源高效的媒体 Feed。
我们来回顾一下要点:
- 使用 PreloadManagerListener 收集分析洞见并实现强大的错误处理。
- 请始终使用单个 DefaultPreloadManager.Builder 来创建管理器和播放器实例,以确保共享重要组件。
- 通过主动管理 add() 和 remove() 调用来实现滑动窗口模式,以防止出现 OutOfMemoryError。
- 使用 TargetPreloadStatusControl 创建智能分层预加载策略,以平衡性能和资源消耗。
第 3 部分的后续内容:使用预加载的媒体进行缓存
将数据预加载到内存中可立即提升性能,但可能会带来一些权衡。应用关闭或预加载的媒体从管理器中移除后,数据就会消失。为了实现更持久的优化效果,我们可以将预加载与磁盘缓存相结合。此功能正在积极开发中,将在未来几个月内推出。
您是否有任何反馈要分享?我们期待收到您的来信。
敬请期待,快去加快视频播放速度吧!🚀
继续阅读
-
产品资讯
在当今以媒体为中心的应用中,提供流畅、不间断的播放体验是打造出色用户体验的关键。用户希望视频能够立即开始播放,并且流畅播放,不会出现暂停。
Mayuri Khinvasara Khabya • 阅读用时:8 分钟
-
产品资讯
每位开发者的 AI 工作流程和需求都是独一无二的,因此能够选择 AI 如何帮助您进行开发非常重要。1 月,我们推出了在 Android Studio 中选择任何本地或远程 AI 模型来支持 AI 功能的功能
Matthew Warner • 阅读用时:2 分钟
-
产品资讯
Android Studio Panda 3 现已是稳定版,可在生产环境中使用。此版本可让您对 AI 赋能的工作流程进行更多控制和自定义,从而比以往更轻松地构建高品质的 Android 应用。
Matt Dyor • 阅读用时:3 分钟
随时了解最新动态
每周通过电子邮件接收最新的 Android 开发洞见。