Download de mídia

O ExoPlayer oferece funcionalidade para baixar mídia para reprodução off-line. Na maioria dos casos de uso, é desejável que os downloads continuem mesmo quando o aplicativo estiver em segundo plano. Para esses casos de uso, seu aplicativo deve criar uma subclasse de 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 baixar mídia. As direções das setas indicam o fluxo de dados.

  • DownloadService: Envolve um DownloadManager e encaminha comandos para ele. O serviço permite que o DownloadManager continue em execução mesmo quando o aplicativo está em segundo plano.
  • DownloadManager: Gerencia vários downloads, carregando (e armazenando) seus estados de (e para) um DownloadIndex, iniciando e interrompendo downloads com base em requisitos como conectividade de rede e assim por diante. Para baixar o conteúdo, o gerenciador normalmente lê os dados que estão sendo baixados de um HttpDataSource e os grava em um Cache.
  • DownloadIndex: persiste os estados dos downloads.

Criando um serviço de download

Para criar um DownloadService, crie uma subclasse e implemente seus métodos abstratos:

  • getDownloadManager(): retorna o DownloadManager a ser usado.
  • getScheduler(): retorna um Scheduler opcional, que pode reiniciar o serviço quando os requisitos necessários para o progresso dos downloads pendentes forem atendidos. O ExoPlayer oferece estas implementações:
    • PlatformScheduler, que usa JobScheduler (API mínima é 21). Consulte a documentação javadoc do PlatformScheduler para obter informações sobre os requisitos de permissão do aplicativo.
    • WorkManagerScheduler, que usa o WorkManager.
  • getForegroundNotification(): Retorna uma notificação a ser exibida 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 seu 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>

VerDemoDownloadService eAndroidManifest.xml Para um exemplo concreto, consulte o aplicativo de demonstração do ExoPlayer.

Criando um Gerenciador de Downloads

O seguinte trecho de código demonstra como instanciar um DownloadManager, que pode ser retornado por getDownloadManager() em seu 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 ver um exemplo concreto.

Adicionando um download

Para adicionar um download, crie um DownloadRequest e envie-o para o seu DownloadService. Para fluxos adaptativos, usarDownloadHelper para ajudar a construir umDownloadRequest. 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 único para o conteúdo. Em casos simples, o contentUri pode muitas vezes ser usado como o contentId, no entanto, os aplicativos são livres para usar qualquer esquema de ID que melhor se adapte ao seu caso de uso. DownloadRequest.Builder também possui alguns setters opcionais. Por exemplo, setKeySetId e setData podem ser usados para definir DRM e dados personalizados que o aplicativo deseja 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.

Uma vez criada, a solicitação pode ser enviada para o DownloadService para adicionar o download:

Kotlin

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

Java

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

Neste exemplo, MyDownloadService é a subclasse DownloadService do aplicativo e o parâmetro foreground controla se o serviço será iniciado em primeiro plano. Se o seu aplicativo já estiver em primeiro plano, o parâmetro foreground normalmente deve ser definido como false porque o DownloadService 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, onde 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);

Você também pode remover todos os dados baixados com DownloadService.sendRemoveAllDownloads.

Como iniciar e interromper downloads

O download só prosseguirá se quatro condições forem atendidas:

  • O download não possui um motivo de interrupção.
  • Os downloads não estão pausados.
  • Os requisitos para que os downloads prossigam foram atendidos. Os requisitos podem especificar restrições nos tipos de rede permitidos, bem como se o dispositivo precisa estar ocioso ou conectado a um carregador.
  • O número máximo de downloads paralelos não foi excedido.

Todas essas condições podem ser controladas enviando comandos para o seu DownloadService.

Configurar e limpar os motivos de interrupção do download

É possível definir um motivo para que um ou todos os downloads sejam interrompidos:

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

Quando um download tem um motivo de interrupção diferente de zero, ele fica no estado Download.STATE_STOPPED. Os motivos de parada são persistidos em DownloadIndex e, portanto, sã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 ficarão no estado Download.STATE_QUEUED. Ao contrário de definir motivos de parada, esta abordagem não persiste nenhuma alteração de estado. Isso afeta apenas o estado de tempo de execução do DownloadManager.

Definir os requisitos para que os downloads prossigam.

RequirementsPode ser usado para especificar restrições que devem 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 o 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 pode prosseguir porque os requisitos não são atendidos, ele ficará no estado Download.STATE_QUEUED. Você pode consultar os requisitos não atendidos com DownloadManager.getNotMetRequirements().

Definir o número máximo de downloads paralelos

O número máximo de downloads paralelos pode ser definido chamando DownloadManager.setMaxParallelDownloads(). Normalmente, isso seria feito ao criar o DownloadManager, como no exemplo acima.

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

Consultando downloads

O DownloadIndex de um DownloadManager pode ser consultado para obter o estado de todos os downloads, incluindo aqueles que foram concluídos ou falharam. O DownloadIndex pode ser obtido chamando DownloadManager.getDownloadIndex(). Um cursor que itera sobre todos os downloads pode então ser obtido chamando DownloadIndex.getDownloads(). Alternativamente, o estado de um único download pode ser consultado chamando DownloadIndex.getDownload().

DownloadManager também fornece DownloadManager.getCurrentDownloads(), que retorna o estado dos downloads atuais (ou seja, não concluídos ou falhados). Este método é útil para atualizar notificações e outros componentes da interface do usuário que exibem o progresso e o status dos downloads em andamento.

Ouvindo downloads

Você pode adicionar um ouvinte a DownloadManager para ser notificado 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.
    });

VerDownloadManagerListener no aplicativo de demonstraçãoDownloadTracker aula para um exemplo concreto.

Reproduzindo conteúdo baixado

Reproduzir conteúdo baixado é semelhante a reproduzir conteúdo online, com a diferença de que os dados são lidos do arquivo baixado.Cache em vez de pela rede.

Para reproduzir conteúdo baixado, crie um CacheDataSource.Factory usando a mesma instância de Cache que foi usada para o download e injete-a em DefaultMediaSourceFactory ao construir o reprodutor:

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 baixado, então o CacheDataSource.Factory deve ser configurado como somente leitura para evitar o download desse conteúdo também durante a reprodução.

Uma vez que o reprodutor tenha sido configurado com o CacheDataSource.Factory, ele terá acesso ao conteúdo baixado para reprodução. Reproduzir um download é então tão simples quanto passar o MediaItem correspondente para o reprodutor. Um MediaItem pode ser obtido de um Download usando Download.request.toMediaItem, ou diretamente de um DownloadRequest usando DownloadRequest.toMediaItem.

Configuração do MediaSource

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

Baixar e reproduzir fluxos adaptativos

Os fluxos adaptativos (por exemplo, DASH, SmoothStreaming e HLS) normalmente contêm várias faixas de mídia. Geralmente, existem várias faixas que contêm o mesmo conteúdo em diferentes qualidades (por exemplo, faixas de vídeo SD, HD e 4K). Também pode haver várias faixas do mesmo tipo contendo conteúdo diferente (por exemplo, várias faixas de áudio em idiomas diferentes).

Para reprodução em streaming, um seletor de faixas pode ser usado para escolher quais faixas serão reproduzidas. Da mesma forma, para fazer download, um DownloadHelper pode ser usado para escolher quais das faixas serão baixadas. O uso típico de um DownloadHelper segue estes passos:

  1. Construa um DownloadHelper usando uma instância de DownloadHelper.Factory. Prepare a função auxiliar e aguarde o retorno da chamada.

    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);
  2. Opcionalmente, inspecione as faixas selecionadas por padrão 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 passada para o seu DownloadService para adicionar o download, conforme descrito acima.
  4. Libere o auxiliar usando release().

A reprodução de conteúdo adaptativo baixado requer a configuração do player e a passagem do MediaItem correspondente, conforme descrito acima.

Ao construir o MediaItem, MediaItem.localConfiguration.streamKeys deve ser definido para corresponder aos do DownloadRequest para que o reprodutor tente reproduzir apenas o subconjunto de faixas que foram baixadas. Usar Download.request.toMediaItem e DownloadRequest.toMediaItem para construir o MediaItem cuidará disso para você.