Skip to content

Instantly share code, notes, and snippets.

@savekirk
Last active June 4, 2023 18:42
Show Gist options
  • Save savekirk/a5a0eccbb805f64ae6170fbca6c61893 to your computer and use it in GitHub Desktop.
Save savekirk/a5a0eccbb805f64ae6170fbca6c61893 to your computer and use it in GitHub Desktop.
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)
}
}
}
/*
* 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)
}
}
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.
}
}
}
})
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