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

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

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

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

Создание службы загрузки

Для создания объекта DownloadService необходимо создать его подкласс и реализовать его абстрактные методы:

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

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

Котлин

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

Java

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

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

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

Котлин

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 идентифицирует загружаемый файл, который необходимо удалить:

Котлин

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

Java

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

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

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

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

Котлин

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

Котлин

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

Запрос на скачивание

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

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

Прослушивание загруженных файлов

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

Котлин

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

Java

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

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

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

Воспроизведение загруженного контента аналогично воспроизведению онлайн-контента, за исключением того, что данные считываются из 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()

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

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 используя экземпляр DownloadHelper.Factory . Подготовьте вспомогательный класс и дождитесь обратного вызова.
  2. При желании, вы можете просмотреть выбранные по умолчанию треки с помощью getMappedTrackInfo и getTrackSelections , а также внести корректировки с помощью clearTrackSelections , replaceTrackSelections и addTrackSelection .
  3. Создайте запрос DownloadRequest для выбранных треков, вызвав getDownloadRequest . Запрос можно передать в вашу DownloadService для добавления загрузки, как описано выше.
  4. Освободите вспомогательный объект, используя release() .

Котлин

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

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

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