Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public data class StreamCacheConfig(
* enabled, replaying or seeking within a previously watched video reuses cached byte ranges
* instead of re-downloading from the CDN.
*
* Cache entries are keyed by URL path only; query parameters are stripped. URLs that differ
* only in their query string share the same cache entry.
*
* @param maxSizeBytes Soft cap on cache size; LRU eviction kicks in once exceeded. Files larger
* than this cap are not effectively cached. Size [maxSizeBytes] to comfortably exceed the
* largest expected video.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,18 @@ public class VideoMediaCache private constructor(
}

/**
* Returns a [VideoMediaCache] backed by the [SimpleCache] at [cacheDir]. If an instance
* for that absolute directory path already exists in this process, that instance is
* returned and [config] is ignored beyond the first call; this prevents a second
* [SimpleCache] from being constructed against the same directory (which would throw).
* Returns a [VideoMediaCache] backed by the [SimpleCache] at [cacheDir], or `null` if
* [SimpleCache] construction fails (e.g. a stale directory lock left by a prior crash).
* If an instance for that absolute directory path already exists in this process, that
* instance is returned and [config] is ignored beyond the first call; this prevents a
* second [SimpleCache] from being constructed against the same directory (which would throw).
*
* @param appContext Application context used to construct the [StandaloneDatabaseProvider].
* @param cacheDir Directory that backs the [SimpleCache]. Created if it does not exist.
* @param config Cache configuration. Honored only on the first call for [cacheDir].
*/
@JvmStatic
public fun create(appContext: Context, cacheDir: File, config: VideoCacheConfig): VideoMediaCache =
public fun create(appContext: Context, cacheDir: File, config: VideoCacheConfig): VideoMediaCache? =
synchronized(instances) {
cacheDir.mkdirs()
val key = cacheDir.absolutePath
Expand All @@ -146,13 +147,18 @@ public class VideoMediaCache private constructor(
}
return@synchronized existing
}
val dbProvider = StandaloneDatabaseProvider(appContext)
val simpleCache = SimpleCache(
cacheDir,
LeastRecentlyUsedCacheEvictor(config.maxSizeBytes),
dbProvider,
)
VideoMediaCache(simpleCache, dbProvider, key).also { instances[key] = it }
try {
val dbProvider = StandaloneDatabaseProvider(appContext)
val simpleCache = SimpleCache(
cacheDir,
LeastRecentlyUsedCacheEvictor(config.maxSizeBytes),
dbProvider,
)
VideoMediaCache(simpleCache, dbProvider, key).also { instances[key] = it }
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
logger.e(e) { "[create] Failed to construct SimpleCache at '$key'; video caching disabled." }
null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ internal class StreamMediaDataSourceCacheIntegrationTest {
@Before
fun setUp() {
cacheDir.deleteRecursively()
cache = VideoMediaCache.create(context, cacheDir, VideoCacheConfig())
cache = VideoMediaCache.create(context, cacheDir, VideoCacheConfig())!!
}

@After
Expand Down Expand Up @@ -230,7 +230,7 @@ internal class StreamMediaDataSourceCacheIntegrationTest {
assertFalse("clearAll should report nothing was cleared on an empty registry", VideoMediaCache.clearAll())

// Recreate so tearDown() releases a live instance and unlocks the directory for the next test.
cache = VideoMediaCache.create(context, cacheDir, VideoCacheConfig())
cache = VideoMediaCache.create(context, cacheDir, VideoCacheConfig())!!
}

/**
Expand All @@ -246,7 +246,7 @@ internal class StreamMediaDataSourceCacheIntegrationTest {
context,
evictionDir,
VideoCacheConfig(maxSizeBytes = 2 * TOTAL_LENGTH),
)
)!!
try {
val upstream = RecordingDataSourceFactory()
val factory = VideoCacheDataSourceFactory(evictionCache, upstream)
Expand All @@ -255,7 +255,8 @@ internal class StreamMediaDataSourceCacheIntegrationTest {
// Space the operations out so each span's `lastTouchTimestamp` lands in a distinct
// millisecond; Media3's LeastRecentlyUsedCacheEvictor uses `System.currentTimeMillis()`
// and falls back to alphabetical key order on ties, which would pick A over B under
// load and make the assertions non-deterministic.
// load and make the assertions non-deterministic. The sleeps are wall-clock dependent;
// if this test becomes flaky on a slow runner, increase SPAN_SPACING_MS.
readFully(factory.createDataSource(), DataSpec(Uri.parse(VIDEO_A_URL)))
Thread.sleep(SPAN_SPACING_MS)
readFully(factory.createDataSource(), DataSpec(Uri.parse(VIDEO_B_URL)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class VideoMediaCacheTest {
@Before
fun setUp() {
cacheDir.deleteRecursively()
cache = VideoMediaCache.create(context, cacheDir, VideoCacheConfig())
cache = VideoMediaCache.create(context, cacheDir, VideoCacheConfig())!!
}

@After
Expand Down Expand Up @@ -84,7 +84,7 @@ internal class VideoMediaCacheTest {
assertTrue("Expected a different instance after release", recreated !== cache)
// Reassign so @After releases the recreated instance and Media3's SimpleCache
// unlocks the directory; otherwise the next test's setUp would throw.
cache = recreated
cache = recreated!!
}

private companion object {
Expand Down
Loading