Pobieram multimedia

ExoPlayer zapewnia funkcjonalność umożliwiającą pobieranie multimediów w celu odtwarzania ich w trybie offline. W większości przypadków pobieranie powinno być kontynuowane nawet wtedy, gdy aplikacja działa w tle. W takich przypadkach aplikacja powinna utworzyć podklasę DownloadService i wysyłać polecenia do usługi w celu dodawania, usuwania i kontrolowania pobrań. Poniższy diagram przedstawia główne klasy, które są zaangażowane w ten proces.

Klasy do pobierania multimediów. Kierunki strzałek wskazują przepływ danych.

  • DownloadService: Opakowuje DownloadManager i przekazuje do niego polecenia. Usługa umożliwia działanie DownloadManager nawet wtedy, gdy aplikacja działa w tle.
  • DownloadManager: Zarządza wieloma pobraniami, ładując (i przechowując) ich stany z (i do) DownloadIndex, rozpoczynając i zatrzymując pobieranie na podstawie wymagań, takich jak łączność sieciowa itd. Aby pobrać zawartość, menedżer zazwyczaj odczyta dane pobierane z HttpDataSource i zapisze je w Cache.
  • DownloadIndex: Utrwala stany pobranych plików.

Tworzenie usługi pobierania

Aby utworzyć DownloadService, utwórz jego podklasę i zaimplementuj jego metody abstrakcyjne:

  • getDownloadManager(): Zwraca DownloadManager, który ma zostać użyty.
  • getScheduler(): Zwraca opcjonalny Scheduler, który może ponownie uruchomić usługę, gdy zostaną spełnione wymagania dotyczące kontynuacji oczekujących pobrań. ExoPlayer udostępnia te implementacje:
    • PlatformScheduler, który używa JobScheduler (minimalna wersja API to 21). Wymagania dotyczące uprawnień aplikacji można znaleźć w dokumentacji Javadoc PlatformScheduler.
    • WorkManagerScheduler, który używa WorkManager.
  • getForegroundNotification(): zwraca powiadomienie, które ma być wyświetlane, gdy usługa działa na pierwszym planie. Możesz użyć DownloadNotificationHelper.buildProgressNotification, aby utworzyć powiadomienie w domyślnym stylu.

Na koniec zdefiniuj usługę w pliku 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>

Konkretny przykład znajdziesz w DemoDownloadServiceAndroidManifest.xml w aplikacji demonstracyjnej ExoPlayera.

Tworzenie obiektu DownloadManager

Ten fragment kodu pokazuje, jak utworzyć instancję DownloadManager, która może być zwracana przez getDownloadManager()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);

Konkretny przykład znajdziesz w sekcji DemoUtil w aplikacji demonstracyjnej.

Dodawanie pobierania

Aby dodać plik do pobrania, utwórz DownloadRequest i wyślij go do DownloadService. W przypadku strumieni adaptacyjnych użyj DownloadHelper, aby ułatwić utworzenie DownloadRequest. Ten przykład pokazuje, jak utworzyć żądanie pobierania:

Kotlin

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

Java

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

W tym przykładzie contentId jest unikatowym identyfikatorem zawartości. W prostych przypadkach contentUri można często używać jako contentId, jednak aplikacje mogą swobodnie używać dowolnego schematu identyfikatorów, który najlepiej odpowiada ich potrzebom. DownloadRequest.Builder ma również kilka opcjonalnych setterów. Na przykład setKeySetId i setData można użyć do ustawienia DRM i niestandardowych danych, które aplikacja chce powiązać z pobieraniem. Typ MIME zawartości można również określić za pomocą setMimeType, co stanowi wskazówkę w przypadkach, gdy typu zawartości nie można wywnioskować z contentUri.

Po utworzeniu żądanie można wysłać do DownloadService w celu dodania pobierania:

Kotlin

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

Java

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

W tym przykładzie MyDownloadService to podklasa DownloadService aplikacji, a parametr foreground określa, czy usługa zostanie uruchomiona na pierwszym planie. Jeśli aplikacja jest już na pierwszym planie, parametr foreground powinien być zwykle ustawiony na false, ponieważ DownloadService przejdzie na pierwszy plan, jeśli uzna, że ma coś do zrobienia.

Usuwam pobrane

Pobieranie można usunąć, wysyłając polecenie usuwania do DownloadService, gdzie contentId identyfikuje pobieranie do usunięcia:

Kotlin

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

Java

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

Możesz też usunąć wszystkie pobrane dane za pomocą funkcji DownloadService.sendRemoveAllDownloads.

Rozpoczynanie i zatrzymywanie pobierania

Pobieranie będzie kontynuowane tylko wtedy, gdy spełnione zostaną cztery warunki:

  • Pobieranie nie ma powodu zatrzymania.
  • Pobieranie nie jest wstrzymywane.
  • Wymagania dotyczące dalszego pobierania są spełnione. Wymagania mogą określać ograniczenia dotyczące dozwolonych typów sieci, a także to, czy urządzenie powinno być bezczynne, czy podłączone do ładowarki.
  • Maksymalna liczba równoległych pobrań nie została przekroczona.

Wszystkimi tymi warunkami można sterować, wysyłając polecenia do DownloadService.

Ustawianie i usuwanie przyczyn zatrzymania pobierania

Można ustawić powód zatrzymania jednego lub wszystkich pobrań:

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 może być dowolną wartością różną od zera (Download.STOP_REASON_NONE = 0 to specjalna wartość oznaczająca, że pobieranie nie jest zatrzymywane). Aplikacje, które mają wiele powodów zatrzymania pobierania, mogą używać różnych wartości, aby śledzić przyczynę zatrzymania każdego pobierania. Ustawianie i czyszczenie przyczyny zatrzymania dla wszystkich pobrań działa w taki sam sposób jak ustawianie i czyszczenie przyczyny zatrzymania dla pojedynczego pobierania, z tą różnicą, że contentId należy ustawić na null.

Jeśli powód zatrzymania pobierania jest różny od zera, pobieranie będzie miało stan Download.STATE_STOPPED. Powody zatrzymania są zapisywane w DownloadIndex i zostają zachowane, jeśli proces aplikacji zostanie zamknięty i uruchomiony ponownie.

Wstrzymywanie i wznawianie wszystkich pobrań

Każde pobieranie można wstrzymać i wznowić w następujący sposób:

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

Po wstrzymaniu pobierania będzie ono miało stan Download.STATE_QUEUED. W przeciwieństwie do ustawiania przyczyn zatrzymania, to podejście nie powoduje utrwalenia żadnych zmian stanu. Ma to wpływ tylko na stan środowiska wykonawczego DownloadManager.

Ustawianie wymagań dotyczących postępu pobierania

Requirements można użyć do określenia ograniczeń, które muszą zostać spełnione, aby można było pobrać pliki. Wymagania można ustawić, wywołując funkcję DownloadManager.setRequirements() podczas tworzenia funkcji DownloadManager, jak w przykładzie powyżej. Można je też zmieniać dynamicznie, wysyłając polecenie do 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);

Jeśli pobieranie nie może być kontynuowane, ponieważ nie są spełnione wymagania, będzie ono w stanie Download.STATE_QUEUED. Zapytanie dotyczące niespełnionych wymagań możesz wysłać za pomocą DownloadManager.getNotMetRequirements().

Ustawianie maksymalnej liczby równoległych pobrań

Maksymalną liczbę równoległych pobrań można ustawić, wywołując funkcję DownloadManager.setMaxParallelDownloads(). Zwykle robi się to podczas tworzenia DownloadManager, jak w przykładzie powyżej.

Jeśli nie można kontynuować pobierania, ponieważ osiągnięto już maksymalną liczbę równoległych operacji pobierania, będzie ono miało stan Download.STATE_QUEUED.

Wykonywanie zapytań dotyczących pobranych plików

Możesz wysłać zapytanie do DownloadIndex DownloadManager, aby sprawdzić stan wszystkich pobrań, w tym tych, które zostały zakończone lub nie powiodły się. DownloadIndex Można je uzyskać, dzwoniąc pod numer DownloadManager.getDownloadIndex(). Kursor, który iteruje po wszystkich pobranych plikach, można uzyskać, wywołując funkcję DownloadIndex.getDownloads(). Stan pojedynczego pobierania można też sprawdzić, wywołując funkcję DownloadIndex.getDownload().

DownloadManager udostępnia też DownloadManager.getCurrentDownloads(), które zwraca stan tylko bieżących (czyli nieukończonych lub nieudanych) pobrań. Ta metoda jest przydatna do aktualizowania powiadomień i innych komponentów interfejsu, które wyświetlają postęp i stan bieżących pobrań.

Słuchanie pobranych plików

Możesz dodać detektor do DownloadManager, aby otrzymywać powiadomienia o zmianach stanu bieżących pobrań:

Kotlin

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

Java

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

Konkretny przykład znajdziesz w klasie DownloadTrackerDownloadManagerListener w aplikacji demonstracyjnej.

Odtwarzanie pobranych treści

Odtwarzanie pobranych treści jest podobne do odtwarzania treści online, z tą różnicą, że dane są odczytywane z pobranego pliku Cache, a nie z sieci.

Aby odtworzyć pobraną zawartość, utwórz CacheDataSource.Factory przy użyciu tej samej instancji Cache, która została użyta do pobrania, i wstrzyknij ją do DefaultMediaSourceFactory podczas tworzenia odtwarzacza:

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

Jeśli ta sama instancja odtwarzacza będzie również używana do odtwarzania niepobranych treści, należy skonfigurować CacheDataSource.Factory jako tylko do odczytu, aby uniknąć pobierania tych treści podczas odtwarzania.

Po skonfigurowaniu odtwarzacza za pomocą CacheDataSource.Factory będzie on miał dostęp do pobranej zawartości i będzie mógł ją odtwarzać. Aby odtworzyć pobrany plik, wystarczy przesłać odpowiedni MediaItem do odtwarzacza. MediaItem można uzyskać z Download za pomocą Download.request.toMediaItem lub bezpośrednio z DownloadRequest za pomocą DownloadRequest.toMediaItem.

Konfiguracja MediaSource

W powyższym przykładzie pamięć podręczna pobierania jest dostępna do odtwarzania wszystkich MediaItem. Możesz też udostępnić pamięć podręczną pobierania poszczególnym instancjom MediaSource, które można przekazać bezpośrednio do odtwarzacza:

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

Pobieranie i odtwarzanie strumieni adaptacyjnych

Strumienie adaptacyjne (np. DASH, SmoothStreaming i HLS) zwykle zawierają wiele ścieżek multimedialnych. Często istnieje wiele ścieżek zawierających te same treści w różnej jakości (np. ścieżki wideo w jakości SD, HD i 4K). Mogą również istnieć wiele utworów tego samego typu, ale zawierających różną treść (np. wiele utworów audio w różnych językach).

W przypadku odtwarzania strumieniowego można użyć selektora utworów, aby wybrać, które utwory mają zostać odtworzone. Podobnie, przy pobieraniu można użyć DownloadHelper, aby wybrać, które utwory zostaną pobrane. Typowe zastosowanie DownloadHelper wygląda następująco:

  1. Zbuduj DownloadHelper przy użyciu instancji DownloadHelper.Factory. Przygotuj pomocnika i czekaj na oddzwonienie.

    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. Opcjonalnie możesz sprawdzić domyślnie wybrane ścieżki za pomocą ikon getMappedTrackInfogetTrackSelections oraz wprowadzić zmiany za pomocą ikon clearTrackSelections, replaceTrackSelectionsaddTrackSelection.
  3. Utwórz DownloadRequest dla wybranych utworów, wywołując getDownloadRequest. Żądanie można przekazać do DownloadService w celu dodania pobierania, jak opisano powyżej.
  4. Zwolnij metodę pomocniczą za pomocą release().

Aby odtworzyć pobraną zawartość adaptacyjną, należy skonfigurować odtwarzacz i przekazać odpowiedni parametr MediaItem, jak opisano powyżej.

Podczas tworzenia pliku MediaItem należy ustawić parametr MediaItem.localConfiguration.streamKeys tak, aby odpowiadał parametrom w pliku DownloadRequest. W ten sposób odtwarzacz będzie próbował odtworzyć tylko ten podzbiór utworów, który został pobrany. Użycie Download.request.toMediaItem i DownloadRequest.toMediaItem do zbudowania MediaItem zajmie się tym za Ciebie.