Download de mídia

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

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

  • DownloadService: encapsula um DownloadManager e encaminha comandos para ele. A permite que o DownloadManager continue em execução mesmo quando o app estiver em em segundo plano.
  • DownloadManager: gerencia vários downloads, carregamento (e armazenamento) dos próprios estados de (e para) uma DownloadIndex, iniciando e interrompendo downloads com base em requisitos como conectividade de rede e assim por diante. Para fazer o download do o gerenciador normalmente lê os dados que estão sendo baixados de um HttpDataSource e a grave em um Cache.
  • DownloadIndex: mantém os estados dos downloads.

Como criar um DownloadService

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

  • getDownloadManager(): retorna o DownloadManager a ser usado.
  • getScheduler(): retorna um Scheduler opcional, que pode reiniciar o quando os requisitos necessários para o andamento dos downloads pendentes forem atendidos. O ExoPlayer oferece estas implementações:
  • getForegroundNotification(): retorna uma notificação que será exibida quando o serviço está sendo executado em primeiro plano. Você pode usar DownloadNotificationHelper.buildProgressNotification para criar um 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>

Consultar DemoDownloadService e AndroidManifest.xml no ExoPlayer aplicativo de demonstração para um exemplo concreto.

Como criar um Gerenciador de downloads

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 ver um exemplo concreto.

Como adicionar um download

Para adicionar um download, crie um DownloadRequest e envie-o para o DownloadService. Para streams adaptáveis, use DownloadHelper para ajudar criar um DownloadRequest. O seguinte 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 do conteúdo. Em casos simples, contentUri geralmente pode ser usado como o contentId, mas os apps são sem custo financeiro qualquer 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 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 dica para os 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 fazer 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. O parâmetro foreground controla se o serviço será iniciado no primeiro plano. Se o app já estiver em primeiro plano, a foreground parâmetro deve ser definido como false porque DownloadService se coloca em primeiro plano se determina que tem trabalho a fazer.

Removendo downloads

Um download pode ser removido enviando um comando de remoção ao 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);

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

Como iniciar e interromper downloads

Um download só vai progredir se quatro condições forem atendidas:

  • O download não tem um motivo para ser interrompido.
  • Os downloads não são pausados.
  • Os requisitos para progresso dos downloads foram atendidos. Os requisitos podem especificar sobre os tipos de rede permitidos e se o dispositivo deve 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 ao seu DownloadService:

Como definir e limpar motivos de interrupção de download

É possível definir um motivo para um ou todos os downloads serem 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 do motivo da interrupção dos downloads. Definindo e limpando o motivo da parada de todos funciona da mesma maneira que configurar e limpar o motivo da parada de um download único, exceto contentId, que precisa ser definido como null.

Quando um download tem um motivo de parada diferente de zero, ele estará no Download.STATE_STOPPED. Os motivos das paradas são persistentes DownloadIndex e, portanto, serão retidas se o processo do aplicativo for encerrado e reiniciados posteriormente.

Como 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 forem pausados, eles ficarão no estado Download.STATE_QUEUED. Ao contrário da configuração dos motivos das paradas, essa abordagem não mantém nenhum estado. mudanças. Ela afeta apenas o estado do ambiente de execução do DownloadManager.

Como definir os requisitos para o andamento dos downloads

Requirements pode ser usado para especificar restrições que precisam ser atendidas para para continuar. Os requisitos podem ser definidos chamando DownloadManager.setRequirements() ao criar o DownloadManager, como em no exemplo acima. Eles também podem ser alterados dinamicamente enviando um comando ao 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 continuar porque os requisitos não foram atendidos, ele estará no estado Download.STATE_QUEUED. É possível consultar os atributos não atendidos requisitos com DownloadManager.getNotMetRequirements().

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

Para definir o número máximo de downloads paralelos, chame DownloadManager.setMaxParallelDownloads(): Isso normalmente seria feito quando criando o DownloadManager, como no exemplo acima.

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

Como consultar downloads

O DownloadIndex de um DownloadManager pode ser consultado para o estado de todos downloads, incluindo os concluídos ou com falha. O DownloadIndex podem ser obtidos chamando DownloadManager.getDownloadIndex(). Um cursor que Iterações em todos os downloads podem ser obtidas chamando-se DownloadIndex.getDownloads(): Por outro lado, o estado de um único download podem ser consultados chamando DownloadIndex.getDownload().

DownloadManager também fornece DownloadManager.getCurrentDownloads(), que retorna somente o estado dos downloads atuais (ou seja, não concluídos ou com falha). Isso é útil para atualizar notificações e outros componentes de IU que exibem o progresso e o status dos downloads atuais.

Como ouvir os downloads

Você pode adicionar um listener a DownloadManager para receber uma notificação quando a os downloads mudam 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 um exemplo concreto.

Reproduzindo conteúdo baixado

Reproduzir conteúdo salvo é semelhante a reproduzir conteúdo on-line, com a diferença os dados são lidos no Cache do download em vez de na rede.

Para abrir conteúdo salvo, crie um CacheDataSource.Factory usando o mesmo Cache usada para download e injetá-la no 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 baixado o CacheDataSource.Factory precisa ser configurado como somente leitura para evitar fazer o download desse conteúdo também durante a reprodução.

Depois que o player tiver sido configurado com o CacheDataSource.Factory, ele tenham acesso ao conteúdo baixado para reprodução. A reprodução de um download é tão simples quanto transmitir o MediaItem correspondente ao jogador. Um MediaItem pode ser extraída 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 reprodução de todos MediaItems. Também é possível disponibilizar o cache de downloads para instâncias individuais de MediaSource, que podem ser transmitidas diretamente ao jogador:

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

Como fazer o download e abrir streams adaptáveis

Transmissões 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 qualidades diferentes (por exemplo, faixas de vídeo em SD, HD e 4K). Também pode haver várias faixas do mesmo tipo com conteúdo diferente (por exemplo, várias faixas de áudio em diferentes idiomas).

Para reproduções de streaming, um seletor de faixa pode ser usado para escolher qual das faixas forem tocadas. Da mesma forma, para fazer o download, um DownloadHelper pode ser usado para escolha qual das faixas será baixada. Uso típico de um DownloadHelper siga estas etapas:

  1. Crie um DownloadHelper usando um dos DownloadHelper.forMediaItem métodos. Prepare o assistente 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. Como alternativa, inspecione as faixas padrão selecionadas usando getMappedTrackInfo e getTrackSelections, além de fazer ajustes usando clearTrackSelections. replaceTrackSelections e addTrackSelection.
  3. Crie um DownloadRequest para as faixas selecionadas chamando getDownloadRequest. A solicitação pode ser transmitida para o DownloadService para adicione-o, conforme descrito acima.
  4. Libere o assistente usando release().

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

Ao criar o MediaItem, o MediaItem.localConfiguration.streamKeys precisa ser definido para corresponder aos do DownloadRequest, de modo que o jogador só tente reproduzir o subconjunto de faixas que foram transferidas por download. Usando Download.request.toMediaItem e DownloadRequest.toMediaItem para criar o MediaItem vai cuidar disso para você.