产品新闻

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

阅读用时:9 分钟
查看 Mayuri Khinvasara Khabya 的个人资料
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 的 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 组件。

黄金法则:一个通用的播放循环器,统管一切

在创建播放器时,可以通过传递 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 共享这些组件。如果您使用默认配置的默认实现,通过 DefaultPreloadManager.Builder 的 buildExoPlayer 构建 ExoPlayer 实例时,这些组件会自动相互引用。但如果您使用自定义组件或自定义配置,则应通过上述 API 明确告知 DefaultPreloadManager。

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

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

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

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

您有任何反馈要分享吗?我们期待收到您的来信。

敬请期待,快去加快视频播放速度吧!🚀

继续阅读