Загрузка мультимедиа

ExoPlayer предоставляет функционал для загрузки медиафайлов для воспроизведения офлайн. В большинстве случаев желательно, чтобы загрузка продолжалась, даже когда приложение работает в фоновом режиме. В таких случаях ваше приложение должно создать подкласс DownloadService и отправлять сервису команды для добавления, удаления и управления загрузками. На следующей диаграмме показаны основные задействованные классы.

Классы для загрузки медиафайлов. Направления стрелок указывают поток данных.

  • DownloadService : Создаёт оболочку DownloadManager и передаёт ему команды. Сервис позволяет DownloadManager продолжать работу, даже когда приложение находится в фоновом режиме.
  • DownloadManager : управляет несколькими загрузками, загружая (и сохраняя) их состояния из (и в) DownloadIndex , запуская и останавливая загрузки в зависимости от таких требований, как сетевое подключение и т. д. Чтобы загрузить контент, менеджер обычно считывает загружаемые данные из HttpDataSource и записывает их в Cache .
  • DownloadIndex : Сохраняет состояния загрузок.

Создание DownloadService

Чтобы создать DownloadService , создайте для него подкласс и реализуйте его абстрактные методы:

  • getDownloadManager() : возвращает DownloadManager для использования.
  • getScheduler() : возвращает необязательный Scheduler , который может перезапустить службу при выполнении условий, необходимых для выполнения ожидающих загрузок. ExoPlayer предоставляет следующие реализации:
    • PlatformScheduler , который использует JobScheduler (минимальный API — 21). Требования к разрешениям приложения см. в документации Java по PlatformScheduler .
    • WorkManagerScheduler , который использует WorkManager .
  • 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>

Конкретный пример см. в DemoDownloadService и AndroidManifest.xml в демонстрационном приложении ExoPlayer.

Создание DownloadManager

В следующем фрагменте кода показано, как создать экземпляр DownloadManager , который может быть возвращен методом getDownloadManager() в вашем DownloadService :

Котлин

// 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

Ява

// 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 . В следующем примере показано, как создать запрос на загрузку:

Котлин

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

Ява

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

В этом примере contentId — уникальный идентификатор контента. В простых случаях в качестве contentUri часто можно использовать contentUri, однако приложения могут использовать любую схему идентификаторов, наиболее подходящую для их случая. DownloadRequest.Builder также имеет несколько дополнительных сеттеров. Например, setKeySetId и setData можно использовать для установки DRM и пользовательских данных, которые приложение хочет связать с загрузкой, соответственно. contentId тип контента также можно указать с помощью setMimeType в качестве подсказки для случаев, когда тип контента невозможно определить из contentUri .

После создания запрос можно отправить в DownloadService для добавления загрузки:

Котлин

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

Ява

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

В этом примере MyDownloadService — подкласс DownloadService приложения, а параметр foreground управляет запуском службы в активном режиме. Если приложение уже запущено в активном режиме, параметр foreground обычно следует установить равным false поскольку DownloadService сам перейдет в активное состояние, если определит, что ему нужно выполнить какие-то задачи.

Удаление загрузок

Загрузку можно удалить, отправив команду remove в DownloadService , где contentId идентифицирует удаляемую загрузку:

Котлин

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

Ява

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

Вы также можете удалить все загруженные данные с помощью DownloadService.sendRemoveAllDownloads .

Запуск и остановка загрузок

Загрузка будет продолжена только при соблюдении четырех условий:

  • Загрузка не имеет причины остановки.
  • Загрузки не приостанавливаются.
  • Требования для продолжения загрузки соблюдены. Требования могут определять ограничения на разрешённые типы сетей, а также указывать, должно ли устройство находиться в режиме ожидания или быть подключенным к зарядному устройству.
  • Максимальное количество параллельных загрузок не превышено.

Все эти условия можно контролировать, отправляя команды в DownloadService .

Установка и удаление причин остановки загрузки

Можно указать причину остановки одной или всех загрузок:

Котлин

// 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
)

Ява

// 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 и сохраняются даже при завершении и последующем перезапуске процесса приложения.

Приостановка и возобновление всех загрузок

Все загрузки можно приостановить и возобновить следующим образом:

Котлин

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

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

Ява

// 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.setRequirements() при создании DownloadManager , как в примере выше . Их также можно изменять динамически, отправив команду в DownloadService :

Котлин

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

Ява

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

Если загрузка не может быть продолжена из-за невыполнения требований, она переходит в состояние Download.STATE_QUEUED . Вы можете запросить невыполненные требования с помощью DownloadManager.getNotMetRequirements() .

Установка максимального количества параллельных загрузок

Максимальное количество параллельных загрузок можно задать, вызвав метод DownloadManager.setMaxParallelDownloads() . Обычно это делается при создании DownloadManager , как в примере выше .

Если загрузка не может быть продолжена из-за того, что уже выполняется максимальное количество параллельных загрузок, она перейдет в состояние Download.STATE_QUEUED .

Запрос загрузок

DownloadIndex объекта DownloadManager можно запросить для получения информации о состоянии всех загрузок, включая завершённые и неудавшиеся. DownloadIndex можно получить, вызвав DownloadManager.getDownloadIndex() . Курсор, который перебирает все загрузки, можно получить, вызвав DownloadIndex.getDownloads() . Кроме того, состояние отдельной загрузки можно запросить, вызвав DownloadIndex.getDownload() .

DownloadManager также предоставляет DownloadManager.getCurrentDownloads() , который возвращает состояние только текущих загрузок (т. е. незавершённых или неудачных). Этот метод полезен для обновления уведомлений и других компонентов пользовательского интерфейса, отображающих ход и статус текущих загрузок.

Прослушивание загрузок

Вы можете добавить прослушиватель в DownloadManager , чтобы получать уведомления об изменении состояния текущих загрузок:

Котлин

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

Ява

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

Конкретный пример см. в классе DownloadManagerListener демонстрационного приложения DownloadTracker .

Воспроизведение загруженного контента

Воспроизведение загруженного контента похоже на воспроизведение онлайн-контента, за исключением того, что данные считываются из Cache загрузки, а не по сети.

Чтобы воспроизвести загруженный контент, создайте CacheDataSource.Factory , используя тот же экземпляр Cache , который использовался для загрузки, и внедрите его в DefaultMediaSourceFactory при сборке проигрывателя:

Котлин

// 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()

Ява

// 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 . MediaItem можно получить из объекта Download с помощью Download.request.toMediaItem или напрямую из объекта DownloadRequest с помощью DownloadRequest.toMediaItem .

Конфигурация MediaSource

В предыдущем примере кэш загрузки доступен для воспроизведения всех объектов MediaItem . Вы также можете сделать кэш загрузки доступным для отдельных экземпляров MediaSource , передавая его напрямую в проигрыватель:

Котлин

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

Ява

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 используя экземпляр DownloadHelper.Factory . Подготовьте помощник и дождитесь обратного вызова.

    Котлин

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

    Ява

    DownloadHelper downloadHelper =
       new DownloadHelper.Factory()
            .setRenderersFactory(new DefaultRenderersFactory(context))
            .setDataSourceFactory(dataSourceFactory)
            .create(MediaItem.fromUri(contentUri));
    downloadHelper.prepare(callback);
  2. При желании вы можете проверить выбранные по умолчанию треки с помощью getMappedTrackInfo и getTrackSelections и внести изменения с помощью clearTrackSelections , replaceTrackSelections и addTrackSelection .
  3. Создайте DownloadRequest для выбранных треков, вызвав getDownloadRequest . Запрос можно передать в DownloadService для добавления загрузки, как описано выше.
  4. Освободите помощника с помощью release() .

Для воспроизведения загруженного адаптивного контента требуется настроить проигрыватель и передать соответствующий MediaItem , как описано выше.

При создании MediaItem необходимо настроить MediaItem.localConfiguration.streamKeys так, чтобы они соответствовали значениям в DownloadRequest , чтобы проигрыватель пытался воспроизвести только подмножество загруженных треков. Использование Download.request.toMediaItem и DownloadRequest.toMediaItem для создания MediaItem сделает это автоматически.