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

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

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

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

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

Чтобы создать 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 , который может быть возвращен методом 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 часто можно использовать в качестве contentId , однако приложения могут использовать любую схему идентификатора, которая лучше всего подходит для их случая использования. DownloadRequest.Builder также имеет несколько дополнительных установщиков. Например, setKeySetId и setData можно использовать для установки DRM и пользовательских данных, которые приложение желает связать с загрузкой соответственно. MIME-тип контента также можно указать с помощью 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 переведет себя на передний план, если определит, что ему есть над чем поработать.

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

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

Конфигурация медиаисточника

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

    Котлин

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

    Ява

    DownloadHelper downloadHelper =
       DownloadHelper.forMediaItem(
           context,
           MediaItem.fromUri(contentUri),
           new DefaultRenderersFactory(context),
           dataSourceFactory);
    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 позаботится об этом за вас.