产品资讯

提升媒体播放体验:深入了解 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 的活动会消耗活跃播放器所需的带宽,导致当前视频卡顿。播放期间卡顿是严重的用户体验问题。

通过共享单个 BandwidthMeter,TrackSelector 能够在预加载或播放期间,根据当前网络条件和缓冲区状态选择最高质量的轨道。然后,它可以做出智能决策,以保护活跃的播放会话并确保流畅的体验。

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

确保与 ExoPlayer 的共享 LoadControl、TrackSelector、Renderer 组件保持一致

  • LoadControl:此组件负责控制缓冲政策,例如在开始播放之前要缓冲多少数据,以及何时开始或停止加载更多数据。共享 LoadControl 可确保播放器和 PreloadManager 的内存消耗由预加载和正在播放的媒体的单一协调缓冲策略引导,从而防止资源争用。您必须根据预加载的内容数量和时长,巧妙地分配缓冲区空间,以确保一致性。在发生冲突时,播放器会优先播放屏幕上显示的当前内容。使用共享的 LoadControl 时,只要为预加载分配的目标缓冲区字节数未达到上限,预加载管理器就会继续预加载,而不会等到播放加载完成。

注意:在最新版本的 Media3 (1.8) 中,共享 LoadControl 可确保其 Allocator 能够正确地与 PreloadManager 和播放器共享。使用 LoadControl 有效控制预加载是即将发布的 Media3 1.9 版本中提供的一项功能。

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector:此组件负责选择要加载和播放的轨道(例如,特定分辨率的视频、特定语言的音频)。共享可确保在预加载期间选择的轨道与播放器将使用的轨道相同。这样可以避免出现以下浪费资源的情况:预加载了 480p 视频轨道,但播放器在播放时立即将其舍弃并提取 720p 轨道。< br /> 预加载管理器不应与播放器共享同一 TrackSelector 实例。而是应使用不同的 TrackSelector 实例,但采用相同的实现。因此,我们在 DefaultPreloadManager.Builder 中设置了 TrackSelectorFactory,而不是 TrackSelector。

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer:此组件负责了解播放器的功能,而无需创建完整的渲染器。它会检查此蓝图,以了解最终播放器将支持哪些视频、音频和文本格式。这样,播放器就可以智能地选择并仅下载兼容的媒体轨道,从而避免在播放器无法实际播放的内容上浪费带宽。

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,用于分配内存缓冲区。随着这些对象的累积,它们可能会耗尽应用的堆空间。解决方案是一种您可能已经熟悉的算法,称为滑动窗口。滑动窗口模式会在内存中维护一小组可管理的内容,这些内容在逻辑上与用户在信息流中的当前位置相邻。当用户滚动时,这个由受管理项组成的“窗口”会随之滑动,添加进入视野的新项,并移除现在距离较远的项。

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

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

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

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

继续阅读