产品动态

提升媒体播放体验:隆重推出 Media3 预加载功能 - 第 1 部分

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

在当今以媒体为中心的应用中,提供流畅、不间断的播放体验是打造出色用户体验的关键。用户希望视频能够立即开始播放,并且流畅播放,不会出现暂停。

核心挑战是延迟。传统上,视频播放器只有在用户选择要播放的项后才会开始工作,即连接、下载、解析、缓冲。对于当今的短视频环境,这种被动方法速度较慢。解决方案是主动出击。我们需要预测用户接下来会观看什么内容,并提前准备好内容。这就是预加载的本质。

预加载的主要优势包括:

  • 🚀 更快的播放开始速度: 视频已准备就绪,因此项之间的过渡更快,并且可以立即开始播放。
  • 📉 减少缓冲: 通过主动加载数据,播放不太可能停滞,例如由于网络故障而停滞。
  • ✨ 带来更流畅的用户体验: 更快的开始速度和更少的缓冲相结合,为用户创造了更流畅、更顺畅的互动体验。

在这个由三部分组成的系列中,我们将介绍并深入探讨 Media3 的强大实用程序,用于(预)加载组件。

  • 在第 1 部分中,我们将介绍基础知识:了解 Media3 中提供的不同预加载策略、启用 PreloadConfiguration 并设置 DefaultPreloadManager,使您的应用能够预加载项。阅读完这篇博文后,您应该能够使用配置的排名和时长预加载和播放媒体项。
  • 第 2 部分中,我们将深入探讨 DefaultPreloadManager 的更高级主题:使用监听器进行分析、探索可用于生产用途的最佳实践,例如滑动窗口模式以及 DefaultPreloadManager 和 ExoPlayer 的自定义共享组件。
  • 在第 3 部分中,我们将深入探讨使用 DefaultPreloadManager 进行磁盘缓存。

预加载来助你脱困!🦸‍♀️

预加载背后的核心理念很简单:在需要媒体内容之前加载它。当用户滑动到下一个视频时,视频的第一个片段已下载并可用,可以立即播放。

不妨将其想象成一家餐厅。繁忙的厨房不会等到接到订单才开始切洋葱。🧅 他们会提前做好准备工作。预加载就是视频播放器的准备工作。

启用后,当用户在播放缓冲区到达下一项之前跳到下一项时,预加载有助于最大限度地缩短加入延迟时间。系统会准备好下一个窗口的第一个周期,并缓冲视频、音频和文本样本。预加载的周期随后会排入播放器队列,缓冲的样本会立即可用,并准备好馈送到编解码器以进行呈现。

在 Media3 中,有两个主要的预加载 API,每个 API 都适用于不同的用例。选择正确的 API 是第一步。

1. 使用 PreloadConfiguration 预加载播放列表项

这是一种简单的方法,适用于线性、顺序媒体(如播放列表),其中播放顺序是可预测的(如一系列剧集)。您可以使用 ExoPlayer 的 播放列表 API 为播放器提供完整的媒体项列表,并为播放器设置 PreloadConfiguration,然后播放器会自动预加载序列中的下一项(如配置)。当用户在播放缓冲区已重叠到下一项之前跳到下一项时,此 API 会尝试优化加入延迟时间。

只有在没有为正在进行的播放加载任何媒体时,才会开始预加载,这样可以防止预加载与主要播放争用带宽。

如果您仍然不确定是否需要预加载,那么此 API 是一个不错的低成本选项,可以试用一下!

  player.preloadConfiguration =
    PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)

使用上述 PreloadConfiguration,播放器会尝试为播放列表中的下一项预加载 5 秒的媒体。

选择启用后,可以使用 PreloadConfiguration.DEFAULT 停用播放列表预加载,从而再次关闭播放列表预加载:

  player.preloadConfiguration = PreloadConfiguration.DEFAULT

2. 使用 PreloadManager 预加载动态列表

对于垂直 Feed 或轮播界面等动态界面,其中“下一项”由用户互动决定,PreloadManager API 是合适的选择。这是 Media3 ExoPlayer 库中的一个功能强大的全新独立组件,专门用于主动预加载。它管理着一系列潜在的 MediaSource,根据与用户当前位置的接近程度对它们进行优先级排序,并提供对要预加载的内容的精细 控制,适用于复杂的场景,例如短视频的动态 Feed。

设置 PreloadManager

DefaultPreloadManager 是 PreloadManager 的规范实现。

DefaultPreloadManager 的构建器可以构建 DefaultPreloadManager 和任何 ExoPlayer 实例,这些实例将播放其预加载的内容。如需创建 DefaultPreloadManager,您需要传递 TargetPreloadStatusControl,预加载管理器可以查询该控件以了解要为项加载多少内容。我们将在下面的部分中解释并定义 TargetPreloadStatusControl 的示例。

  val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
val preloadManager = val preloadManagerBuilder.build()

// Build ExoPlayer with DefaultPreloadManager.Builder
val player = preloadManagerBuilder.buildExoPlayer()

必须对 ExoPlayer 和 DefaultPreloadManager 使用相同的 构建器,这样可以确保正确共享它们后台的组件。

就是这样!您现在有一个管理器,可以随时接收指令。

使用 TargetPreloadStatusControl 配置时长和排名

如果您想预加载 10 秒的视频,该怎么办?您可以提供媒体项在轮播界面中的位置,DefaultPreloadManager 会根据用户当前播放的内容与待加载内容的接近程度,来确定待加载内容的加载优先级。

如果您想控制要预加载的项的时长,可以使用 DefaultPreloadManager.PreloadStatus 返回该时长。

例如,

  • 项“A”的优先级最高,加载 5 秒的视频。
  • 项“B”的优先级中等,但当您到达该项时,加载 3 秒的视频。
  • 项“C”的优先级较低,仅加载轨道。
  • 项“D”的优先级更低,只需准备即可。
  • 任何其他项都距离较远,请勿预加载任何内容。

这种精细控制有助于您优化资源利用率,建议您这样做以实现流畅播放。

  import androidx.media3.exoplayer.DefaultPreloadManager.PreloadStatus


class MyTargetPreloadStatusControl(
    currentPlayingIndex: Int = C.INDEX_UNSET
) : TargetPreloadStatusControl<Int,PreloadStatus> {


    // The app is responsible for updating this based on UI state
    override fun getTargetPreloadStatus(index: Int): PreloadStatus? {

        val distance = index - currentPlayingIndex

        // Adjacent items (Next): preload 5 seconds
        if (distance == 1) { 
        // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and suggest loading // 5000ms from the default start position
                    return PreloadStatus.specifiedRangeLoaded(5000L)
                } 

        // Adjacent items (Previous): preload 3 seconds
        else if (distance == -1) { 
        // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED //and suggest loading 3000ms from the default start position
                    return PreloadStatus.specifiedRangeLoaded(3000L)
                } 

        // Items two positions away: just select tracks
        else if (distance) == 2) {
        // Return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED
                    return PreloadStatus.TRACKS_SELECTED
                } 

        // Items four positions away: just select prepare
        else if (abs(distance) <= 4) {
        // Return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED
                    return PreloadStatus.SOURCE_PREPARED
                }

             // All other items are too far away
             return null
            }
}

提示:PreloadManager 可以预加载前一项和后一项,而 PreloadConfiguration 只会预加载后一项。

管理预加载项

创建管理器后,您可以开始告知它要处理的内容。当用户滚动浏览 Feed 时,您将识别即将播放的视频并将其添加到管理器中。与 PreloadManager 的互动是界面和预加载引擎之间基于状态的对话。

1. 添加媒体项

在填充 Feed 时,您必须告知 管理器 需要跟踪的媒体。如果您刚开始使用,可以添加要预加载的完整列表。随后,您可以根据需要随时向列表中添加单个项。您可以完全控制预加载列表中的项,这意味着您还必须管理添加到管理器和从管理器中移除的内容。

  val initialMediaItems = pullMediaItemsFromService(/* count= */ 20)
for (index in 0 until initialMediaItems.size) {
    preloadManager.add(
        initialMediaItems.get(index),index)
    )
}

管理器现在将开始在后台提取此 MediaItem 的数据。

添加后,告知管理器重新评估其新列表(提示某些内容已更改,例如添加/ 移除项,或者用户切换为播放新项)。

  preloadManager.invalidate()

2. 检索和播放项

接下来是主要的播放逻辑。当用户决定播放该视频时,您无需创建新的 MediaSource。相反,您需要向 PreloadManager 请求它已准备好的内容。您可以使用 MediaItem 从预加载管理器检索 MediaSource。

如果从 PreloadManager 检索到的项为 null,则表示 mediaItem 尚未预加载或添加到 PreloadMamager,因此您选择直接设置 mediaItem。

  // When a media item is about to displ​​ay on the screen
val mediaSource = preloadManager.getMediaSource(mediaItem)
if (mediaSource!= null) {
  player.setMediaSource(mediaSource)
} else {
  // If mediaSource is null, that mediaItem hasn't been added yet.
  // So, send it directly to the player.
  player.setMediaItem(mediaItem)
}
player.prepare()
// When the media item is displaying at the center of the screen
player.play()

通过准备从 PreloadManager 检索到的 MediaSource,您可以无缝地从预加载过渡到播放,使用内存中已有的数据。这就是开始时间更快的原因。

3. 使当前索引与界面保持同步

由于我们的 Feed / 列表可能是动态的,因此务必将当前播放索引通知 PreloadManager,以便它始终优先预加载最接近当前索引的项。

  preloadManager.setCurrentPlayingIndex(currentIndex)
// Need to call invalidate() to update the priorities
preloadManager.invalidate()

4. 移除项

为了保持管理器的效率,您应移除它不再需要跟踪的项,例如距离用户当前位置较远的项。

  // When an item is too far from the current playing index
preloadManager.remove(mediaItem)

如果您需要一次清除所有项,可以调用 preloadManager.reset()

5. 释放管理器

如果您不再需要 PreloadManager(例如,当界面被销毁时),则必须释放它以释放其资源。一个不错的做法是在您已释放播放器资源的位置执行此操作。建议您在播放器之前释放管理器,因为如果您不再需要任何预加载,播放器可以继续播放。

  // In your Activity's onDestroy() or Composable's onDispose
preloadManager.release()

演示时间

观看实际演示 👍

在下面的演示中,我们可以看到 PreloadManager 对右侧的影响,右侧的加载时间更快,而左侧显示的是现有体验。您还可以查看演示的代码示例。(奖励:它还会显示每个视频的启动延迟时间)

Demo-PreloadManager_2.webp

后续操作

第 1 部分到此结束!您现在拥有构建动态预加载系统的工具。您可以使用 PreloadConfiguration 预加载 ExoPlayer 中播放列表的下一项,也可以设置 DefaultPreloadManager、动态添加和移除项、配置目标预加载状态,并正确检索预加载的内容以进行播放。

第 2 部分 中,我们将深入探讨 DefaultPreloadManager。我们将探索如何监听预加载事件,讨论最佳实践(例如使用滑动窗口来避免内存问题),并深入了解 ExoPlayer 和 DefaultPreloadManager 的自定义共享组件。

您有任何反馈意见要 分享吗?我们非常期待您的反馈。

敬请关注,让您的应用运行得更快!🚀

继续阅读