产品动态

提升媒体播放体验:深入了解 Media3 的 PreloadManager - 第 2 部分

9 分钟阅读时间
Mayuri Khinvasara Khabya
开发者关系工程师

欢迎观看我们关于使用 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 播放交互的可视化表示

preloadManager2.png

通过共享 BandwidthMeter 防止带宽冲突

BandwidthMeter 根据历史传输速率估算可用网络带宽。如果 PreloadManager 和播放器使用单独的实例,它们将不知道彼此的网络活动,这可能会导致失败情况。例如,假设用户正在观看视频,其网络连接质量下降,并且预加载 MediaSource 同时为未来的视频启动了积极的下载。预加载 MediaSource 的活动会消耗活跃播放器所需的带宽,导致当前视频停滞。播放期间的停滞是严重的用户体验失败。

通过共享单个 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 轨道。<br /> 预加载管理器不应与播放器共享 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 在不同的线程上运行,可能会发生竞态条件。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 共享这些组件。当您通过 buildExoPlayer 的 DefaultPreloadManager.Builder 构建 ExoPlayer 实例时,如果您使用默认配置的默认实现,这些组件会自动相互引用。但是,如果您使用自定义组件或自定义配置,则应通过上述 API 明确 通知 DefaultPreloadManager。

可用于生产用途的预加载:滑动窗口模式

在动态 Feed 中,用户可以滚动浏览几乎无限的内容。如果您不断向 DefaultPreloadManager 添加视频,而没有相应的移除策略,则不可避免地会导致 OutOfMemoryError。每个预加载的 MediaSource 都保留一个SampleQueue,用于分配内存缓冲区。随着这些缓冲区的累积,它们可能会耗尽应用的堆空间。解决方案是您可能已经熟悉的算法,称为滑动窗口。滑动窗口模式在内存中维护一小部分可管理的项,这些项在逻辑上与用户在 Feed 中的当前位置相邻。当用户滚动时,此“窗口”中的托管项会随之滑动,添加进入视图的新项,并移除现在很远的项。

slidingwindow.png

实现滑动窗口模式

务必了解 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 部分的后续内容:使用预加载媒体进行缓存

将数据预加载到内存中可立即带来性能优势,但可能会带来一些权衡。关闭应用或从管理器中移除预加载的媒体后,数据将消失。为了实现更持久的优化级别,我们可以将预加载与磁盘缓存相结合。此功能正在积极开发中,将在几个月内推出。

您有什么反馈要分享吗?我们很期待您的反馈。

敬请关注,让您的视频播放速度更快!🚀

作者:

继续阅读