Download di contenuti multimediali

ExoPlayer fornisce funzionalità per scaricare contenuti multimediali da riprodurre offline. Nella maggior parte dei casi d'uso, è preferibile che i download continuino anche quando l'app è in background. Per questi casi d'uso, l'app deve sottoclasse DownloadService e inviare al servizio i comandi per aggiungere, rimuovere e controllare i download. Il seguente diagramma mostra le classi principali coinvolte.

Corsi per il download di contenuti multimediali. Le frecce indicano il flusso di dati.

  • DownloadService: esegue il wrapping di un DownloadManager e vi inoltra i comandi. Il servizio consente a DownloadManager di rimanere in esecuzione anche quando l'app è in background.
  • DownloadManager: gestisce più download, il caricamento (e l'archiviazione) dei relativi stati da (e in) un DownloadIndex, l'avvio e l'interruzione dei download in base a requisiti come la connettività di rete e così via. Per scaricare i contenuti, il gestore in genere legge i dati scaricati da un HttpDataSource e li scrive in un Cache.
  • DownloadIndex: gli stati dei download rimangono persistenti.

Creazione di un DownloadService

Per creare un oggetto DownloadService, sottoclasse e implementa i suoi metodi astratti:

  • getDownloadManager(): restituisce DownloadManager da utilizzare.
  • getScheduler(): restituisce un Scheduler facoltativo, che può riavviare il servizio quando vengono soddisfatti i requisiti necessari per l'avanzamento dei download in attesa. ExoPlayer offre le seguenti implementazioni:
    • PlatformScheduler, che utilizza JobScheduler (l'API minima è 21). Per i requisiti di autorizzazione delle app, consulta la documentazione Java di PlatformScheduler.
    • WorkManagerScheduler, che utilizza WorkManager.
  • getForegroundNotification(): restituisce una notifica da visualizzare quando il servizio è in esecuzione in primo piano. Puoi utilizzare DownloadNotificationHelper.buildProgressNotification per creare una notifica in stile predefinito.

Infine, definisci il servizio nel file 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>

Guarda DemoDownloadService e AndroidManifest.xml nell'app demo di ExoPlayer per un esempio concreto.

Creazione di un DownloadManager

Lo snippet di codice riportato di seguito mostra come creare un'istanza di DownloadManager, che può essere restituita da getDownloadManager() nel tuo 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);

Vedi DemoUtil nell'app demo per un esempio concreto.

Aggiunta di un download

Per aggiungere un download, crea un DownloadRequest e invialo al tuo DownloadService. Per gli stream adattivi, utilizza DownloadHelper per creare un DownloadRequest. L'esempio seguente mostra come creare una richiesta di download:

Kotlin

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

Java

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

In questo esempio, contentId è un identificatore univoco dei contenuti. In casi semplici, l'contentUri può essere spesso utilizzato come contentId, ma le app sono senza costi per utilizzare qualsiasi schema ID adatto al loro caso d'uso. DownloadRequest.Builder ha anche alcuni setter facoltativi. Ad esempio, setKeySetId e setData possono essere utilizzati per impostare rispettivamente dati DRM e personalizzati che l'app vuole associare al download. Il tipo MIME dei contenuti può essere specificato anche utilizzando setMimeType, come suggerimento per i casi in cui il tipo di contenuto non può essere dedotto da contentUri.

Una volta creata, la richiesta può essere inviata a DownloadService per aggiungere il download:

Kotlin

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

Java

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

In questo esempio, MyDownloadService è la sottoclasse DownloadService dell'app e il parametro foreground determina se il servizio verrà avviato o meno in primo piano. Se la tua app è già in primo piano, il parametro foreground dovrebbe normalmente essere impostato su false perché DownloadService si mette in primo piano se stabilisce che ha del lavoro da svolgere.

Rimozione dei download…

Un download può essere rimosso inviando un comando di rimozione al DownloadService, dove contentId identifica il download da rimuovere:

Kotlin

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

Java

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

Puoi anche rimuovere tutti i dati scaricati con DownloadService.sendRemoveAllDownloads.

Avvio e interruzione dei download

Un download avvierà solo se vengono soddisfatte quattro condizioni:

  • Il download non presenta un motivo per l'interruzione.
  • I download non vengono messi in pausa.
  • I requisiti per l'avanzamento dei download sono soddisfatti. I requisiti possono specificare vincoli sui tipi di rete consentiti, nonché se il dispositivo deve essere inattivo o collegato a un caricabatterie.
  • Il numero massimo di download paralleli non è stato superato.

Tutte queste condizioni possono essere controllate inviando comandi al tuo DownloadService.

Impostazione e cancellazione dei motivi dell'interruzione del download

È possibile impostare il motivo dell'interruzione di uno o di tutti i download:

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 può essere qualsiasi valore diverso da zero (Download.STOP_REASON_NONE = 0 è un valore speciale che indica che il download non viene interrotto). Le app per cui esistono vari motivi per interrompere i download possono utilizzare valori diversi per tenere traccia del motivo per cui ogni download viene interrotto. L'impostazione e la cancellazione del motivo dell'interruzione per tutti i download funziona come l'impostazione e la cancellazione del motivo dell'interruzione per un singolo download, ad eccezione del fatto che contentId deve essere impostato su null.

Quando un download ha un motivo di interruzione diverso da zero, sarà nello stato Download.STATE_STOPPED. I motivi di interruzione sono persistenti in DownloadIndex e vengono quindi conservati se il processo dell'applicazione viene interrotto e riavviato in un secondo momento.

Sospensione e ripresa di tutti i download

Tutti i download possono essere messi in pausa e ripresi come segue:

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

Quando i download sono in pausa, saranno nello stato Download.STATE_QUEUED. A differenza dell'impostazione dei motivi di interruzione, questo approccio non ripristina modifiche di stato. Interessa solo lo stato di runtime di DownloadManager.

Impostazione dei requisiti per l'avanzamento dei download

Puoi utilizzare Requirements per specificare i vincoli che devono essere soddisfatti affinché i download proseguano. I requisiti possono essere impostati chiamando DownloadManager.setRequirements() durante la creazione di DownloadManager, come nell'esempio sopra. Possono anche essere modificate dinamicamente inviando 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);

Se non è possibile procedere perché i requisiti non sono soddisfatti, il download sarà nello stato Download.STATE_QUEUED. Puoi eseguire query sui requisiti non soddisfatti con DownloadManager.getNotMetRequirements().

Impostazione del numero massimo di download paralleli

Il numero massimo di download paralleli può essere impostato chiamando DownloadManager.setMaxParallelDownloads(). In genere questa operazione viene eseguita durante la creazione di DownloadManager, come nell'esempio sopra.

Se un download non può procedere perché è già in corso il numero massimo di download paralleli, il download sarà nello stato Download.STATE_QUEUED.

Esecuzione di query sui download

È possibile eseguire query sull'elemento DownloadIndex di un DownloadManager per conoscere lo stato di tutti i download, inclusi quelli completati o non riusciti. Il DownloadIndex può essere ottenuto chiamando DownloadManager.getDownloadIndex(). Chiamando DownloadIndex.getDownloads(), puoi ottenere un cursore che esegue l'iterazione di tutti i download. In alternativa, è possibile eseguire una query sullo stato di un singolo download chiamando DownloadIndex.getDownload().

DownloadManager fornisce anche DownloadManager.getCurrentDownloads(), che restituisce solo lo stato dei download correnti (ovvero non completati o non riusciti). Questo metodo è utile per aggiornare le notifiche e altri componenti dell'interfaccia utente che mostrano l'avanzamento e lo stato dei download correnti.

Ascolto dei download

Puoi aggiungere un listener a DownloadManager per ricevere un avviso quando lo stato dei download attuali cambia:

Kotlin

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

Java

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

Guarda DownloadManagerListener nella classe DownloadTracker dell'app demo per un esempio concreto.

Riproduzione dei contenuti scaricati

La riproduzione dei contenuti scaricati è simile alla riproduzione dei contenuti online, tranne per il fatto che i dati vengono letti dal download Cache anziché attraverso la rete.

Per riprodurre i contenuti scaricati, crea un elemento CacheDataSource.Factory utilizzando la stessa istanza Cache utilizzata per il download e inseriscila in DefaultMediaSourceFactory durante la creazione del player:

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

Se la stessa istanza del player verrà utilizzata anche per riprodurre contenuti non scaricati, CacheDataSource.Factory deve essere configurato come di sola lettura per evitare di scaricare anche questi contenuti durante la riproduzione.

Una volta configurato il player con CacheDataSource.Factory, il player avrà accesso ai contenuti scaricati per riprodurli. Per riprodurre un download basta trasmettere il valore MediaItem corrispondente al player. Un MediaItem può essere ottenuto da un Download utilizzando Download.request.toMediaItem o direttamente da un DownloadRequest utilizzando DownloadRequest.toMediaItem.

Configurazione MediaSource

L'esempio precedente rende disponibile la cache dei download per la riproduzione di tutti i contenuti MediaItem. Puoi anche rendere disponibile la cache di download per singole istanze MediaSource, che possono essere trasmesse direttamente al player:

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

Download e riproduzione degli stream adattivi

Gli stream adattivi (ad es. DASH, LiveStreaming e HLS) in genere contengono più tracce multimediali. Spesso sono presenti più tracce con gli stessi contenuti di qualità diverse (ad es. tracce video SD, HD e 4K). Potrebbero anche esserci più tracce dello stesso tipo con contenuti diversi (ad es. più tracce audio in lingue diverse).

Per le riproduzioni in streaming, puoi utilizzare un selettore di tracce per scegliere la traccia da riprodurre. Analogamente, per il download, è possibile utilizzare un DownloadHelper per scegliere quali tracce scaricare. L'utilizzo tipico di DownloadHelper prosegue i seguenti passaggi:

  1. Crea un oggetto DownloadHelper utilizzando uno dei metodi DownloadHelper.forMediaItem. Prepara l'assistente e attendi che venga richiamato.

    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. Se vuoi, esamina i canali selezionati predefiniti utilizzando getMappedTrackInfo e getTrackSelections e apporta le modifiche utilizzando clearTrackSelections, replaceTrackSelections e addTrackSelection.
  3. Crea un DownloadRequest per le tracce selezionate chiamando il numero getDownloadRequest. La richiesta può essere trasmessa a DownloadService per aggiungere il download, come descritto sopra.
  4. Rilascia l'helper utilizzando release().

La riproduzione dei contenuti adattivi scaricati richiede la configurazione del player e il superamento del codice MediaItem corrispondente, come descritto sopra.

Quando crei MediaItem, i MediaItem.localConfiguration.streamKeys devono essere impostati in modo che corrispondano a quelli presenti in DownloadRequest, in modo che il player provi a riprodurre soltanto il sottoinsieme di tracce scaricate. Se utilizzi Download.request.toMediaItem e DownloadRequest.toMediaItem per creare MediaItem, te ne occuperemo al posto tuo.