ExoPlayer proporciona la funcionalidad para descargar contenido multimedia y reproducirlo sin conexión. En la mayoría de los casos de uso, es conveniente que las descargas continúen incluso cuando la app se ejecuta en segundo plano. Para 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 involucradas.
DownloadService: Envuelve unDownloadManagery reenvía comandos a él. El servicio permite queDownloadManagersiga ejecutándose incluso cuando la app está en segundo plano.DownloadManager: Administra varias descargas, carga (y almacena) sus estados desde (y hacia) unDownloadIndex, inicia y detiene descargas según requisitos como la conectividad de red, etcétera. Para descargar el contenido, el administrador suele leer los datos que se descargan de unHttpDataSourcey los escribe en unCache.DownloadIndex: Persiste los estados de las descargas.
Cómo crear un DownloadService
Para crear un DownloadService, crea una subclase y, luego, implementa sus métodos abstractos:
getDownloadManager(): Devuelve elDownloadManagerque se usará.getScheduler(): Devuelve unScheduleropcional, que puede reiniciar el servicio cuando se cumplen los requisitos necesarios para que avancen las descargas pendientes. ExoPlayer proporciona las siguientes implementaciones:PlatformScheduler, que usa JobScheduler (la API mínima es 21). Consulta la documentación de PlatformScheduler en Javadoc para conocer los requisitos de permisos de la app.WorkManagerScheduler, que usa WorkManager.
getForegroundNotification(): Devuelve una notificación que se mostrará cuando el servicio se ejecute en primer plano. Puedes usarDownloadNotificationHelper.buildProgressNotificationpara 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 devolver 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 ver un ejemplo concreto.
Cómo agregar una descarga
Para agregar una descarga, crea un objeto DownloadRequest y envíalo a tu objeto DownloadService. Para 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, a menudo se puede usar contentUri como contentId, pero 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 de configuración opcionales. Por ejemplo, setKeySetId y setData se pueden usar para establecer DRM y 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 el tipo de contenido no se puede inferir a partir 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 tu app ya está en primer plano, el parámetro foreground normalmente debe establecerse en false, ya que DownloadService se pondrá en primer plano si determina que tiene trabajo que hacer.
Quitando descargas
Para quitar una descarga, envía un comando de eliminación a DownloadService, donde 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
La descarga solo progresará si se cumplen cuatro condiciones:
- La descarga no tiene un motivo de detención.
- Las descargas no se pausaron.
- Se cumplen los requisitos para que avance la descarga. Los requisitos pueden especificar restricciones sobre los tipos de redes permitidos, así como si el dispositivo debe estar inactivo o conectado a un cargador.
- No se excedió la cantidad máxima de descargas paralelas.
Todas estas condiciones se pueden controlar enviando comandos a tu DownloadService.
Cómo establecer y borrar los motivos de detención de la descarga
Es posible establecer un motivo para detener 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 que significa que la descarga no se detiene). Las apps que tienen varios motivos para detener las descargas pueden usar diferentes valores para hacer un seguimiento de por qué se detiene cada descarga. Establecer y borrar el motivo de detención para todas las descargas funciona de la misma manera que establecer y borrar el motivo de detención para una sola descarga, excepto que contentId debe establecerse en null.
Cuando una descarga tiene un motivo de detención distinto de cero, estará en el estado Download.STATE_STOPPED. Los motivos de detención persisten en DownloadIndex y, por lo tanto, se conservan si el proceso de la aplicación se cierra y se reinicia más tarde.
Cómo pausar y reanudar todas las descargas
Todas las descargas se pueden pausar y reanudar 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, estarán en el estado Download.STATE_QUEUED.
A diferencia de establecer motivos de detención, este enfoque no conserva ningún cambio de estado. Solo afecta el estado de tiempo de ejecución de DownloadManager.
Cómo establecer los requisitos para que se completen las descargas
Requirements se puede usar para especificar restricciones que se deben cumplir para que se realicen las descargas. 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 enviando 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 no se pueda continuar con una descarga porque no se cumplen los requisitos, estará en el estado Download.STATE_QUEUED. Puedes consultar los requisitos no cumplidos con DownloadManager.getNotMetRequirements().
Cómo establecer la cantidad máxima de descargas paralelas
La cantidad máxima de descargas paralelas se puede establecer llamando a DownloadManager.setMaxParallelDownloads(). Esto se haría normalmente cuando se crea el DownloadManager, como en el ejemplo anterior.
Cuando no se pueda continuar con una descarga porque ya se está realizando la cantidad máxima de descargas paralelas, se encontrará en el estado Download.STATE_QUEUED.
Cómo consultar descargas
Se puede consultar el DownloadIndex de un DownloadManager para conocer el estado de todas las descargas, incluidas las que se completaron o fallaron. Puedes obtener el DownloadIndex llamando a DownloadManager.getDownloadIndex(). Luego, se puede obtener un cursor que itera sobre todas las descargas llamando a DownloadIndex.getDownloads(). Como alternativa, se puede consultar el estado de una sola descarga llamando a DownloadIndex.getDownload().
DownloadManager también proporciona DownloadManager.getCurrentDownloads(), que solo devuelve el estado de las descargas actuales (es decir, no completadas o fallidas). Este método es útil para actualizar notificaciones y otros componentes de la IU que muestran el progreso y el estado de las descargas actuales.
Cómo escuchar contenido descargado
Puedes agregar un objeto de escucha a DownloadManager para recibir información cuando cambie el estado de las descargas actuales:
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 contenido descargado
Reproducir contenido descargado es similar a reproducir contenido en línea, excepto que los datos se leen desde la descarga Cache en lugar de hacerlo a través de la red.
Para reproducir el contenido descargado, crea un CacheDataSource.Factory con la misma instancia de Cache que se usó para la descarga y, luego, insértalo 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, el CacheDataSource.Factory se debe configurar como de solo lectura para evitar que también se descargue ese contenido durante la reproducción.
Una vez que se configure el reproductor con el objeto CacheDataSource.Factory, tendrá acceso al contenido descargado para su reproducción. Luego, reproducir una descarga es tan simple como pasar el MediaItem correspondiente al reproductor. Se puede obtener un objeto MediaItem a partir de un objeto Download con Download.request.toMediaItem, o bien directamente desde un objeto DownloadRequest con DownloadRequest.toMediaItem.
Configuración de MediaSource
En el ejemplo anterior, se habilita la caché de descargas para la reproducción de todos los MediaItem. También puedes hacer que la caché de descargas esté disponible para instancias de MediaSource individuales, 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 varios segmentos de medios. A menudo, hay varias pistas que contienen 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 que contengan 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áles se reproducen. Del mismo modo, para la descarga, se puede usar un DownloadHelper para elegir cuáles de las pistas se descargan. El uso típico de un DownloadHelper sigue estos pasos:
- Compila un
DownloadHelpercon una instancia deDownloadHelper.Factory. Prepara el asistente y espera la devolución de llamada. - De manera opcional, inspecciona los segmentos seleccionados de forma predeterminada con
getMappedTrackInfoygetTrackSelections, y realiza ajustes conclearTrackSelections,replaceTrackSelectionsyaddTrackSelection. - Llama a
getDownloadRequestpara crear unDownloadRequestpara los segmentos seleccionados. La solicitud se puede pasar a tuDownloadServicepara agregar la descarga, como se describió anteriormente. - Libera el asistente con
release().
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);
La reproducción del contenido adaptable descargado requiere configurar el reproductor y pasar el objeto MediaItem correspondiente, como se describió anteriormente.
Cuando se compila el objeto MediaItem, se debe configurar MediaItem.localConfiguration.streamKeys para que coincida con los valores de DownloadRequest, de modo que el reproductor solo intente reproducir el subconjunto de pistas que se descargaron. Si usas Download.request.toMediaItem y DownloadRequest.toMediaItem para compilar MediaItem, esto se controlará automáticamente.