diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt index 449f1c5b231..4147ad13845 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt @@ -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. diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt index 90574907655..eced80c3de8 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt @@ -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 @@ -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 + } } } } diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/StreamMediaDataSourceCacheIntegrationTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/StreamMediaDataSourceCacheIntegrationTest.kt index 1055f9a8a5a..438f6f2b89e 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/StreamMediaDataSourceCacheIntegrationTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/StreamMediaDataSourceCacheIntegrationTest.kt @@ -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 @@ -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())!! } /** @@ -246,7 +246,7 @@ internal class StreamMediaDataSourceCacheIntegrationTest { context, evictionDir, VideoCacheConfig(maxSizeBytes = 2 * TOTAL_LENGTH), - ) + )!! try { val upstream = RecordingDataSourceFactory() val factory = VideoCacheDataSourceFactory(evictionCache, upstream) @@ -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))) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/VideoMediaCacheTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/VideoMediaCacheTest.kt index 9c6d9251cc1..a122add0e03 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/VideoMediaCacheTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/VideoMediaCacheTest.kt @@ -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 @@ -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 {