下載媒體

ExoPlayer 提供下載媒體的功能,方便使用者離線播放。在大多數情況下,即使應用程式在背景執行,也應繼續下載。在這些情況下,應用程式應將 DownloadService 子類別化,並將指令傳送至服務,以新增、移除及控管下載作業。下圖顯示相關的主要類別。

用於下載媒體的課程。箭頭方向表示資料流向。

  • 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,瞭解所有下載作業的狀態,包括已完成或失敗的下載作業。呼叫 DownloadManager.getDownloadIndex() 即可取得 DownloadIndex。然後呼叫 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)通常包含多個媒體軌道。通常有多個軌道包含相同內容,但品質不同(例如標清、高清和 4K 視訊軌道)。也可能存在多個相同類型的音軌,但內容不同(例如,多個不同語言的音軌)。

對於串流媒體播放,可以使用曲目選擇器來選擇要播放的曲目。同樣,對於下載,可以使用 DownloadHelper 來選擇要下載哪些曲目。DownloadHelper 的典型用法如下:

  1. 使用 DownloadHelper.Factory 執行個體建構 DownloadHelper。準備好輔助函數並等待回調。

    Kotlin

    val downloadHelper =
         DownloadHelper.Factory()
          .setRenderersFactory(DefaultRenderersFactory(context))
          .setDataSourceFactory(dataSourceFactory)
          .create(MediaItem.fromUri(contentUri))
    downloadHelper.prepare(callback)

    Java

    DownloadHelper downloadHelper =
       new DownloadHelper.Factory()
            .setRenderersFactory(new DefaultRenderersFactory(context))
            .setDataSourceFactory(dataSourceFactory)
            .create(MediaItem.fromUri(contentUri));
    downloadHelper.prepare(callback);
  2. (可選)使用 getMappedTrackInfogetTrackSelections 檢查預設選定的軌道,並使用 clearTrackSelectionsreplaceTrackSelectionsaddTrackSelection 進行調整。
  3. 透過呼叫 getDownloadRequest 為選定的曲目建立 DownloadRequest。可以按照上述說明將請求傳遞給您的 DownloadService 以新增下載。
  4. 使用 release() 釋放輔助函數。

播放下載的自適應內容需要配置播放器並傳遞對應的 MediaItem,如上所述。

建置 MediaItem 時,必須將 MediaItem.localConfiguration.streamKeys 設定為與 DownloadRequest 中的設定相匹配,以便播放器僅嘗試播放已下載的曲目子集。使用 Download.request.toMediaItemDownloadRequest.toMediaItem 來建構 MediaItem 即可解決這個問題。