Cómo descargar contenido multimedia

ExoPlayer proporciona la funcionalidad de descargar contenido multimedia para reproducirlo sin conexión. En la mayoría de los casos de uso, es conveniente que las descargas continúen incluso cuando la app está en segundo plano. En estos casos de uso, tu app debe crear una subclase de DownloadService y enviar comandos al servicio para agregar, quitar y controlar las descargas. En el siguiente diagrama, se muestran las clases principales que participan.

Clases para descargar contenido multimedia Las direcciones de las flechas indican el flujo de los datos.

  • DownloadService: Une un DownloadManager y le reenvía comandos. El servicio permite que DownloadManager siga ejecutándose incluso cuando la app está en segundo plano.
  • DownloadManager: Administra varias descargas, carga (y almacena) sus estados desde (y hasta) un DownloadIndex, inicia y detén descargas según requisitos como la conectividad de red, etcétera. Para descargar el contenido, el administrador generalmente leerá los datos que se descargan de un HttpDataSource y los escribirá en un Cache.
  • DownloadIndex: Conserva los estados de las descargas.

Cómo crear un DownloadService

Para crear una DownloadService, puedes subclasificarla e implementar sus métodos abstractos:

  • getDownloadManager(): Muestra el DownloadManager que se usará.
  • getScheduler(): Muestra un Scheduler opcional, que puede reiniciar el servicio cuando se cumplen los requisitos necesarios para que las descargas pendientes avancen. ExoPlayer proporciona las siguientes implementaciones:
    • PlatformScheduler, que usa JobScheduler (la API mínima es 21) Consulta los javadocs de PlatformScheduler para conocer los requisitos de permisos de la app.
    • WorkManagerScheduler, que usa WorkManager
  • getForegroundNotification(): Muestra una notificación que se mostrará cuando el servicio se ejecute en primer plano. Puedes usar DownloadNotificationHelper.buildProgressNotification para crear una notificación con el estilo predeterminado.

Por último, define el servicio en tu archivo 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>

Consulta DemoDownloadService y AndroidManifest.xml en la app de demostración de ExoPlayer para ver un ejemplo concreto.

Cómo crear un DownloadManager

En el siguiente fragmento de código, se muestra cómo crear una instancia de DownloadManager, que getDownloadManager() puede mostrar en tu DownloadService:

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

Consulta DemoUtil en la app de demostración para obtener un ejemplo concreto.

Cómo agregar una descarga

Para agregar una descarga, crea un DownloadRequest y envíalo a tu DownloadService. En el caso de las transmisiones adaptables, usa DownloadHelper para ayudar a compilar un DownloadRequest. En el siguiente ejemplo, se muestra cómo crear una solicitud de descarga:

Kotlin

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

Java

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

En este ejemplo, contentId es un identificador único para el contenido. En casos simples, contentUri a menudo se puede usar como contentId; sin embargo, las apps pueden usar el esquema de ID que mejor se adapte a su caso de uso. DownloadRequest.Builder también tiene algunos métodos set opcionales. Por ejemplo, se pueden usar setKeySetId y setData para configurar la DRM y los datos personalizados que la app desea asociar con la descarga, respectivamente. El tipo de MIME del contenido también se puede especificar con setMimeType, como sugerencia para los casos en los que no se puede inferir el tipo de contenido de contentUri.

Una vez creada, la solicitud se puede enviar a DownloadService para agregar la descarga:

Kotlin

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

Java

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

En este ejemplo, MyDownloadService es la subclase DownloadService de la app, y el parámetro foreground controla si el servicio se iniciará en primer plano. Si la app ya está en primer plano, el parámetro foreground normalmente debería establecerse en false porque DownloadService se pondrá en primer plano si determina que tiene trabajo para hacer.

Quitando descargas

Para quitar una descarga, se puede enviar un comando de eliminación a DownloadService, en el que contentId identifica la descarga que se quitará:

Kotlin

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

Java

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

También puedes quitar todos los datos descargados con DownloadService.sendRemoveAllDownloads.

Cómo iniciar y detener descargas

Una descarga solo progresará si se cumplen cuatro condiciones:

  • La descarga no tiene un motivo para detenerlo.
  • Las descargas no se pausaron.
  • Se cumplen los requisitos para que las descargas progresen. Los requisitos pueden especificar restricciones en los tipos de red permitidos y también si el dispositivo debe estar inactivo o conectado a un cargador.
  • No se supera la cantidad máxima de descargas paralelas.

Todas estas condiciones se pueden controlar mediante el envío de comandos a tu DownloadService.

Cómo configurar y borrar los motivos de detención de la descarga

Es posible establecer el motivo por el que se detienen una o todas las descargas:

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 puede ser cualquier valor distinto de cero (Download.STOP_REASON_NONE = 0 es un valor especial, lo que significa que la descarga no se detiene). Las apps que tienen varios motivos para detener las descargas pueden usar valores diferentes a fin de realizar un seguimiento de por qué se detiene cada descarga. Configurar y borrar el motivo de detención para todas las descargas funciona de la misma manera que configurar y borrar el motivo de detención de una sola descarga, con la excepción de que contentId se debe establecer en null.

Cuando una descarga tiene un motivo de detención distinto de cero, su estado es Download.STATE_STOPPED. Los motivos de detención se mantienen en DownloadIndex y, por lo tanto, se conservan si el proceso de la aplicación se cierra y luego se reinicia.

Cómo pausar y reanudar todas las descargas

Puedes detener y reanudar todas las descargas de la siguiente manera:

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

Cuando se pausen las descargas, tendrán el estado Download.STATE_QUEUED. A diferencia de la configuración de los motivos de detención, este enfoque no conserva ningún cambio de estado. Solo afecta el estado del tiempo de ejecución de DownloadManager.

Establece los requisitos para el progreso de las descargas

Requirements se puede usar para especificar las restricciones que se deben cumplir para que las descargas continúen. Los requisitos se pueden establecer llamando a DownloadManager.setRequirements() cuando se crea el DownloadManager, como en el ejemplo anterior. También se pueden cambiar de forma dinámica si se envía un comando a 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);

Cuando una descarga no pueda continuar porque no se cumplen los requisitos, tendrá el estado Download.STATE_QUEUED. Puedes consultar los requisitos que no se cumplieron con DownloadManager.getNotMetRequirements().

Configura la cantidad máxima de descargas paralelas

La cantidad máxima de descargas paralelas se puede configurar llamando a DownloadManager.setMaxParallelDownloads(). Por lo general, esto se hace cuando se crea el DownloadManager, como en el ejemplo anterior.

Cuando una descarga no pueda continuar porque la cantidad máxima de descargas paralelas ya está en curso, tendrá el estado Download.STATE_QUEUED.

Consulta descargas

Se puede consultar el DownloadIndex de un DownloadManager para conocer el estado de todas las descargas, incluidas las que se completaron o que fallaron. El DownloadIndex se puede obtener llamando a DownloadManager.getDownloadIndex(). Luego, se puede obtener un cursor que se itera en todas las descargas llamando a DownloadIndex.getDownloads(). De manera alternativa, el estado de una sola descarga se puede consultar llamando a DownloadIndex.getDownload().

DownloadManager también proporciona DownloadManager.getCurrentDownloads(), que solo muestra el estado de las descargas actuales (es decir, no completadas o con errores). Este método es útil para actualizar las notificaciones y otros componentes de la IU que muestran el progreso y el estado de las descargas actuales.

Cómo escuchar las descargas

Puedes agregar un objeto de escucha a DownloadManager para recibir una notificación cuando las descargas actuales cambien de estado:

Kotlin

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

Java

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

Consulta DownloadManagerListener en la clase DownloadTracker de la app de demostración para ver un ejemplo concreto.

Cómo reproducir el contenido descargado

La reproducción del contenido descargado es similar a la reproducción de contenido en línea, excepto que los datos se leen desde la Cache de descarga en lugar de hacerlo a través de la red.

Para reproducir contenido descargado, crea un CacheDataSource.Factory con la misma instancia de Cache que se usó para la descarga y, luego, inyéctalo en DefaultMediaSourceFactory cuando compiles el reproductor:

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

Si la misma instancia del reproductor también se usará para reproducir contenido no descargado, se debe configurar CacheDataSource.Factory como de solo lectura para evitar descargar ese contenido también durante la reproducción.

Una vez que el reproductor se haya configurado con el CacheDataSource.Factory, tendrá acceso al contenido descargado para la reproducción. Luego, reproducir una descarga es tan simple como pasar el MediaItem correspondiente al reproductor. Se puede obtener un MediaItem de un Download mediante Download.request.toMediaItem o directamente de un DownloadRequest mediante DownloadRequest.toMediaItem.

Configuración de MediaSource

En el ejemplo anterior, la caché de descarga está disponible para la reproducción de todos los objetos MediaItem. También puedes hacer que la caché de descarga esté disponible para instancias individuales de MediaSource, que se pueden pasar directamente al reproductor:

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

Cómo descargar y reproducir transmisiones adaptables

Las transmisiones adaptables (p.ej., DASH, SmoothStreaming y HLS) suelen contener varias pistas multimedia. A menudo, hay varias pistas que incluyen el mismo contenido en diferentes calidades (p.ej., pistas de video en SD, HD y 4K). También puede haber varias pistas del mismo tipo con contenido diferente (p.ej., varias pistas de audio en diferentes idiomas).

En el caso de las reproducciones de transmisión, se puede usar un selector de pistas para elegir cuál de las pistas se reproduce. Del mismo modo, se puede usar un objeto DownloadHelper para la descarga y elegir cuál de las pistas se descargará. El uso típico de un DownloadHelper sigue estos pasos:

  1. Compila un DownloadHelper con uno de los métodos DownloadHelper.forMediaItem. Prepara el asistente y espera la devolución de llamada.

    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. De manera opcional, puedes inspeccionar los segmentos seleccionados de forma predeterminada con getMappedTrackInfo y getTrackSelections, y realizar ajustes con clearTrackSelections, replaceTrackSelections y addTrackSelection.
  3. Llama a getDownloadRequest para crear un DownloadRequest para los segmentos seleccionados. La solicitud se puede pasar a tu DownloadService para agregar la descarga, como se describió anteriormente.
  4. Libera el asistente con release().

La reproducción del contenido adaptable descargado requiere configurar el reproductor y pasar el MediaItem correspondiente, como se describió anteriormente.

Cuando se compila MediaItem, se debe configurar MediaItem.localConfiguration.streamKeys para que coincida con las de DownloadRequest, de modo que el reproductor solo intente reproducir el subconjunto de pistas que se descargaron. El uso de Download.request.toMediaItem y DownloadRequest.toMediaItem para compilar MediaItem se encargará de esto.