미디어 다운로드

ExoPlayer는 오프라인 재생을 위해 미디어를 다운로드하는 기능을 제공합니다. 대부분의 경우 앱이 백그라운드에 있을 때도 다운로드를 계속하는 것이 좋습니다. 이러한 사용 사례의 경우 앱은 DownloadService를 서브클래스로 분류하고 서비스에 명령어를 전송하여 다운로드를 추가, 삭제, 제어해야 합니다. 다음 다이어그램은 관련된 기본 클래스를 보여줍니다.

미디어 다운로드를 위한 클래스입니다. 화살표 방향은 데이터의 흐름을 나타냅니다.

  • DownloadService: DownloadManager를 래핑하고 명령어를 전달합니다. 이 서비스를 사용하면 앱이 백그라운드에 있을 때도 DownloadManager를 계속 실행할 수 있습니다.
  • DownloadManager: 여러 다운로드를 관리하고 DownloadIndex에서 상태를 로드(및 저장)하고 네트워크 연결과 같은 요구사항에 따라 다운로드를 시작하고 중지합니다. 콘텐츠를 다운로드하기 위해 관리자는 일반적으로 HttpDataSource에서 다운로드 중인 데이터를 읽고 Cache에 씁니다.
  • DownloadIndex: 다운로드 상태를 유지합니다.

DownloadService 만들기

DownloadService를 만들려면 서브클래스를 만들고 추상 메서드를 구현합니다.

  • getDownloadManager(): 사용할 DownloadManager를 반환합니다.
  • getScheduler(): 대기 중인 다운로드의 진행 요구사항이 충족되면 서비스를 다시 시작할 수 있는 선택적 Scheduler를 반환합니다. ExoPlayer는 다음과 같은 구현을 제공합니다.
    • PlatformScheduler: JobScheduler (최소 API는 21)를 사용합니다. 앱 권한 요구사항은 PlatformScheduler javadocs를 참고하세요.
    • WorkManagerScheduler: WorkManager를 사용합니다.
  • getForegroundNotification(): 서비스가 포그라운드에서 실행 중일 때 표시할 알림을 반환합니다. DownloadNotificationHelper.buildProgressNotification를 사용하여 기본 스타일로 알림을 만들 수 있습니다.

마지막으로 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>

구체적인 예는 ExoPlayer 데모 앱의 DemoDownloadServiceAndroidManifest.xml를 참고하세요.

DownloadManager 생성

다음 코드 스니펫은 DownloadServicegetDownloadManager()에서 반환할 수 있는 DownloadManager를 인스턴스화하는 방법을 보여줍니다.

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

구체적인 예는 데모 앱의 DemoUtil를 참고하세요.

다운로드 추가

다운로드를 추가하려면 DownloadRequest를 만들어 DownloadService로 전송합니다. 적응형 스트림의 경우 DownloadRequest를 빌드하는 데 도움이 되는 DownloadHelper를 사용합니다. 다음 예는 다운로드 요청을 만드는 방법을 보여줍니다.

Kotlin

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

Java

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

이 예에서 contentId는 콘텐츠의 고유 식별자입니다. 간단한 사례에서는 contentUricontentId로 사용할 수 있는 경우가 많지만 앱은 사용 사례에 가장 적합한 ID 스키마를 자유롭게 사용할 수 있습니다. DownloadRequest.Builder에는 선택적 setter도 있습니다. 예를 들어 setKeySetIdsetData는 각각 앱이 다운로드와 연결할 DRM 및 맞춤 데이터를 설정하는 데 사용할 수 있습니다. 콘텐츠의 MIME 유형을 contentUri에서 콘텐츠 유형을 추론할 수 없는 경우에 관한 힌트로 setMimeType를 사용하여 지정할 수도 있습니다.

생성된 후에는 요청을 DownloadService로 전송하여 다운로드를 추가할 수 있습니다.

Kotlin

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

Java

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

이 예에서 MyDownloadService는 앱의 DownloadService 서브클래스이고 foreground 매개변수는 서비스를 포그라운드에서 시작할지 여부를 제어합니다. 앱이 이미 포그라운드에 있는 경우 foreground 매개변수는 일반적으로 false로 설정되어야 합니다. DownloadService이 실행할 작업이 있다고 판단하면 자체적으로 포그라운드에 배치되기 때문입니다.

다운로드 항목 삭제 중

DownloadService에 삭제 명령어를 전송하여 다운로드를 삭제할 수 있습니다. 여기서 contentId는 삭제할 다운로드를 식별합니다.

Kotlin

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

Java

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

DownloadService.sendRemoveAllDownloads를 사용하여 다운로드한 모든 데이터를 삭제할 수도 있습니다.

다운로드 시작 및 중지

다음 네 가지 조건을 충족하는 경우에만 다운로드가 진행됩니다.

  • 다운로드에 중지 이유가 없습니다.
  • 다운로드가 일시중지되지 않았습니다.
  • 다운로드 진행을 위한 요구사항을 충족합니다. 요구사항은 기기가 유휴 상태이거나 충전기에 연결되어 있어야 하는지 여부뿐만 아니라 허용되는 네트워크 유형에 관한 제약 조건을 지정할 수 있습니다.
  • 최대 동시 다운로드 수를 초과하지 않았습니다.

이러한 모든 조건은 DownloadService에 명령어를 전송하여 제어할 수 있습니다.

다운로드 중지 이유 설정 및 삭제

하나 또는 모든 다운로드가 중지되는 이유를 설정할 수 있습니다.

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은 0이 아닌 값이 될 수 있습니다 (Download.STOP_REASON_NONE = 0는 다운로드가 중지되지 않음을 의미하는 특수 값). 다운로드를 중지하는 이유가 여러 가지인 앱은 서로 다른 값을 사용하여 각 다운로드가 중지된 이유를 추적할 수 있습니다. 모든 다운로드의 중지 이유를 설정하고 삭제하는 것은 단일 다운로드의 중지 이유를 설정하고 삭제하는 것과 같은 방식으로 작동합니다. 단, contentIdnull로 설정해야 합니다.

다운로드의 중지 이유가 0이 아닌 경우 Download.STATE_STOPPED 상태가 됩니다. 중지 이유는 DownloadIndex에서 유지되므로 애플리케이션 프로세스가 종료되었다가 나중에 다시 시작되더라도 유지됩니다.

모든 다운로드 일시중지 및 다시 시작

모든 다운로드를 다음과 같이 일시중지 및 재개할 수 있습니다.

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

다운로드가 일시중지되면 Download.STATE_QUEUED 상태가 됩니다. 중지 이유 설정과 달리 이 방법은 상태 변경을 유지하지 않습니다. DownloadManager의 런타임 상태에만 영향을 미칩니다.

다운로드 진행을 위한 요구사항 설정

Requirements를 사용하여 다운로드를 계속하기 위해 충족해야 하는 제약 조건을 지정할 수 있습니다. 요구사항은 의 예와 같이 DownloadManager를 만들 때 DownloadManager.setRequirements()를 호출하여 설정할 수 있습니다. 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);

요구사항이 충족되지 않아 다운로드를 진행할 수 없는 경우 Download.STATE_QUEUED 상태가 됩니다. DownloadManager.getNotMetRequirements()를 사용하여 충족되지 않은 요구사항을 쿼리할 수 있습니다.

최대 동시 다운로드 수 설정

최대 동시 다운로드 수는 DownloadManager.setMaxParallelDownloads()를 호출하여 설정할 수 있습니다. 이는 위의 예와 같이 일반적으로 DownloadManager를 만들 때 이루어집니다.

최대 병렬 다운로드 수가 이미 진행 중이므로 다운로드를 진행할 수 없는 경우 Download.STATE_QUEUED 상태가 됩니다.

다운로드 쿼리

DownloadManagerDownloadIndex를 쿼리하여 완료 또는 실패한 다운로드를 비롯한 모든 다운로드의 상태를 확인할 수 있습니다. DownloadIndexDownloadManager.getDownloadIndex()를 호출하여 얻을 수 있습니다. 그런 다음 DownloadIndex.getDownloads()를 호출하여 모든 다운로드에서 반복되는 커서를 가져올 수 있습니다. 또는 DownloadIndex.getDownload()를 호출하여 단일 다운로드 상태를 쿼리할 수 있습니다.

DownloadManager는 현재 (예: 완료되지 않음 또는 실패) 다운로드 상태만 반환하는 DownloadManager.getCurrentDownloads()도 제공합니다. 이 메서드는 현재 다운로드의 진행률과 상태를 표시하는 알림 및 기타 UI 구성요소를 업데이트하는 데 유용합니다.

오프라인 저장 음악 듣기

DownloadManager에 리스너를 추가하여 현재 다운로드 상태가 변경되면 알림을 받을 수 있습니다.

Kotlin

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

Java

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

구체적인 예는 데모 앱의 DownloadTracker 클래스에서 DownloadManagerListener를 참고하세요.

오프라인 저장한 콘텐츠 재생 중

다운로드한 콘텐츠의 재생은 온라인 콘텐츠 재생과 비슷하지만, 데이터가 네트워크가 아닌 다운로드 Cache에서 데이터를 읽어오는 점만 다릅니다.

다운로드한 콘텐츠를 재생하려면 다운로드에 사용된 것과 동일한 Cache 인스턴스를 사용하여 CacheDataSource.Factory를 만들고 플레이어를 빌드할 때 DefaultMediaSourceFactory에 삽입합니다.

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

동일한 플레이어 인스턴스를 사용하여 다운로드되지 않은 콘텐츠를 재생하는 경우 재생 중에도 콘텐츠가 다운로드되지 않도록 CacheDataSource.Factory를 읽기 전용으로 구성해야 합니다.

플레이어가 CacheDataSource.Factory로 구성되면 재생할 수 있도록 다운로드한 콘텐츠에 액세스할 수 있습니다. 그러면 다운로드를 재생하는 것은 플레이어에 상응하는 MediaItem를 전달하기만 하면 됩니다. MediaItemDownload.request.toMediaItem를 사용하여 Download에서, 또는 DownloadRequest.toMediaItem를 사용하여 DownloadRequest에서 직접 가져올 수 있습니다.

MediaSource 구성

위의 예에서는 다운로드 캐시를 모든 MediaItem의 재생에 사용할 수 있도록 합니다. 플레이어에 직접 전달할 수 있는 개별 MediaSource 인스턴스에 다운로드 캐시를 사용할 수도 있습니다.

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

적응형 스트림 다운로드 및 재생하기

적응형 스트림 (예: DASH, SmoothStreaming, HLS)에는 일반적으로 여러 미디어 트랙이 포함됩니다. 동일한 콘텐츠가 서로 다른 품질로 포함된 트랙 (예: SD, HD, 4K 동영상 트랙)이 여러 개 있는 경우가 많습니다. 다른 콘텐츠를 포함하는 동일한 유형의 여러 트랙 (예: 서로 다른 언어의 여러 오디오 트랙)이 있을 수도 있습니다.

스트리밍 재생의 경우 트랙 선택기를 사용하여 재생할 트랙을 선택할 수 있습니다. 마찬가지로 다운로드의 경우 DownloadHelper를 사용하여 다운로드할 트랙을 선택할 수 있습니다. DownloadHelper의 일반적인 사용법은 다음과 같습니다.

  1. DownloadHelper.forMediaItem 메서드 중 하나를 사용하여 DownloadHelper를 빌드합니다. 도우미를 준비하고 콜백을 기다립니다.

    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. 원하는 경우 getMappedTrackInfogetTrackSelections를 사용하여 선택된 기본 트랙을 검사하고 clearTrackSelections, replaceTrackSelections, addTrackSelection를 사용하여 조정합니다.
  3. getDownloadRequest를 호출하여 선택한 트랙의 DownloadRequest를 만듭니다. 위에서 설명한 대로 요청을 DownloadService에 전달하여 다운로드를 추가할 수 있습니다.
  4. release()를 사용하여 도우미를 해제합니다.

다운로드한 적응형 콘텐츠를 재생하려면 위에서 설명한 대로 플레이어를 구성하고 상응하는 MediaItem를 전달해야 합니다.

MediaItem를 빌드할 때 플레이어가 다운로드된 트랙의 하위 집합만 재생하려고 하도록 MediaItem.localConfiguration.streamKeysDownloadRequest의 항목과 일치하도록 설정해야 합니다. Download.request.toMediaItemDownloadRequest.toMediaItem를 사용하여 MediaItem를 빌드하면 이 작업이 자동으로 처리됩니다.