Skip to content

Add opt-in video disk cache for ExoPlayer playback#6542

Open
VelikovPetar wants to merge 3 commits into
developfrom
port/v6-to-develop/opt-in-video-disk-cache
Open

Add opt-in video disk cache for ExoPlayer playback#6542
VelikovPetar wants to merge 3 commits into
developfrom
port/v6-to-develop/opt-in-video-disk-cache

Conversation

@VelikovPetar

@VelikovPetar VelikovPetar commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Goal

Resolves: AND-1275

Media attachments currently re-stream from the CDN on every replay, seek, or scroll-back — wasting customer bandwidth and forcing users to wait through buffering on content they have already watched. This PR introduces an opt-in on-disk media cache so previously watched videos (and other ExoPlayer-driven media, including audio) replay from local bytes.

Port of v6 PR #6533 (squash commit eadde4d) to develop. This was a manual port, not a clean cherry-pick, because:

  • ChatClient.kt has diverged significantly between v6 and develop.
  • The compose media-gallery player logic was refactored on develop (the v6 change to MediaGalleryPreviewScreen.kt maps to MediaGalleryPlayerState.kt here).

Intentional API difference from the v6 PR: the cache is configured via a new ChatClientConfig.cacheConfig property (mirroring the existing messageLimitConfig pattern) rather than a ChatClient.Builder.cacheConfig(...) builder method. The v6 PR's incidental MediaPreviewActivity.getIntent @JvmOverloads churn was dropped as unrelated to caching.

Implementation

Public API

  • StreamCacheConfig(video: VideoCacheConfig? = null) — aggregate config; future cache types can be added as additional nullable properties without breaking the call site.
  • VideoCacheConfig(maxSizeBytes: Long) — soft cap on cache size; defaults to 150 MB, LRU eviction once exceeded. Rejects non-positive sizes at construction.
  • ChatClientConfig.cacheConfig: StreamCacheConfig — single entry point, off by default (existing apps see no behavior change).
val config = ChatClientConfig(
    cacheConfig = StreamCacheConfig(video = VideoCacheConfig()),
)
ChatClient.Builder("api-key", context)
    .config(config)
    .build()

The config is named VideoCacheConfig because video was the original motivation, but a single cache now backs all ExoPlayer media.

Internals

  • VideoMediaCache owns a Media3 SimpleCache + StandaloneDatabaseProvider. A process-wide registry keyed by the cache directory's absolute path prevents the "two SimpleCache instances against the same directory" crash if a customer rebuilds ChatClient in the same process.
  • VideoCacheDataSourceFactory wraps Media3's CacheDataSource.Factory and composes outside any custom CDN — on a hit the bytes are served from disk and the customer's CDN.fileRequest is not consulted; on a miss the CDN signs the URL just-in-time. The cache key strips URL query parameters, so rotating pre-signed signature/query values still resolve to the same cached entry.
  • Wired into all four Media3 playback sites, each passing the shared videoCache:
    • Compose: StreamMediaPlayerContent (createPlayer)
    • UI components: AttachmentMediaActivity, AttachmentGalleryVideoPageFragment
    • Audio playback: StreamAudioPlayer pipeline built in ChatClient.Builder.build()
  • Cache clearing is in-place: clearCacheAndTemporaryFiles(context) clears every live cache in the process via VideoMediaCache.clearAll() (which empties the SimpleCache while keeping it alive and its directory lock held), and only falls back to deleting the cache directory when no live cache owns it.
  • Player.prepareIfNeeded(...) skips re-preparing an already-loaded URL, avoiding a second concurrent load that could leave a cached file's head uncached.

UI Changes

No UI changes.

Testing

Automated — ran locally, all green:

  • StreamMediaDataSourceCacheIntegrationTest{custom CDN, no CDN} × {hit, miss} matrix, rotated-query cache hit, LRU eviction, in-place clear, and clearAll.
  • VideoMediaCacheTest — directory creation, release() idempotency, per-directory instance registry.
  • VideoCacheConfigTest — default/positive/zero/negative maxSizeBytes validation.
  • StreamFileManagerTest / ChatClientCacheAndTemporaryFilesTest — cache directory lookup and cleanup wiring.
  • PrepareIfNeededTest (compose) — redundant-prepare skipping.
./gradlew :stream-chat-android-client:apiDump :stream-chat-android-compose:apiDump :stream-chat-android-ui-components:apiDump
./gradlew :stream-chat-android-client:detekt :stream-chat-android-compose:detekt :stream-chat-android-ui-components:detekt
./gradlew :stream-chat-android-client:testDebugUnitTest --tests "io.getstream.chat.android.client.cache.*" \
    --tests "io.getstream.chat.android.client.ChatClientCacheAndTemporaryFilesTest"
./gradlew :stream-chat-android-compose:testDebugUnitTest \
    --tests "io.getstream.chat.android.compose.ui.attachments.preview.internal.PrepareIfNeededTest"

Manual smoke

  1. Enable via ChatClientConfig(cacheConfig = StreamCacheConfig(video = VideoCacheConfig())).
  2. Send a video attachment, scroll away, scroll back, tap to play. Close, then tap the same video again.
  3. Expected: the second play has no network traffic; the cache directory under <cacheDir>/stream_video_cache/ contains files.
  4. Kill/restart the app; the same media still serves from cache.
  5. Trigger clearCacheAndTemporaryFiles(context); cached bytes are removed and playback re-fetches, with the cache staying functional afterwards.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional on-disk video caching for streamed media, helping video attachments load more smoothly and reducing repeated network fetches.
    • Media gallery and attachment playback now use the new video cache automatically when available.
  • Bug Fixes

    • Improved cache cleanup so video cache data is cleared correctly alongside other temporary files.
    • Playback setup now avoids unnecessary reloading when the same media is already selected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VelikovPetar VelikovPetar added the pr:new-feature New feature label Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.93 MB 5.93 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.20 MB 11.20 MB 0.00 MB 🟢
stream-chat-android-compose 12.66 MB 12.67 MB 0.00 MB 🟢

@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@VelikovPetar VelikovPetar marked this pull request as ready for review July 6, 2026 13:34
@VelikovPetar VelikovPetar requested a review from a team as a code owner July 6, 2026 13:34
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR introduces an opt-in on-disk video cache for streamed media. It adds StreamCacheConfig/VideoCacheConfig types wired through ChatClientConfig, a VideoMediaCache backed by Media3 SimpleCache, integration into StreamMediaDataSource/ChatClient, cache-clearing support, and player wiring updates (including a Player.prepareIfNeeded extension) in Compose and UI-components video playback.

Changes

Video Disk Cache Feature

Layer / File(s) Summary
Cache config types and ChatClientConfig wiring
stream-chat-android-client/src/main/.../cache/StreamCacheConfig.kt, .../api/ChatClientConfig.kt, stream-chat-android-client/api/stream-chat-android-client.api, .../cache/VideoCacheConfigTest.kt
Adds StreamCacheConfig/VideoCacheConfig data classes with a DEFAULT_MAX_SIZE_BYTES constant and validation, and exposes a new cacheConfig property on ChatClientConfig with updated public API surface.
VideoMediaCache implementation
.../cache/internal/VideoMediaCache.kt, .../cache/internal/VideoMediaCacheTest.kt
Implements a process-wide SimpleCache registry keyed by directory, with create, clear, release, and clearAll lifecycle operations and instance reuse.
Cache-aware data source and CDN wiring
.../cache/internal/VideoCacheDataSourceFactory.kt, .../cdn/internal/StreamMediaDataSource.kt, .../cdn/internal/CDNDataSourceFactory.kt, .../cache/internal/StreamMediaDataSourceCacheIntegrationTest.kt, .../cdn/internal/CDNDataSourceTest.kt
Adds VideoCacheDataSourceFactory wrapping CacheDataSource with query-agnostic cache keys, extends StreamMediaDataSource.factory to accept an optional videoCache parameter, switches @UnstableApi usages to @OptIn, and adds integration tests for cache hit/miss/eviction behavior.
ChatClient integration and cache clearing
ChatClient.kt, .../internal/file/StreamFileManager.kt, MockedChatClientTest.kt, ChatClientCacheAndTemporaryFilesTest.kt, StreamFileManagerTest.kt
Adds ChatClient.videoCache, constructs it from cacheConfig.video in the builder, extends clearCacheAndTemporaryFiles to clear video cache, adds StreamFileManager.clearVideoCache, and updates tests/mocks.
Player integration
MediaGalleryPlayerState.kt, StreamMediaPlayerContent.kt, PrepareIfNeededTest.kt, AttachmentMediaActivity.kt, AttachmentGalleryVideoPageFragment.kt
Adds Player.prepareIfNeeded extension to avoid redundant media preparation and updates Compose/UI-components media players to pass videoCache into StreamMediaDataSource.factory.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • GetStream/stream-chat-android#6533: Implements the same on-disk video caching feature, including StreamCacheConfig/VideoCacheConfig, ChatClient.videoCache, and StreamMediaDataSource.factory cache wiring.

Suggested reviewers: gpunto

Poem

🐇 A rabbit hops to fetch a clip,
No need to CDN-sign each trip —
SimpleCache keeps bytes tucked tight,
prepareIfNeeded skips the fight,
Video streams now cached and quick,
Hop, hop, hooray — no buffer trick!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding an opt-in video disk cache for ExoPlayer playback.
Description check ✅ Passed The description covers Goal, Implementation, UI Changes, and Testing with commands and smoke steps, so it is mostly complete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch port/v6-to-develop/opt-in-video-disk-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt (1)

73-108: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid process-wide clearAll() as the signal for deleting this directory.
VideoMediaCache.clearAll() returns true for any live cache in the JVM, even one owned by another test. That can skip fileManager.clearVideoCache(context) here and leave videoCache behind, making this assertion order-dependent. Make the delete decision based on whether this directory is actually owned, not whether some other VideoMediaCache exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt`
around lines 73 - 108, The cache cleanup test is using the global
VideoMediaCache clearAll() state to decide whether this specific directory
should be deleted, which makes the assertion dependent on other tests. Update
the cleanup logic around chatClient.clearCacheAndTemporaryFiles and
fileManager.clearVideoCache(context) so it checks ownership of the current
videoCache directory instead of any live cache instance, and ensure the test
always verifies deletion of the directory created in this test.
🧹 Nitpick comments (2)
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoCacheDataSourceFactory.kt (1)

40-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider interface delegation for DataSource.Factory.

Static analysis flags this class for interface delegation via by. cacheKeyFor only touches dataSpec (no instance state), so it could be extracted to a top-level/private function, letting delegate be delegated directly in the class header.

♻️ Proposed refactor
+private fun cacheKeyFor(dataSpec: DataSpec): String =
+    dataSpec.key ?: dataSpec.uri.buildUpon().clearQuery().build().toString()
+
 `@OptIn`(UnstableApi::class)
 internal class VideoCacheDataSourceFactory(
     videoCache: VideoMediaCache,
     upstreamFactory: DataSource.Factory,
-) : DataSource.Factory {
-
-    private val delegate: DataSource.Factory = CacheDataSource.Factory()
-        .setCache(videoCache.cache)
-        .setUpstreamDataSourceFactory(upstreamFactory)
-        .setCacheKeyFactory(::cacheKeyFor)
-        .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
-
-    override fun createDataSource(): DataSource = delegate.createDataSource()
-
-    /**
-     * Returns the cache key for [dataSpec]. Strips the URI's query so rotating signature or expiry
-     * parameters on the same path land on the same cache entry; a caller-supplied [DataSpec.key] is
-     * honoured when present.
-     */
-    `@OptIn`(UnstableApi::class)
-    private fun cacheKeyFor(dataSpec: DataSpec): String =
-        dataSpec.key ?: dataSpec.uri.buildUpon().clearQuery().build().toString()
-}
+) : DataSource.Factory by CacheDataSource.Factory()
+    .setCache(videoCache.cache)
+    .setUpstreamDataSourceFactory(upstreamFactory)
+    .setCacheKeyFactory(::cacheKeyFor)
+    .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoCacheDataSourceFactory.kt`
around lines 40 - 61, The VideoCacheDataSourceFactory class can be simplified by
using interface delegation for DataSource.Factory instead of manually forwarding
createDataSource through a delegate property. Move cacheKeyFor out of the class
as a private top-level helper since it only depends on DataSpec, then delegate
DataSource.Factory directly in the class declaration and keep the cache setup
logic intact.

Source: Linters/SAST tools

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt (1)

56-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add video cache cleanup to tearDown().

Unlike stream/image/timestamped caches, clearAllCache intentionally never touches the video cache directory (per StreamFileManager.clearAllCache). This test creates a video cache dir but relies solely on the in-test clearCacheAndTemporaryFiles call to remove it; tearDown() provides no fallback if that assertion ever fails, so a video cache directory could leak into subsequent test runs.

♻️ Proposed defensive cleanup
     `@After`
     fun tearDown() {
         // Clean up all cache and external storage after each test
         streamFileManager.clearAllCache(context)
         streamFileManager.clearExternalStorage(context)
+        streamFileManager.clearVideoCache(context)
     }

Also applies to: 73-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt`
around lines 56 - 61, The tearDown cleanup in
ChatClientCacheAndTemporaryFilesTest does not remove the video cache directory
because StreamFileManager.clearAllCache intentionally skips it, so add explicit
video cache cleanup alongside the existing clearAllCache and
clearExternalStorage calls. Update the test’s tearDown() to invoke the
appropriate StreamFileManager cleanup for the video cache path so it always gets
removed even if the in-test clearCacheAndTemporaryFiles assertion fails. Apply
the same defensive cleanup to the other referenced test block as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt`:
- Around line 137-156: The VideoMediaCache.create(...) path currently constructs
SimpleCache directly, which can throw and abort
ChatClient.Builder.internalBuild() when the cache dir is locked or unusable.
Wrap the SimpleCache initialization in create(...) with failure handling, and on
error fall back to returning no video cache instead of propagating the
exception. Keep the guard localized to VideoMediaCache.create and preserve the
existing reuse behavior in the instances map when cache creation succeeds.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt`:
- Around line 1517-1532: The video cache cleanup in ChatClient should not decide
between clearing live caches and deleting the directory in two separate steps.
Update the cache removal flow around VideoMediaCache.clearAll() so the “clear
live caches or delete on disk” decision is handled atomically under the registry
lock, and remove the fallback delete from ChatClient’s cache-clearing path. Use
the existing VideoMediaCache and fileManager.clearVideoCache(context) symbols to
locate the affected logic and route the fallback through a single synchronized
API that prevents a newly created cache from being deleted mid-flight.

---

Outside diff comments:
In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt`:
- Around line 73-108: The cache cleanup test is using the global VideoMediaCache
clearAll() state to decide whether this specific directory should be deleted,
which makes the assertion dependent on other tests. Update the cleanup logic
around chatClient.clearCacheAndTemporaryFiles and
fileManager.clearVideoCache(context) so it checks ownership of the current
videoCache directory instead of any live cache instance, and ensure the test
always verifies deletion of the directory created in this test.

---

Nitpick comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoCacheDataSourceFactory.kt`:
- Around line 40-61: The VideoCacheDataSourceFactory class can be simplified by
using interface delegation for DataSource.Factory instead of manually forwarding
createDataSource through a delegate property. Move cacheKeyFor out of the class
as a private top-level helper since it only depends on DataSpec, then delegate
DataSource.Factory directly in the class declaration and keep the cache setup
logic intact.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt`:
- Around line 56-61: The tearDown cleanup in
ChatClientCacheAndTemporaryFilesTest does not remove the video cache directory
because StreamFileManager.clearAllCache intentionally skips it, so add explicit
video cache cleanup alongside the existing clearAllCache and
clearExternalStorage calls. Update the test’s tearDown() to invoke the
appropriate StreamFileManager cleanup for the video cache path so it always gets
removed even if the in-test clearCacheAndTemporaryFiles assertion fails. Apply
the same defensive cleanup to the other referenced test block as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bbed4cb6-da81-49eb-a1d0-4ba2f86e3bd9

📥 Commits

Reviewing files that changed from the base of the PR and between 48f5ab2 and af32c44.

📒 Files selected for processing (21)
  • stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/MockedChatClientTest.kt
  • stream-chat-android-client/api/stream-chat-android-client.api
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatClientConfig.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoCacheDataSourceFactory.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/CDNDataSourceFactory.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/StreamMediaDataSource.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientCacheAndTemporaryFilesTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/VideoCacheConfigTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/StreamMediaDataSourceCacheIntegrationTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cache/internal/VideoMediaCacheTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/cdn/internal/CDNDataSourceTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/file/StreamFileManagerTest.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPlayerState.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/internal/PrepareIfNeededTest.kt
  • stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/gallery/AttachmentMediaActivity.kt
  • stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/gallery/internal/AttachmentGalleryVideoPageFragment.kt

Comment on lines +137 to +156
@JvmStatic
public fun create(appContext: Context, cacheDir: File, config: VideoCacheConfig): VideoMediaCache =
synchronized(instances) {
cacheDir.mkdirs()
val key = cacheDir.absolutePath
instances[key]?.let { existing ->
logger.w {
"[create] Reusing existing VideoMediaCache for '$key'; " +
"additional VideoCacheConfig values are ignored."
}
return@synchronized existing
}
val dbProvider = StandaloneDatabaseProvider(appContext)
val simpleCache = SimpleCache(
cacheDir,
LeastRecentlyUsedCacheEvictor(config.maxSizeBytes),
dbProvider,
)
VideoMediaCache(simpleCache, dbProvider, key).also { instances[key] = it }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Can androidx.media3 SimpleCache constructor throw an exception if the cache directory lock is stale or corrupted?

💡 Result:

Yes, the androidx.media3 SimpleCache constructor can throw an exception if the cache directory is already locked by another SimpleCache instance [1][2]. Specifically, if you attempt to instantiate SimpleCache for a directory that is already in use, the constructor will throw a java.lang.IllegalStateException with a message indicating that another SimpleCache instance is already using that folder [3]. This mechanism ensures that only one instance of SimpleCache manages a given directory at any time to prevent corruption [1][2]. Regarding stale or corrupted locks specifically: - If a previous SimpleCache instance was not properly released (e.g., due to an application crash or improper shutdown), the lock on the directory may persist, causing subsequent attempts to instantiate a new SimpleCache in that same directory to fail with the aforementioned IllegalStateException [3]. - The API provides a static method, SimpleCache.isCacheFolderLocked(File cacheFolder), which allows you to check if a directory is currently locked by a SimpleCache instance [1][2]. - To resolve issues where a folder remains locked, you must ensure that all previous SimpleCache instances associated with that directory are properly released by calling their release method [3][2]. Note that this behavior relates to the filesystem locking mechanism implemented by SimpleCache to maintain integrity; it is not typically described in terms of "corrupted" lock files, but rather as a persistent lock that requires the corresponding instance to be explicitly released [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '^(stream-chat-android-client/src/main/java/io/getstream/chat/android/client/(cache/internal/VideoMediaCache\.kt|ChatClient\.kt|.*VideoCacheConfig.*|.*CacheConfig.*))$'

echo "== locate create call sites =="
rg -n "VideoMediaCache\.create\(" stream-chat-android-client/src/main/java/io/getstream/chat/android/client -S

echo "== relevant outlines =="
ast-grep outline stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt --view expanded
echo "---"
ast-grep outline stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt --view expanded

Repository: GetStream/stream-chat-android

Length of output: 8516


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== VideoMediaCache.create slice =="
sed -n '120,180p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt

echo
echo "== ChatClient internalBuild slice around video cache =="
sed -n '5260,5310p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

echo
echo "== StreamCacheConfig slice =="
sed -n '1,220p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt

Repository: GetStream/stream-chat-android

Length of output: 6310


Guard SimpleCache construction here

SimpleCache(...) can throw when the cache folder is locked or otherwise unusable, and this path is called directly from ChatClient.Builder.internalBuild(). If video caching is enabled, a stale cache dir can abort client initialization instead of just disabling cache. Catch the failure and fall back to no video cache so cache issues don't take down startup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt`
around lines 137 - 156, The VideoMediaCache.create(...) path currently
constructs SimpleCache directly, which can throw and abort
ChatClient.Builder.internalBuild() when the cache dir is locked or unusable.
Wrap the SimpleCache initialization in create(...) with failure handling, and on
error fall back to returning no video cache instead of propagating the
exception. Keep the guard localized to VideoMediaCache.create and preserve the
existing reuse behavior in the instances map when cache creation succeeds.

Comment on lines +1517 to +1532
// Clear video cache: in-place via any live cache in the process (keeps the SimpleCache
// alive so playback continues to work), or by deleting the directory when no live cache
// owns it. The registry is process-wide, so this covers caches from a prior ChatClient
// build even if the current client was built without a cacheConfig.
val videoCacheResult = if (VideoMediaCache.clearAll()) {
Result.Success(Unit)
} else {
fileManager.clearVideoCache(context)
}
// Clear all cache directories
val cacheResult = fileManager.clearAllCache(context)
// Clear external (temporary) storage files - always run regardless of cache result
val externalStorageResult = fileManager.clearExternalStorage(context)
// Return the first failure if any, otherwise success
when {
videoCacheResult is Result.Failure -> videoCacheResult

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant symbols before reading slices
echo "== ChatClient outline =="
ast-grep outline stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt --view expanded | sed -n '1,220p'

echo
echo "== VideoMediaCache search =="
rg -n "class VideoMediaCache|fun clearAll|fun create|instances|clearVideoCache|clearAllCache|clearExternalStorage" stream-chat-android-client/src/main/java -S

echo
echo "== FileManager outline if present =="
fd -a "FileManager.kt" stream-chat-android-client/src/main/java || true

Repository: GetStream/stream-chat-android

Length of output: 31263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ChatClient outline =="
ast-grep outline stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt --view expanded | sed -n '1,220p'

echo
echo "== VideoMediaCache search =="
rg -n "class VideoMediaCache|fun clearAll|fun create|instances|clearVideoCache|clearAllCache|clearExternalStorage" stream-chat-android-client/src/main/java -S

echo
echo "== FileManager files =="
fd -a "FileManager.kt" stream-chat-android-client/src/main/java || true

Repository: GetStream/stream-chat-android

Length of output: 31250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant slices with line numbers once the file locations are known.
sed -n '1500,1545p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

echo
echo "== VideoMediaCache.kt slices =="
sed -n '1,260p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/helpers/VideoMediaCache.kt

Repository: GetStream/stream-chat-android

Length of output: 3026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the exact files and relevant symbols.
git ls-files 'stream-chat-android-client/src/main/java/**' | rg 'ChatClient\.kt|VideoMediaCache\.kt|FileManager\.kt'

echo
for f in \
  stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt \
  stream-chat-android-client/src/main/java/io/getstream/chat/android/client/helpers/VideoMediaCache.kt \
  stream-chat-android-client/src/main/java/io/getstream/chat/android/client/helpers/FileManager.kt
do
  if [ -f "$f" ]; then
    echo "== $f =="
    wc -l "$f"
  fi
done

Repository: GetStream/stream-chat-android

Length of output: 783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for the helper implementation and call sites.
rg -n "class VideoMediaCache|object VideoMediaCache|clearAll\(|create\(|deleteRecursively|clearVideoCache\(" stream-chat-android-client/src/main/java/io/getstream/chat/android/client -S

Repository: GetStream/stream-chat-android

Length of output: 7459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the relevant sections of the two helper files.
python3 - <<'PY'
from pathlib import Path

paths = [
    Path("stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt"),
    Path("stream-chat-android-client/src/main/java/io/getstream/chat/android/client/helpers/VideoMediaCache.kt"),
    Path("stream-chat-android-client/src/main/java/io/getstream/chat/android/client/helpers/FileManager.kt"),
]
for p in paths:
    if p.exists():
        print(f"\n== {p} ==")
        lines = p.read_text().splitlines()
        for i, line in enumerate(lines, 1):
            if ("clearCacheAndTemporaryFiles" in line or
                "clearAll(" in line or
                "clearVideoCache" in line or
                "deleteRecursively" in line or
                "class VideoMediaCache" in line or
                "object VideoMediaCache" in line or
                "fun create(" in line):
                start = max(1, i - 20)
                end = min(len(lines), i + 40)
                for j in range(start, end + 1):
                    print(f"{j:5d}: {lines[j-1]}")
                print()
PY

Repository: GetStream/stream-chat-android

Length of output: 14827


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f="stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt"
wc -l "$f"
echo
sed -n '1,240p' "$f"

Repository: GetStream/stream-chat-android

Length of output: 7626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '360,390p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt

Repository: GetStream/stream-chat-android

Length of output: 1522


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt")
lines = p.read_text().splitlines()
for start in [360]:
    end = min(len(lines), start+35)
    for i in range(start, end+1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: GetStream/stream-chat-android

Length of output: 1954


Close the video-cache clear/delete race.

VideoMediaCache.clearAll() only tells you whether a live cache existed at that moment; if it returns false, the fallback fileManager.clearVideoCache(context) in ChatClient.kt:1521-1524 can run after another thread has created a new VideoMediaCache for the same directory. That deleteRecursively() path can corrupt the live SimpleCache index/lock. Move the fallback delete behind a single atomic VideoMediaCache API that holds the registry lock across the “clear live caches or delete on disk” decision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt`
around lines 1517 - 1532, The video cache cleanup in ChatClient should not
decide between clearing live caches and deleting the directory in two separate
steps. Update the cache removal flow around VideoMediaCache.clearAll() so the
“clear live caches or delete on disk” decision is handled atomically under the
registry lock, and remove the fallback delete from ChatClient’s cache-clearing
path. Use the existing VideoMediaCache and fileManager.clearVideoCache(context)
symbols to locate the affected logic and route the fallback through a single
synchronized API that prevents a newly created cache from being deleted
mid-flight.

@andremion andremion left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, design is solid. Two nits inline, both optional.

* Wrap an instance in [StreamCacheConfig] and set it on
* [io.getstream.chat.android.client.api.ChatClientConfig.cacheConfig] to opt in. When the cache is
* enabled, replaying or seeking within a previously watched video reuses cached byte ranges
* instead of re-downloading from the CDN.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small doc suggestion. The cache key strips the URL query, so entries are keyed by path alone, and this wraps all ExoPlayer media, not only Stream attachments. Worth one line here noting the cache assumes the path identifies the content? Sets expectations for anyone playing non-Stream URLs where the query is content-significant.

// and falls back to alphabetical key order on ties, which would pick A over B under
// load and make the assertions non-deterministic.
readFully(factory.createDataSource(), DataSpec(Uri.parse(VIDEO_A_URL)))
Thread.sleep(SPAN_SPACING_MS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spaces writes with Thread.sleep(5L) because the evictor orders by System.currentTimeMillis(). Reasoning is clear, but it's wall-clock dependent so it could flake on a loaded runner. Just flagging in case it ever goes red.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants