下载媒体

ExoPlayer 提供了下载媒体以进行离线播放的功能。在大多数情况下,即使您的应用在后台运行,下载也应该继续进行。对于这些用例,您的应用应该对 DownloadService 进行子类化,并向服务发送命令以添加、删除和控制下载。下图显示了涉及的主要类。

Classes for downloading media. The arrow directions indicate the flow of data.

  • DownloadService:包装 DownloadManager 并将命令转发给它。该服务允许 DownloadManager 即使在应用处于后台时也能继续运行。
  • DownloadManager:管理多个下载,从(以及到)DownloadIndex加载(和存储)其状态,根据网络连接等需求启动和停止下载。为了下载内容,管理器通常会从HttpDataSource读取正在下载的数据,并将其写入Cache
  • DownloadIndex:持久化下载的状态。

创建 DownloadService

要创建一个DownloadService,请对其进行子类化并实现其抽象方法。

  • getDownloadManager():返回要使用的DownloadManager
  • getScheduler():返回一个可选的Scheduler,当满足挂起下载继续进行所需的条件时,它可以重新启动服务。ExoPlayer 提供了以下实现:
  • getForegroundNotification():返回在服务在前台运行时要显示的通知。您可以使用DownloadNotificationHelper.buildProgressNotification创建默认样式的通知。

最后,在您的AndroidManifest.xml文件中定义服务。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<application>
  <service android:name="com.myapp.MyDownloadService"
      android:exported="false"
      android:foregroundServiceType="dataSync">
    <!-- This is needed for Scheduler -->
    <intent-filter>
      <action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
      <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
  </service>
</application>

请参阅 ExoPlayer 演示应用中的DemoDownloadServiceAndroidManifest.xml,了解具体的示例。

创建 DownloadManager

以下代码片段演示了如何实例化DownloadManager,它可以在您的DownloadService中的getDownloadManager()中返回。

Kotlin

// Note: This should be a singleton in your app.
val databaseProvider = StandaloneDatabaseProvider(context)

// A download cache should not evict media, so should use a NoopCacheEvictor.
val downloadCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), databaseProvider)

// Create a factory for reading the data from the network.
val dataSourceFactory = DefaultHttpDataSource.Factory()

// Choose an executor for downloading data. Using Runnable::run will cause each download task to
// download data on its own thread. Passing an executor that uses multiple threads will speed up
// download tasks that can be split into smaller parts for parallel execution. Applications that
// already have an executor for background downloads may wish to reuse their existing executor.
val downloadExecutor = Executor(Runnable::run)

// Create the download manager.
val downloadManager =
  DownloadManager(context, databaseProvider, downloadCache, dataSourceFactory, downloadExecutor)

// Optionally, properties can be assigned to configure the download manager.
downloadManager.requirements = requirements
downloadManager.maxParallelDownloads = 3

Java

// Note: This should be a singleton in your app.
databaseProvider = new StandaloneDatabaseProvider(context);

// A download cache should not evict media, so should use a NoopCacheEvictor.
downloadCache = new SimpleCache(downloadDirectory, new NoOpCacheEvictor(), databaseProvider);

// Create a factory for reading the data from the network.
dataSourceFactory = new DefaultHttpDataSource.Factory();

// Choose an executor for downloading data. Using Runnable::run will cause each download task to
// download data on its own thread. Passing an executor that uses multiple threads will speed up
// download tasks that can be split into smaller parts for parallel execution. Applications that
// already have an executor for background downloads may wish to reuse their existing executor.
Executor downloadExecutor = Runnable::run;

// Create the download manager.
downloadManager =
    new DownloadManager(
        context, databaseProvider, downloadCache, dataSourceFactory, downloadExecutor);

// Optionally, setters can be called to configure the download manager.
downloadManager.setRequirements(requirements);
downloadManager.setMaxParallelDownloads(3);

请参阅演示应用中的DemoUtil,了解具体的示例。

添加下载

要添加下载,请创建一个DownloadRequest并将其发送到您的DownloadService。对于自适应流,使用DownloadHelper来帮助构建DownloadRequest。以下示例显示了如何创建下载请求:

Kotlin

val downloadRequest = DownloadRequest.Builder(contentId, contentUri).build()

Java

DownloadRequest downloadRequest = new DownloadRequest.Builder(contentId, contentUri).build();

在此示例中,contentId是内容的唯一标识符。在简单情况下,contentUri通常可以用作contentId,但是应用程序可以自由使用最适合其用例的任何 ID 方案。DownloadRequest.Builder还有一些可选的设置器。例如,setKeySetIdsetData可分别用于设置DRM和应用程序希望与下载关联的自定义数据。还可以使用setMimeType指定内容的MIME类型,作为无法从contentUri推断出内容类型的情况下的提示。

创建请求后,可以将其发送到DownloadService以添加下载。

Kotlin

DownloadService.sendAddDownload(
  context,
  MyDownloadService::class.java,
  downloadRequest,
  /* foreground= */ false
)

Java

DownloadService.sendAddDownload(
    context, MyDownloadService.class, downloadRequest, /* foreground= */ false);

在此示例中,MyDownloadService是应用程序的DownloadService子类,foreground参数控制服务是否在前台启动。如果您的应用程序已经在前台,则通常应将foreground参数设置为false,因为如果DownloadService确定它有工作要做,它会将自己置于前台。

删除下载

可以通过向DownloadService发送删除命令来删除下载,其中contentId标识要删除的下载。

Kotlin

DownloadService.sendRemoveDownload(
  context,
  MyDownloadService::class.java,
  contentId,
  /* foreground= */ false
)

Java

DownloadService.sendRemoveDownload(
    context, MyDownloadService.class, contentId, /* foreground= */ false);

您还可以使用DownloadService.sendRemoveAllDownloads删除所有下载的数据。

启动和停止下载

只有满足四个条件,下载才会进行:

  • 下载没有停止原因。
  • 下载未暂停。
  • 满足下载继续进行的要求。要求可以指定对允许的网络类型的约束,以及设备是否应处于空闲状态或连接到充电器。
  • 未超过最大并行下载数。

所有这些条件都可以通过向您的DownloadService发送命令来控制。

设置和清除下载停止原因

可以为一个或所有停止的下载设置原因。

Kotlin

// Set the stop reason for a single download.
DownloadService.sendSetStopReason(
  context,
  MyDownloadService::class.java,
  contentId,
  stopReason,
  /* foreground= */ false
)

// Clear the stop reason for a single download.
DownloadService.sendSetStopReason(
  context,
  MyDownloadService::class.java,
  contentId,
  Download.STOP_REASON_NONE,
  /* foreground= */ false
)

Java

// Set the stop reason for a single download.
DownloadService.sendSetStopReason(
    context, MyDownloadService.class, contentId, stopReason, /* foreground= */ false);

// Clear the stop reason for a single download.
DownloadService.sendSetStopReason(
    context,
    MyDownloadService.class,
    contentId,
    Download.STOP_REASON_NONE,
    /* foreground= */ false);

stopReason可以是任何非零值(Download.STOP_REASON_NONE = 0是一个特殊值,表示下载未停止)。具有多个下载停止原因的应用程序可以使用不同的值来跟踪每个下载停止的原因。设置和清除所有下载的停止原因与设置和清除单个下载的停止原因的方式相同,只是contentId应设置为null

当下载具有非零停止原因时,它将处于Download.STATE_STOPPED状态。停止原因会保存在DownloadIndex中,因此如果应用程序进程被终止并在以后重新启动,则会保留这些原因。

暂停和恢复所有下载

所有下载都可以按如下方式暂停和恢复:

Kotlin

// Pause all downloads.
DownloadService.sendPauseDownloads(
  context,
  MyDownloadService::class.java,
  /* foreground= */ false
)

// Resume all downloads.
DownloadService.sendResumeDownloads(
  context,
  MyDownloadService::class.java,
  /* foreground= */ false
)

Java

// Pause all downloads.
DownloadService.sendPauseDownloads(context, MyDownloadService.class, /* foreground= */ false);

// Resume all downloads.
DownloadService.sendResumeDownloads(context, MyDownloadService.class, /* foreground= */ false);

暂停下载时,它们将处于Download.STATE_QUEUED状态。与设置停止原因不同,此方法不会持久化任何状态更改。它只影响DownloadManager的运行时状态。

设置下载继续进行的要求

Requirements可用于指定下载继续进行必须满足的约束条件。可以在创建DownloadManager时通过调用DownloadManager.setRequirements()来设置这些要求,如上面的示例所示。也可以通过向DownloadService发送命令来动态更改它们。

Kotlin

// Set the download requirements.
DownloadService.sendSetRequirements(
  context, MyDownloadService::class.java, requirements, /* foreground= */ false)

Java

// Set the download requirements.
DownloadService.sendSetRequirements(
  context,
  MyDownloadService.class,
  requirements,
  /* foreground= */ false);

如果由于不满足要求而无法继续下载,则它将处于Download.STATE_QUEUED状态。您可以使用DownloadManager.getNotMetRequirements()查询未满足的要求。

设置最大并行下载数

可以通过调用DownloadManager.setMaxParallelDownloads()来设置最大并行下载数。这通常在创建DownloadManager时完成,如上面的示例所示

如果由于最大并行下载数已在进行中而无法继续下载,则它将处于Download.STATE_QUEUED状态。

查询下载

DownloadManagerDownloadIndex可以查询所有下载的状态,包括已完成或失败的下载。DownloadIndex可以通过调用DownloadManager.getDownloadIndex()获得。然后可以通过调用DownloadIndex.getDownloads()获得迭代所有下载的游标。或者,可以通过调用DownloadIndex.getDownload()查询单个下载的状态。

DownloadManager还提供DownloadManager.getCurrentDownloads(),它只返回当前(即未完成或失败)下载的状态。此方法对于更新显示当前下载进度和状态的通知和其他 UI 组件非常有用。

监听下载

您可以向DownloadManager添加监听器,以便在当前下载状态更改时收到通知。

Kotlin

downloadManager.addListener(
  object : DownloadManager.Listener { // Override methods of interest here.
  }
)

Java

downloadManager.addListener(
    new DownloadManager.Listener() {
      // Override methods of interest here.
    });

请参阅演示应用程序的DownloadTracker类中的DownloadManagerListener,了解具体的示例。

播放下载的内容

播放下载的内容类似于播放在线内容,只是数据是从下载Cache读取的,而不是通过网络读取。

要播放下载的内容,请使用与下载时使用的相同Cache实例创建一个CacheDataSource.Factory,并在构建播放器时将其注入DefaultMediaSourceFactory

Kotlin

// Create a read-only cache data source factory using the download cache.
val cacheDataSourceFactory: DataSource.Factory =
  CacheDataSource.Factory()
    .setCache(downloadCache)
    .setUpstreamDataSourceFactory(httpDataSourceFactory)
    .setCacheWriteDataSinkFactory(null) // Disable writing.

val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setDataSourceFactory(cacheDataSourceFactory)
    )
    .build()

Java

// Create a read-only cache data source factory using the download cache.
DataSource.Factory cacheDataSourceFactory =
    new CacheDataSource.Factory()
        .setCache(downloadCache)
        .setUpstreamDataSourceFactory(httpDataSourceFactory)
        .setCacheWriteDataSinkFactory(null); // Disable writing.

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setDataSourceFactory(cacheDataSourceFactory))
        .build();

如果相同的播放器实例也将用于播放未下载的内容,则应将CacheDataSource.Factory配置为只读,以避免在播放过程中也下载该内容。

一旦播放器已配置了CacheDataSource.Factory,它就可以访问下载的内容以进行播放。播放下载就像将相应的MediaItem传递给播放器一样简单。可以使用Download.request.toMediaItemDownload获取MediaItem,或者可以使用DownloadRequest.toMediaItem直接从DownloadRequest获取。

MediaSource 配置

前面的示例使下载缓存可用于所有MediaItem的播放。您还可以使下载缓存可用于单个MediaSource实例,这些实例可以直接传递给播放器。

Kotlin

val mediaSource =
  ProgressiveMediaSource.Factory(cacheDataSourceFactory)
    .createMediaSource(MediaItem.fromUri(contentUri))
player.setMediaSource(mediaSource)
player.prepare()

Java

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
        .createMediaSource(MediaItem.fromUri(contentUri));
player.setMediaSource(mediaSource);
player.prepare();

下载和播放自适应流

自适应流(例如 DASH、SmoothStreaming 和 HLS)通常包含多个媒体轨道。通常有多个轨道包含不同质量的相同内容(例如 SD、HD 和 4K 视频轨道)。还可能有许多相同类型的轨道包含不同的内容(例如,不同语言的多个音频轨道)。

对于流播放,可以使用轨道选择器来选择播放哪些轨道。类似地,对于下载,可以使用DownloadHelper来选择下载哪些轨道。DownloadHelper的典型用法遵循以下步骤:

  1. 使用其中一种DownloadHelper.forMediaItem方法构建DownloadHelper。准备助手并等待回调。

    Kotlin

    val downloadHelper =
     DownloadHelper.forMediaItem(
       context,
       MediaItem.fromUri(contentUri),
       DefaultRenderersFactory(context),
       dataSourceFactory
     )
    downloadHelper.prepare(callback)

    Java

    DownloadHelper downloadHelper =
       DownloadHelper.forMediaItem(
           context,
           MediaItem.fromUri(contentUri),
           new DefaultRenderersFactory(context),
           dataSourceFactory);
    downloadHelper.prepare(callback);
  2. 可选地,使用getMappedTrackInfogetTrackSelections检查默认选择的轨道,并使用clearTrackSelectionsreplaceTrackSelectionsaddTrackSelection进行调整。
  3. 通过调用getDownloadRequest为选定的轨道创建DownloadRequest。该请求可以传递到您的DownloadService以添加下载,如上所述。
  4. 使用release()释放助手。

播放下载的自适应内容需要配置播放器并传递相应的MediaItem,如上所述。

构建MediaItem时,必须将MediaItem.localConfiguration.streamKeys设置为与DownloadRequest中的值匹配,以便播放器只尝试播放已下载的轨道子集。使用Download.request.toMediaItemDownloadRequest.toMediaItem构建MediaItem将为您处理此问题。