Download de mídia

O ExoPlayer oferece a funcionalidade de fazer o download de mídia para reprodução off-line. Na maioria dos casos de uso, é desejável que os downloads continuem mesmo quando o app estiver em segundo plano. Para esses casos de uso, seu app precisa criar uma subclasse DownloadService e enviar comandos ao serviço para adicionar, remover e controlar os downloads. O diagrama a seguir mostra as principais classes envolvidas.

Classes para fazer o download de mídia. As direções das setas indicam o fluxo de dados.

  • DownloadService: encapsula uma DownloadManager e encaminha comandos para ela. O serviço permite que o DownloadManager continue em execução mesmo quando o app estiver em segundo plano.
  • DownloadManager: gerencia vários downloads, carregando (e armazenando) os estados de (e até) uma DownloadIndex, iniciando e interrompendo downloads com base em requisitos como conectividade de rede e assim por diante. Para fazer o download do conteúdo, o gerenciador normalmente lê os dados que estão sendo transferidos por download de um HttpDataSource e os grava em um Cache.
  • DownloadIndex: mantém os estados dos downloads.

Como criar um DownloadService

Para criar uma DownloadService, crie uma subclasse e implemente os métodos abstratos dela:

  • getDownloadManager(): retorna o DownloadManager a ser usado.
  • getScheduler(): retorna um Scheduler opcional, que poderá reiniciar o serviço quando os requisitos necessários para que os downloads pendentes sejam atendidos. O ExoPlayer oferece estas implementações:
  • getForegroundNotification(): retorna uma notificação que será mostrada quando o serviço estiver em execução em primeiro plano. Você pode usar DownloadNotificationHelper.buildProgressNotification para criar uma notificação no estilo padrão.

Por fim, defina o serviço no arquivo 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>

Consulte DemoDownloadService e AndroidManifest.xml no app de demonstração do ExoPlayer para ver um exemplo concreto.

Como criar um DownloadManager

O snippet de código abaixo demonstra como instanciar um DownloadManager, que pode ser retornado por getDownloadManager() no 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);

Consulte DemoUtil no app de demonstração para conferir um exemplo concreto.

Como adicionar um download

Para adicionar um download, crie um DownloadRequest e envie-o para seu DownloadService. Para streams adaptáveis, use DownloadHelper para ajudar a criar um DownloadRequest. O exemplo a seguir mostra como criar uma solicitação de download:

Kotlin

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

Java

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

Neste exemplo, contentId é um identificador exclusivo para o conteúdo. Em casos simples, o contentUri geralmente pode ser usado como o contentId. No entanto, os apps são livres para usar o esquema de ID mais adequado ao caso de uso. DownloadRequest.Builder também tem alguns setters opcionais. Por exemplo, setKeySetId e setData podem ser usados para definir o DRM e dados personalizados que o app quer associar ao download, respectivamente. O tipo MIME do conteúdo também pode ser especificado usando setMimeType, como uma dica para casos em que o tipo de conteúdo não pode ser inferido de contentUri.

Depois de criada, a solicitação pode ser enviada ao DownloadService para adicionar o download:

Kotlin

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

Java

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

Nesse exemplo, MyDownloadService é a subclasse DownloadService do app, e o parâmetro foreground controla se o serviço será iniciado em primeiro plano. Se o app já estiver em primeiro plano, o parâmetro foreground normalmente será definido como false, porque DownloadService vai se colocar em primeiro plano se determinar que tem trabalho a fazer.

Removendo downloads

Um download pode ser removido enviando um comando de remoção para DownloadService, em que contentId identifica o download a ser removido:

Kotlin

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

Java

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

Também é possível remover todos os dados transferidos por download com DownloadService.sendRemoveAllDownloads.

Como iniciar e interromper downloads

Um download só será realizado se quatro condições forem atendidas:

  • O download não tem um motivo para interromper.
  • Os downloads não são pausados.
  • Os requisitos para o progresso dos downloads foram atendidos. Os requisitos podem especificar restrições nos tipos de rede permitidos, bem como se o dispositivo deve estar inativo ou conectado a um carregador.
  • O número máximo de downloads paralelos não foi excedido.

Todas essas condições podem ser controladas pelo envio de comandos para a DownloadService.

Como definir e limpar os motivos das interrupções de download

É possível definir um motivo para a interrupção de um ou todos os downloads:

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 pode ser qualquer valor diferente de zero, sendo Download.STOP_REASON_NONE = 0 um valor especial que significa que o download não é interrompido. Apps que têm vários motivos para interromper downloads podem usar valores diferentes para acompanhar por que cada download foi interrompido. Definir e limpar o motivo da interrupção de todos os downloads funciona da mesma forma que definir e limpar o motivo da interrupção de um único download, mas com a exceção de que contentId precisa ser definido como null.

Quando um download tem um motivo de parada diferente de zero, ele fica no estado Download.STATE_STOPPED. Os motivos de parada são mantidos no DownloadIndex e, portanto, serão mantidos se o processo do aplicativo for encerrado e reiniciado posteriormente.

Pausar e retomar todos os downloads

Todos os downloads podem ser pausados e retomados da seguinte maneira:

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 os downloads são pausados, eles ficam no estado Download.STATE_QUEUED. Ao contrário da definição de motivos de parada, essa abordagem não mantém nenhuma mudança de estado. Isso afeta apenas o estado do ambiente de execução da DownloadManager.

Como definir os requisitos para andamento dos downloads

Requirements pode ser usado para especificar restrições que precisam ser atendidas para que os downloads prossigam. Os requisitos podem ser definidos chamando DownloadManager.setRequirements() ao criar o DownloadManager, como no exemplo acima. Eles também podem ser alterados dinamicamente, enviando um comando para 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);

Quando um download não puder continuar porque os requisitos não foram atendidos, ele ficará no estado Download.STATE_QUEUED. É possível consultar os requisitos não atendidos com DownloadManager.getNotMetRequirements().

Como definir o número máximo de downloads paralelos

O número máximo de downloads paralelos pode ser definido chamando DownloadManager.setMaxParallelDownloads(). Isso normalmente seria feito durante a criação do DownloadManager, como no exemplo acima.

Quando o download não pode continuar porque o número máximo de downloads paralelos já está em andamento, ele fica no estado Download.STATE_QUEUED.

Como consultar downloads

O DownloadIndex de um DownloadManager pode ser consultado para ver o estado de todos os downloads, incluindo os que foram concluídos ou falharam. O DownloadIndex pode ser obtido chamando DownloadManager.getDownloadIndex(). Um cursor que itera em todos os downloads pode ser recebido chamando DownloadIndex.getDownloads(). Como alternativa, o estado de um único download pode ser consultado chamando DownloadIndex.getDownload().

DownloadManager também fornece DownloadManager.getCurrentDownloads(), que retorna o estado apenas dos downloads atuais (ou seja, não concluídos ou com falha). Esse método é útil para atualizar notificações e outros componentes da interface que mostram o progresso e o status dos downloads atuais.

Como ouvir downloads

Você pode adicionar um listener a DownloadManager para ser informado quando os downloads atuais mudarem de estado:

Kotlin

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

Java

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

Consulte DownloadManagerListener na classe DownloadTracker do app de demonstração para ver um exemplo concreto.

Exibindo conteúdo transferido por download

Reproduzir conteúdo salvo é semelhante a reproduzir conteúdo on-line. A diferença é que os dados são lidos pela Cache de download, e não pela rede.

Para abrir conteúdo transferido por download, crie um CacheDataSource.Factory usando a mesma instância de Cache que foi usada para download e injete-a em DefaultMediaSourceFactory ao criar o 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 a mesma instância do player também for usada para reproduzir conteúdo não transferido, o CacheDataSource.Factory precisará ser configurado como somente leitura para evitar o download desse conteúdo durante a reprodução.

Depois de configurar o player com CacheDataSource.Factory, ele terá acesso ao conteúdo salvo para reprodução. Reproduzir um download é tão simples quanto transmitir o MediaItem correspondente ao player. Um MediaItem pode ser recebido de um Download usando Download.request.toMediaItem ou diretamente de um DownloadRequest usando DownloadRequest.toMediaItem.

Configuração do MediaSource

O exemplo anterior disponibiliza o cache de download para a reprodução de todos os MediaItems. Você também pode disponibilizar o cache de download para instâncias de MediaSource individuais, que podem ser transmitidas diretamente ao 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();

Fazer o download e reproduzir streams adaptáveis

Os streams adaptáveis (por exemplo, DASH, SmoothStreaming e HLS) normalmente contêm várias faixas de mídia. Muitas vezes, há várias faixas com o mesmo conteúdo em diferentes qualidades (por exemplo, faixas de vídeo em SD, HD e 4K). Também pode haver várias faixas do mesmo tipo com conteúdos diferentes (por exemplo, várias faixas de áudio em idiomas diferentes).

Para reproduções de streaming, um seletor de faixa pode ser usado para escolher quais das faixas são tocadas. Da mesma forma, para fazer o download, um DownloadHelper pode ser usado para escolher quais das faixas serão transferidas por download. O uso típico de um DownloadHelper segue estas etapas:

  1. Crie um DownloadHelper usando um dos métodos DownloadHelper.forMediaItem. Prepare o auxiliar e aguarde o callback.

    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 quiser, inspecione as faixas padrão selecionadas usando getMappedTrackInfo e getTrackSelections e faça ajustes usando clearTrackSelections, replaceTrackSelections e addTrackSelection.
  3. Crie um DownloadRequest para as faixas selecionadas chamando getDownloadRequest. A solicitação pode ser transmitida ao seu DownloadService para adicionar o download, conforme descrito acima.
  4. Libere o auxiliar usando release().

A reprodução de conteúdo adaptável transferido por download requer a configuração do player e a transmissão do MediaItem correspondente, conforme descrito acima.

Ao criar o MediaItem, MediaItem.localConfiguration.streamKeys precisa ser definido para corresponder aos do DownloadRequest para que o jogador tente apenas tocar o subconjunto de faixas que foram transferidas por download. Use Download.request.toMediaItem e DownloadRequest.toMediaItem para criar o MediaItem para fazer isso.