-
-
Save savekirk/a5a0eccbb805f64ae6170fbca6c61893 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.media3.common.C | |
import androidx.media3.common.MediaItem | |
import androidx.media3.common.util.UnstableApi | |
import androidx.media3.common.util.Util | |
import androidx.media3.datasource.cache.CacheDataSource | |
import androidx.media3.exoplayer.offline.DefaultDownloaderFactory | |
import androidx.media3.exoplayer.offline.DownloadRequest | |
import androidx.media3.exoplayer.offline.Downloader | |
import java.util.concurrent.Executor | |
@UnstableApi | |
class CustomDownloaderFactory( | |
private val cacheDataSourceFactory: CacheDataSource.Factory, | |
private val executor: Executor | |
) : | |
DefaultDownloaderFactory(cacheDataSourceFactory, executor) { | |
override fun createDownloader(request: DownloadRequest): Downloader { | |
val contentType = | |
Util.inferContentTypeForUriAndMimeType(request.uri, request.mimeType) | |
return when (contentType) { | |
C.CONTENT_TYPE_OTHER -> CustomProgressiveDownloader( | |
MediaItem.Builder() | |
.setUri(request.uri) | |
.setCustomCacheKey(request.customCacheKey) | |
.build(), | |
cacheDataSourceFactory, | |
executor | |
) | |
else -> super.createDownloader(request) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2017 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
import androidx.media3.common.C | |
import androidx.media3.common.MediaItem | |
import androidx.media3.common.PriorityTaskManager | |
import androidx.media3.common.util.Assertions | |
import androidx.media3.common.util.RunnableFutureTask | |
import androidx.media3.common.util.UnstableApi | |
import androidx.media3.common.util.Util | |
import androidx.media3.datasource.DataSpec | |
import androidx.media3.datasource.cache.CacheDataSource | |
import androidx.media3.datasource.cache.CacheWriter | |
import androidx.media3.exoplayer.offline.Downloader | |
import java.io.IOException | |
import java.util.concurrent.ExecutionException | |
import java.util.concurrent.Executor | |
/** A downloader for progressive media streams. */ | |
@UnstableApi | |
class CustomProgressiveDownloader @JvmOverloads constructor( | |
mediaItem: MediaItem, | |
cacheDataSourceFactory: CacheDataSource.Factory, | |
executor: Executor? = Executor { obj: Runnable -> obj.run() } | |
) : | |
Downloader { | |
private val executor: Executor | |
private val dataSpec: DataSpec | |
private val dataSource: CacheDataSource | |
private val cacheWriter: CacheWriter | |
private val priorityTaskManager: PriorityTaskManager? | |
private var progressListener: Downloader.ProgressListener? = null | |
@Volatile | |
private var downloadRunnable: RunnableFutureTask<Void?, IOException?>? = null | |
@Volatile | |
private var isCanceled = false | |
/** | |
* Creates a new instance. | |
* | |
* @param mediaItem The media item with a uri to the stream to be downloaded. | |
* @param cacheDataSourceFactory A [CacheDataSource.Factory] for the cache into which the | |
* download will be written. | |
* @param executor An [Executor] used to make requests for the media being downloaded. In | |
* the future, providing an [Executor] that uses multiple threads may speed up the | |
* download by allowing parts of it to be executed in parallel. | |
*/ | |
/** | |
* Creates a new instance. | |
* | |
* @param mediaItem The media item with a uri to the stream to be downloaded. | |
* @param cacheDataSourceFactory A [CacheDataSource.Factory] for the cache into which the | |
* download will be written. | |
*/ | |
init { | |
this.executor = Assertions.checkNotNull(executor) | |
Assertions.checkNotNull(mediaItem.localConfiguration) | |
dataSpec = DataSpec.Builder() | |
.setUri(mediaItem.localConfiguration!!.uri) | |
.setKey(mediaItem.localConfiguration!!.customCacheKey) | |
.build() | |
dataSource = cacheDataSourceFactory.createDataSourceForDownloading() | |
val progressListener = | |
CacheWriter.ProgressListener { contentLength: Long, bytesCached: Long, newBytesCached: Long -> | |
onProgress( | |
contentLength, | |
bytesCached, | |
newBytesCached | |
) | |
} | |
cacheWriter = CacheWriter(dataSource, dataSpec, /* temporaryBuffer= */null, progressListener) | |
priorityTaskManager = cacheDataSourceFactory.upstreamPriorityTaskManager | |
} | |
@Throws(IOException::class, InterruptedException::class) | |
override fun download(progressListener: Downloader.ProgressListener?) { | |
this.progressListener = progressListener | |
priorityTaskManager?.add(C.PRIORITY_DOWNLOAD) | |
try { | |
var finished = false | |
while (!finished && !isCanceled) { | |
// Recreate downloadRunnable on each loop iteration to avoid rethrowing a previous error. | |
downloadRunnable = object : RunnableFutureTask<Void?, IOException?>() { | |
@Throws(IOException::class) | |
override fun doWork(): Void? { | |
cacheWriter.cache() | |
return null | |
} | |
override fun cancelWork() { | |
cacheWriter.cancel() | |
} | |
} | |
priorityTaskManager?.proceed(C.PRIORITY_DOWNLOAD) | |
executor.execute(downloadRunnable) | |
try { | |
downloadRunnable!!.get() | |
finished = true | |
} catch (e: ExecutionException) { | |
val cause = Assertions.checkNotNull(e.cause) | |
if (cause is PriorityTaskManager.PriorityTooLowException) { | |
// The next loop iteration will block until the task is able to proceed. | |
} else if (cause is IOException) { | |
throw cause | |
} else { | |
// The cause must be an uncaught Throwable type. | |
Util.sneakyThrow(cause) | |
} | |
} | |
} | |
} finally { | |
// If the main download thread was interrupted as part of cancelation, then it's possible that | |
// the runnable is still doing work. We need to wait until it's finished before returning. | |
downloadRunnable?.blockUntilFinished() | |
priorityTaskManager?.remove(C.PRIORITY_DOWNLOAD) | |
} | |
} | |
override fun cancel() { | |
isCanceled = true | |
val downloadRunnable = downloadRunnable | |
downloadRunnable?.cancel( /* interruptIfRunning= */true) | |
} | |
override fun remove() { | |
dataSource.cache.removeResource(dataSource.cacheKeyFactory.buildCacheKey(dataSpec)) | |
} | |
private fun onProgress(contentLength: Long, bytesCached: Long, newBytesCached: Long) { | |
if (progressListener == null) { | |
return | |
} | |
val percentDownloaded = | |
if (contentLength == C.LENGTH_UNSET.toLong() || contentLength == 0L) C.PERCENTAGE_UNSET.toFloat() else bytesCached * 100f / contentLength | |
progressListener!!.onProgress(contentLength, bytesCached, percentDownloaded) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
getDownloadManager(context).addListener(object : DownloadManager.Listener { | |
override fun onDownloadChanged( | |
downloadManager: DownloadManager, download: Download, finalException: Exception? | |
) { | |
when (download.state) { | |
Download.STATE_COMPLETED -> { | |
resumedDownloads.remove(download.request.id) | |
val span = getDownloadCache(context).getCachedSpans(download.request.id).firstOrNull() | |
if (span?.isCached == true && span.file !== null) { | |
val destination = File( | |
context.externalCacheDir!!.absolutePath, "<directory>/${UUID.randomUUID()}.mp4" | |
) | |
// Downloaded media is created with .exo format. | |
// Move it to .mp4 format and delete original | |
span.file!!.copyTo(destination, overwrite = true) | |
downloadManager.removeDownload(download.request.id) | |
} else { | |
onError("Error downloading media") | |
} | |
} | |
else -> { | |
// We're not interested in the other states. | |
} | |
} | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DownloadManager( | |
context, | |
DefaultDownloadIndex(getDatabaseProvider(context)), | |
CustomDownloaderFactory( | |
CacheDataSource.Factory() | |
.setCache(getDownloadCache(context)) | |
.setUpstreamDataSourceFactory(getHttpDataSourceFactory(context)), | |
Executors.newFixedThreadPool(THREADS_COUNT) | |
) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment