Skip to content

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

Open
VelikovPetar wants to merge 6 commits into
v6from
feature/AND-1275_video_attachments_caching
Open

Add opt-in video disk cache for ExoPlayer playback#6533
VelikovPetar wants to merge 6 commits into
v6from
feature/AND-1275_video_attachments_caching

Conversation

@VelikovPetar

@VelikovPetar VelikovPetar commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Goal

Video attachments currently re-stream from the CDN on every replay, seek, or scroll-back. This wastes customer bandwidth and forces users to wait through buffering on content they have already watched. This PR introduces an opt-in on-disk video cache so previously watched videos replay from local bytes.

Implementation

Public API

A new io.getstream.chat.android.client.cache package exposes the opt-in surface:

  • StreamCacheConfig(video: VideoCacheConfig? = null) — aggregate config; future cache types (e.g. images) 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 kicks in once exceeded.
  • ChatClient.Builder.cacheConfig(StreamCacheConfig) — single entry point. Default is off; existing apps see no behavior change.

Usage:

ChatClient.Builder("api-key", context)
    .cacheConfig(StreamCacheConfig(video = VideoCacheConfig()))
    .build()

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.
  • Wired into all four Media3 playback sites:
    • Compose: MediaPreviewActivityStreamMediaPlayerContent, MediaGalleryPager
    • UI components: AttachmentMediaActivity, AttachmentGalleryVideoPageFragment
  • Only confidently-video content (type == AttachmentType.VIDEO or mime starts with video/) is routed through the cache. Audio attachments and other media bypass it entirely so audio bytes don't occupy disk space reserved for video playback.
  • Cache directory lives under StreamFileManager.getVideoCache(context) and is included in clearCacheAndTemporaryFiles(context).

UI Changes

No UI changes.

Testing

Automated

  • StreamMediaDataSourceCacheIntegrationTest — four-cell matrix {custom CDN, no CDN} × {hit, miss} plus an LRU eviction case that writes three videos into a 2-video-capacity cache and verifies the least-recently-used entry is the one dropped.
  • VideoMediaCacheTest — directory creation, release() idempotency, per-directory instance registry.
  • StreamFileManagerTest / ChatClientCacheAndTemporaryFilesTest — cache directory lookup and cleanup wiring.
./gradlew :stream-chat-android-client:testDebugUnitTest \
    --tests "io.getstream.chat.android.client.cache.internal.*" \
    --tests "io.getstream.chat.android.client.ChatClientCacheAndTemporaryFilesTest"

Manual smoke

  1. Enable via ChatClient.Builder.cacheConfig(StreamCacheConfig(video = VideoCacheConfig())) in the sample app.
  2. Send a video attachment, scroll away, scroll back, tap to play.
  3. Close, then tap the same video again.
  4. Expected: the second play has no network traffic in Profiler; the cache directory under <cacheDir>/stream_video_cache/ contains files.
  5. Repeat with an audio attachment — no entries should appear in the cache directory.
  6. Kill and restart the app; playing the same video should still serve from cache (no network).

Summary by CodeRabbit

  • New Features

    • Added optional video caching for streamed media, improving playback performance and reducing repeated downloads.
    • Expanded media preview and gallery playback to use the new caching flow automatically for video content.
    • Added a new configuration option to control cache settings, including cache size.
  • Bug Fixes

    • Improved cache clearing so video cache data is removed correctly.
    • Fixed cache behavior for media URLs with changing query parameters, helping reuse cached video content more reliably.

VelikovPetar and others added 2 commits June 29, 2026 20:50
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@VelikovPetar VelikovPetar added the pr:new-feature New feature label Jun 29, 2026
@github-actions

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 ignored for dependabot PRs).

🎉 Great job! This PR is ready for review.

@github-actions

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.26 MB 5.32 MB 0.05 MB 🟢
stream-chat-android-offline 5.49 MB 5.54 MB 0.04 MB 🟢
stream-chat-android-ui-components 10.64 MB 10.75 MB 0.11 MB 🟢
stream-chat-android-compose 12.87 MB 12.95 MB 0.09 MB 🟢

VelikovPetar and others added 3 commits June 30, 2026 10:01
Pre-signed URLs with rotating Expires/Signature query parameters
would otherwise produce a fresh cache key on every open and defeat
the cache. The CacheKeyFactory now strips the query, so the same
path resolves to the same entry regardless of signature rotation.
The full DataSpec still flows to the upstream on a miss, so a
custom CDN sees the original URL with its query intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@VelikovPetar VelikovPetar marked this pull request as ready for review June 30, 2026 12:51
@VelikovPetar VelikovPetar requested a review from a team as a code owner June 30, 2026 12:51

@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: 5

🧹 Nitpick comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt (1)

86-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider defaulting useVideoCache to false.

The default is true, so any caller that omits the argument routes playback through the video cache — including non-video (e.g., audio) content, which contradicts the "video only" intent documented for this parameter. Current in-cohort callers pass it explicitly, so this is only a guard against future misuse; a conservative false default would make the opt-in semantics safer.

🤖 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-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt`
at line 86, The default value for useVideoCache in StreamMediaPlayerContent is
too permissive; update it to false so cache usage remains an explicit opt-in and
doesn’t affect callers that omit the argument. Adjust the default on the
StreamMediaPlayerContent parameter, keeping the existing explicit call sites
unchanged, and ensure the parameter’s intent remains “video only” in its usage.
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt (1)

19-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add thread/state notes to the new public KDoc.

These are new public client APIs, but the KDoc still omits the thread-safety/state expectations required for this module's public surface. A short note that the configs are immutable value objects and that caching stays disabled unless video is non-null would close that gap. As per coding guidelines "Document public APIs with KDoc, including thread expectations and state notes".

🤖 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/StreamCacheConfig.kt`
around lines 19 - 45, The new public KDoc on StreamCacheConfig and
VideoCacheConfig needs thread/state expectations added. Update the docs for
StreamCacheConfig and its video property to note that these are immutable value
objects and that video caching remains disabled unless video is non-null. Also
add a brief state note to VideoCacheConfig in the same style so the public API
documentation matches the module guidelines.

Source: Coding guidelines

🤖 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 69-80: The `VideoMediaCache.release()` method removes `dirPath`
from `instances` before teardown finishes, which can let `create()` reuse a slot
while `cache.release()` or `databaseProvider.close()` is still failing. Move the
`instances.remove(dirPath)` logic so it only happens after both teardown calls
succeed, and keep the registry entry intact if either close/release throws. Use
`release`, `create`, `instances`, `cache.release()`, and
`databaseProvider.close()` to locate the change.
- Around line 102-119: Normalize the cache directory before deriving the
singleton key in VideoMediaCache.create so alias paths for the same on-disk
directory map to one instance. Update the key generation around
cacheDir.absolutePath to use a canonical/resolved form of the directory, and
keep the existing synchronized(instances) reuse check and SimpleCache
construction behavior unchanged so Media3 still gets only one cache per actual
directory.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt`:
- Around line 43-49: Reject invalid cache sizes at the API boundary by
validating VideoCacheConfig.maxSizeBytes before it reaches VideoMediaCache and
LeastRecentlyUsedCacheEvictor. Add a fail-fast check in VideoCacheConfig (or its
constructor/init path) so 0 and negative values are rejected with a clear error,
while keeping the existing default constant unchanged. Use the VideoCacheConfig
and VideoMediaCache symbols to locate the validation point and ensure all
callers get the same safeguard.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/StreamMediaDataSource.kt`:
- Around line 46-48: Update the KDoc on StreamMediaDataSource to match the
actual cache-key behavior in VideoCacheDataSourceFactory: the cache is not keyed
by the raw dataSpec.uri, but by a query-stripped URI so differently signed or
rotating URLs still resolve to the same cached entry. Keep the description
aligned with the wrapping behavior in StreamMediaDataSource and any helper that
normalizes the cache key, so readers are not misled about distinct cache entries
for query changes.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt`:
- Around line 441-455: The clearVideoCache() helper deletes the video cache
directory while VideoMediaCache may still hold a process-wide SimpleCache on
that path. Update clearAllCache() to tear down/release the shared video cache
first, then call clearVideoCache(context) so the cache lock and index are closed
before deleteRecursively() runs. Use the existing VideoMediaCache teardown path
and keep the clearVideoCache() deletion logic unchanged after the cache is
released.

---

Nitpick comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/StreamCacheConfig.kt`:
- Around line 19-45: The new public KDoc on StreamCacheConfig and
VideoCacheConfig needs thread/state expectations added. Update the docs for
StreamCacheConfig and its video property to note that these are immutable value
objects and that video caching remains disabled unless video is non-null. Also
add a brief state note to VideoCacheConfig in the same style so the public API
documentation matches the module guidelines.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt`:
- Line 86: The default value for useVideoCache in StreamMediaPlayerContent is
too permissive; update it to false so cache usage remains an explicit opt-in and
doesn’t affect callers that omit the argument. Adjust the default on the
StreamMediaPlayerContent parameter, keeping the existing explicit call sites
unchanged, and ensure the parameter’s intent remains “video only” in its usage.
🪄 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: 0dd8e7c8-1426-48be-a800-3705667274a5

📥 Commits

Reviewing files that changed from the base of the PR and between 9e9a59f and 24a6965.

📒 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/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/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/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/handler/MediaAttachmentPreviewHandler.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.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 +441 to +455
@Suppress("TooGenericExceptionCaught")
private fun clearVideoCache(context: Context): Result<Unit> {
return try {
val directory = getVideoCache(context)
if (!directory.exists()) {
Result.Success(Unit)
} else if (directory.deleteRecursively()) {
Result.Success(Unit)
} else {
Result.Failure(Error.GenericError("Could not clear video cache directory."))
}
} catch (e: Exception) {
Result.Failure(Error.ThrowableError("Could not clear video cache directory.", e))
}
}

@coderabbitai coderabbitai Bot Jun 30, 2026

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

🏁 Script executed:

#!/bin/bash
# Inspect VideoMediaCache for release/clear handling and any teardown hooked into cache clearing.
fd -t f 'VideoMediaCache.kt' --exec cat -n {}
rg -nP '\b(release|delete|clearAllCache|clearVideoCache)\s*\(' stream-chat-android-client/src/main/java/io/getstream/chat/android/client

Repository: GetStream/stream-chat-android

Length of output: 9295


🏁 Script executed:

#!/bin/bash
sed -n '230,470p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt
printf '\n---\n'
sed -n '1,140p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt
printf '\n---\n'
sed -n '1510,1545p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

Repository: GetStream/stream-chat-android

Length of output: 17925


Release the shared video cache before deleting its directory. clearAllCache deletes the video cache folder while VideoMediaCache keeps a process-wide SimpleCache open on that same path. Call the cache teardown path first so the lock/index are released before deleteRecursively(), or later video cache writes/playback can fail.

🤖 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/internal/file/StreamFileManager.kt`
around lines 441 - 455, The clearVideoCache() helper deletes the video cache
directory while VideoMediaCache may still hold a process-wide SimpleCache on
that path. Update clearAllCache() to tear down/release the shared video cache
first, then call clearVideoCache(context) so the cache lock and index are closed
before deleteRecursively() runs. Use the existing VideoMediaCache teardown path
and keep the clearVideoCache() deletion logic unchanged after the cache is
released.

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.

@VelikovPetar is this a valid concern?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Outline: StreamFileManager =="
ast-grep outline stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt --view expanded || true

echo
echo "== Outline: VideoMediaCache =="
ast-grep outline stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt --view expanded || true

echo
echo "== StreamFileManager relevant slice =="
sed -n '230,280p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt
echo
sed -n '430,500p' stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/file/StreamFileManager.kt

echo
echo "== VideoMediaCache file =="
cat -n stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt

echo
echo "== Search for release/clear usage =="
rg -n -C3 --type=kt '\b(VideoMediaCache|release|clearVideoCache|clearAllCache)\b' \
  stream-chat-android-client/src/main/java/io/getstream/chat/android/client

Length of output: 14414


@gpunto Yes — I think the concern is valid.

What I verified:

  • In StreamFileManager.kt, clearAllCache() calls clearVideoCache(context) directly, and clearVideoCache() just does deleteRecursively() on the video cache directory.
  • In VideoMediaCache.kt, the cache is process-wide and backed by a SimpleCache, with instances kept in a singleton map by directory path.
  • That file also has a release() method that explicitly releases SimpleCache, closes the database provider, and removes the instance from the registry.

So deleting the directory without releasing that shared cache first is risky: the in-memory singleton can still believe it owns that path, and Media3’s per-directory lock/index lifecycle is exactly the thing release() is meant to tear down.

The only nuance is that the current teardown method is marked @VisibleForTesting and its KDoc says production code “does not need to call this”, so the issue is real, but the final fix may be better as a small production-safe reset API rather than wiring the current test-oriented method directly from StreamFileManager.


🧠 Learnings used
Learnt from: VelikovPetar
Repo: GetStream/stream-chat-android PR: 6152
File: stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImpl.kt:194-212
Timestamp: 2026-02-17T13:56:11.136Z
Learning: In Kotlin code handling channel events, specifically MessageUpdatedEvent, prefer using server-provided data for replyTo rather than enriching it from local state as done in ChannelEventHandlerImpl. This should be treated as the source of truth. Ensure code comments document this design decision and that tests validate server data is used for replyTo. If there are any places that historically enriched replyTo from local state (e.g., ChannelEventHandlerLegacyImpl), consider aligning those implementations or adding a clear rationale in design docs to avoid future regressions.

Learnt from: VelikovPetar
Repo: GetStream/stream-chat-android PR: 6295
File: stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/StreamMediaDataSource.kt:27-49
Timestamp: 2026-03-27T08:18:10.611Z
Learning: In GetStream/stream-chat-android (stream-chat-android-client), the usual expectation to document thread-safety and state/lifecycle for public APIs does not apply to public objects/factory methods annotated with `InternalStreamChatApi` (e.g., `StreamMediaDataSource.factory` in `.../cdn/internal/StreamMediaDataSource.kt`). For these `InternalStreamChatApi`-annotated utilities, maintainers consider that level of thread/lifecycle KDoc detail unnecessary; review/flag KDoc thread/state expectations primarily for non-`InternalStreamChatApi` public APIs.

Learnt from: andremion
Repo: GetStream/stream-chat-android PR: 6301
File: stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/channel/info/AddMembersViewAction.kt:19-25
Timestamp: 2026-03-30T09:19:45.796Z
Learning: In GetStream/stream-chat-android SDK modules that set a module-level Kotlin compiler argument (via build.gradle.kts) like `-opt-in=io.getstream.chat.android.core.internal.InternalStreamChatApi`, do not require per-file opt-in annotations. Specifically, don’t flag missing `file:OptIn(InternalStreamChatApi::class)` in `.kt` files within those modules when using `InternalStreamChatApi` types, since the opt-in is already applied at the module build level. Only flag missing per-file opt-in in modules that do NOT configure this compiler arg.

Learnt from: gpunto
Repo: GetStream/stream-chat-android PR: 6328
File: stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler.kt:19-28
Timestamp: 2026-04-02T14:29:26.633Z
Learning: In Kotlin, if a `private const val` is declared inside a `companion object`, it is directly accessible within the same `companion object` body (where it’s declared) without any import. During review, don’t claim an import is required or that an import would cause a compile error for usage inside that same companion object in the same file; only flag missing/incorrect imports when code is referencing the constant from outside the companion object.

Learnt from: gpunto
Repo: GetStream/stream-chat-android PR: 6468
File: stream-chat-android-ui-components/api/stream-chat-android-ui-components.api:3064-3069
Timestamp: 2026-05-27T15:59:48.528Z
Learning: In this repository, do not flag public Kotlin `sealed` interfaces (e.g., `public sealed interface ...`) for source-compatibility concerns specifically about adding new abstract members. Because Kotlin `sealed` types cannot be implemented outside the permitted scope (so external consumers cannot add new implementors), adding abstract members should not break external compilation on the basis of missing implementations. Note: the project does not generally guarantee binary compatibility, so this guidance is only for the source-compatibility check.

Learnt from: VelikovPetar
Repo: GetStream/stream-chat-android PR: 6472
File: stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatClientConfig.kt:133-136
Timestamp: 2026-06-02T08:42:28.269Z
Learning: For this GetStream/stream-chat-android repository, do not treat Kotlin JVM binary compatibility of SDK public API data classes as a long-term compatibility contract across releases. In code reviews, it is acceptable to add new constructor parameters (e.g., `val`/`var` fields) with default values to existing public `data class` types without adding additive/secondary-constructor workarounds. Therefore, reviewers should not raise binary-compatibility breakage concerns for this repository when such changes are made.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe that we shouldn't fully release the VideoCache - if we release it, it means that our VideoMediaCache instance will reference an outdated SimpleCache. I think we should instead just clear the cache dir via the cache.removeResource(key) method, and not directly via deleteRecursively. Currently running some tests related to this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, valid concern — fixed in 372ab0f. Added VideoMediaCache.clear() which iterates cache.keys and calls SimpleCache.removeResource(key) for each entry; this empties the cache in place without touching the directory Media3's live SimpleCache still owns (deleting the directory out from under it would corrupt the on-disk index and lock).

Wiring:

  • StreamFileManager.clearAllCache no longer touches the video cache directory (the invariant is guarded by a new test).
  • ChatClient.clearCacheAndTemporaryFiles routes video-cache clearing through videoCache?.clear() when a live cache is present, falling back to fileManager.clearVideoCache(context) when no cache is configured (so leftover files from a prior opt-in still get cleaned).

This keeps subsequent playback working immediately after the clear (verified end-to-end via the new clear removes cached bytes from disk and keeps the cache functional test).

@gpunto gpunto 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.

Looks good, just a couple of doubts and a nitpick

Comment on lines +441 to +455
@Suppress("TooGenericExceptionCaught")
private fun clearVideoCache(context: Context): Result<Unit> {
return try {
val directory = getVideoCache(context)
if (!directory.exists()) {
Result.Success(Unit)
} else if (directory.deleteRecursively()) {
Result.Success(Unit)
} else {
Result.Failure(Error.GenericError("Could not clear video cache directory."))
}
} catch (e: Exception) {
Result.Failure(Error.ThrowableError("Could not clear video cache directory.", e))
}
}

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.

@VelikovPetar is this a valid concern?

- Cache all ExoPlayer-driven playback (video, audio, voice recordings)
  through the same VideoCacheConfig; remove the now-dead isVideo gating
  and mimeType/type intent extras on MediaPreviewActivity.
- Clear the video cache in place via VideoMediaCache.clear() so the live
  SimpleCache stays alive after ChatClient.clearCacheAndTemporaryFiles;
  StreamFileManager.clearAllCache no longer deletes the video directory
  out from under the live cache.
- Reject non-positive VideoCacheConfig.maxSizeBytes at the API boundary.
- Fix stale KDoc on StreamMediaDataSource to reflect the query-stripped
  cache key.
- Stabilise the flaky LRU eviction test by spacing cache writes across
  distinct millisecond timestamps.
@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
77.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a configurable on-disk video cache to the Stream Chat Android SDK. New StreamCacheConfig/VideoCacheConfig types wire into ChatClient.Builder and produce a process-wide VideoMediaCache (Media3 SimpleCache), composed via VideoCacheDataSourceFactory with query-stripped cache keys, and integrated into StreamMediaDataSource, StreamFileManager cache clearing, and both Compose and XML media playback paths.

Changes

On-disk Video Cache Feature

Layer / File(s) Summary
Public cache config types and API surface
stream-chat-android-client/src/main/.../cache/StreamCacheConfig.kt, .../ChatClient.kt, .../*.api
Adds StreamCacheConfig/VideoCacheConfig data classes with a 150MB default max size and validation, plus ChatClient.Builder.cacheConfig(...).
VideoMediaCache singleton and cache data source factory
.../cache/internal/VideoMediaCache.kt, .../cache/internal/VideoCacheDataSourceFactory.kt
Adds a per-directory singleton VideoMediaCache wrapping Media3 SimpleCache/StandaloneDatabaseProvider with clear()/release(), and VideoCacheDataSourceFactory deriving cache keys from DataSpec (query-stripped) and delegating to upstream on misses.
StreamFileManager video cache directory handling
.../internal/file/StreamFileManager.kt
Adds VIDEO_CACHE_DIR, getVideoCache, and clearVideoCache, clarifying that clearAllCache no longer deletes the video cache directory directly.
ChatClient wiring
.../ChatClient.kt
Extends the constructor with a nullable videoCache, derives it from cacheConfig?.video during internalBuild(), forwards it into StreamMediaDataSource.factory, and updates clearCacheAndTemporaryFiles to clear it (or fall back to fileManager.clearVideoCache).
StreamMediaDataSource optional cache layer + @OptIn migration
.../cdn/internal/StreamMediaDataSource.kt, .../cdn/internal/CDNDataSourceFactory.kt
factory(...) gains an optional videoCache parameter, wrapping the CDN-aware factory as the outermost cache layer; @UnstableApi annotations replaced with @OptIn(UnstableApi::class).
Compose UI wiring
.../preview/MediaPreviewActivity.kt, .../preview/internal/StreamMediaPlayerContent.kt, compose *.api
Adds a getIntent(Context, String) overload via @JvmOverloads and passes client.videoCache into StreamMediaDataSource.factory.
XML UI wiring
.../gallery/AttachmentMediaActivity.kt, .../gallery/internal/AttachmentGalleryVideoPageFragment.kt
Adds isVideoContent() gating and passes client.videoCache conditionally into StreamMediaDataSource.factory.
Tests and mock stub updates
VideoMediaCacheTest.kt, StreamMediaDataSourceCacheIntegrationTest.kt, VideoCacheConfigTest.kt, StreamFileManagerTest.kt, ChatClientCacheAndTemporaryFilesTest.kt, CDNDataSourceTest.kt, MockedChatClientTest.kt
Adds unit/integration coverage for cache lifecycle, hit/miss/eviction behavior, config validation, and file manager clearing; stubs videoCache to null in mocked test client.

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

Sequence Diagram(s)

sequenceDiagram
  participant Builder as ChatClient.Builder
  participant VMC as VideoMediaCache.create()
  participant SC as SimpleCache (LRU)
  participant VCDSF as VideoCacheDataSourceFactory
  participant CDN as CDNDataSourceFactory
  participant Upstream as HttpDataSource

  Builder->>VMC: create(appContext, cacheDir, VideoCacheConfig)
  VMC-->>Builder: VideoMediaCache (singleton per dir)
  Builder->>VCDSF: wrap(videoCache, upstreamFactory)
  VCDSF->>CDN: createDataSource() on miss
  CDN->>Upstream: open(signed URL)
  Upstream-->>SC: write bytes
  SC-->>VCDSF: serve bytes on hit
Loading

Suggested reviewers: aleksandar-apostolov

Poem

A rabbit hops through cache-dir trails,
stashing video bytes in SimpleCache pails,
CDN signs, then steps aside,
query tails stripped, keys applied,
hop, hop, hooray — no re-fetch fails! 🐇📼

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding an opt-in video disk cache for ExoPlayer playback.
Description check ✅ Passed It includes the required Goal, Implementation, UI Changes, and Testing sections, and the missing non-critical checklist/GIF items don't block comprehension.
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 feature/AND-1275_video_attachments_caching

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.

♻️ Duplicate comments (2)
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt (2)

90-105: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Registry entry removed before teardown succeeds (still unresolved from prior review).

instances.remove(dirPath) at line 93 runs before cache.release()/databaseProvider.close(). If either throws (caught and only logged), a subsequent create() for the same directory will construct a new SimpleCache while the old one may still hold Media3's directory lock, causing the new construction to fail.

🔒 Proposed fix
     internal fun release() {
         synchronized(instances) {
-            instances.remove(dirPath)
+            var released = true
             try {
                 cache.release()
             } catch (`@Suppress`("TooGenericExceptionCaught") e: Exception) {
                 logger.e(e) { "[release] failed to release SimpleCache" }
+                released = false
             }
             try {
                 databaseProvider.close()
             } catch (`@Suppress`("TooGenericExceptionCaught") e: Exception) {
                 logger.e(e) { "[release] failed to close StandaloneDatabaseProvider" }
+                released = false
             }
+            if (released) {
+                instances.remove(dirPath)
+            }
         }
     }
🤖 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 90 - 105, The release flow in VideoMediaCache.release removes the
registry entry before teardown finishes, so a failed cache.release() or
databaseProvider.close() can leave the old Media3 lock active while create()
builds a new instance. Move instances.remove(dirPath) to after both teardown
calls succeed, or otherwise only unregister once release and close have
completed without error, using VideoMediaCache.release, create(), and the
instances map as the key symbols to update.

123-142: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Singleton key still uses raw absolutePath (still unresolved from prior review).

cacheDir.absolutePath doesn't normalize aliases like ./video-cache vs video-cache or symlinked equivalents, so the "one SimpleCache per directory" guard can still be bypassed for the same physical directory.

Proposed fix
         public fun create(appContext: Context, cacheDir: File, config: VideoCacheConfig): VideoMediaCache =
             synchronized(instances) {
                 cacheDir.mkdirs()
-                val key = cacheDir.absolutePath
+                val normalizedDir = cacheDir.canonicalFile
+                val key = normalizedDir.path
                 instances[key]?.let { existing ->
                     ...
                 }
                 val dbProvider = StandaloneDatabaseProvider(appContext)
                 val simpleCache = SimpleCache(
-                    cacheDir,
+                    normalizedDir,
                     LeastRecentlyUsedCacheEvictor(config.maxSizeBytes),
                     dbProvider,
                 )
                 VideoMediaCache(simpleCache, dbProvider, key).also { instances[key] = it }
             }
🤖 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 123 - 142, The singleton lookup in VideoMediaCache.create still
uses cacheDir.absolutePath, so equivalent directories can bypass the reuse
guard. Update the key generation to use a normalized canonical/real path for the
cache directory before checking instances and storing the cache, and keep the
reuse logic in create synchronized on that normalized key.
🧹 Nitpick comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity.kt (1)

187-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reconsider adding @JvmOverloads here.

Based on learnings, this module intentionally avoids @JvmOverloads for public API compatibility and relies on Kotlin default arguments instead. Please confirm there's a specific Java-interop need driving this addition; otherwise consider dropping @JvmOverloads to stay consistent with the established convention.

♻️ Suggested diff
-        `@JvmOverloads`
         public fun getIntent(
             context: Context,
             url: String,
             title: String? = null,
         ): Intent {
🤖 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-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity.kt`
around lines 187 - 192, The public MediaPreviewActivity.getIntent API should not
introduce `@JvmOverloads` unless there is a concrete Java-interop requirement;
this module prefers Kotlin default arguments for public compatibility. Remove
`@JvmOverloads` from getIntent unless you can justify the need, and keep the
signature aligned with the established convention used in this compose module.

Source: Learnings

stream-chat-android-compose/api/stream-chat-android-compose.api (1)

979-981: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New @JvmOverloads-generated overload conflicts with an established module convention.

This entry is generated correctly, but it's a direct byproduct of adding @JvmOverloads to MediaPreviewActivity.Companion.getIntent (see that file). Based on learnings, in this module Kotlin default arguments should be relied on instead of introducing @JvmOverloads-generated overloads for public API compatibility.

🤖 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-compose/api/stream-chat-android-compose.api` around lines
979 - 981, Remove the public overloads introduced by `@JvmOverloads` on
MediaPreviewActivity.Companion.getIntent and rely on Kotlin default arguments
instead. Update the Companion.getIntent API so it exposes only the intended
single Kotlin entry point with defaulted parameters, and regenerate the .api
metadata to drop the extra getIntent overload and getIntent$default symbol.

Source: Learnings

🤖 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.

Duplicate comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoMediaCache.kt`:
- Around line 90-105: The release flow in VideoMediaCache.release removes the
registry entry before teardown finishes, so a failed cache.release() or
databaseProvider.close() can leave the old Media3 lock active while create()
builds a new instance. Move instances.remove(dirPath) to after both teardown
calls succeed, or otherwise only unregister once release and close have
completed without error, using VideoMediaCache.release, create(), and the
instances map as the key symbols to update.
- Around line 123-142: The singleton lookup in VideoMediaCache.create still uses
cacheDir.absolutePath, so equivalent directories can bypass the reuse guard.
Update the key generation to use a normalized canonical/real path for the cache
directory before checking instances and storing the cache, and keep the reuse
logic in create synchronized on that normalized key.

---

Nitpick comments:
In `@stream-chat-android-compose/api/stream-chat-android-compose.api`:
- Around line 979-981: Remove the public overloads introduced by `@JvmOverloads`
on MediaPreviewActivity.Companion.getIntent and rely on Kotlin default arguments
instead. Update the Companion.getIntent API so it exposes only the intended
single Kotlin entry point with defaulted parameters, and regenerate the .api
metadata to drop the extra getIntent overload and getIntent$default symbol.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity.kt`:
- Around line 187-192: The public MediaPreviewActivity.getIntent API should not
introduce `@JvmOverloads` unless there is a concrete Java-interop requirement;
this module prefers Kotlin default arguments for public compatibility. Remove
`@JvmOverloads` from getIntent unless you can justify the need, and keep the
signature aligned with the established convention used in this compose module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 770c5842-5cdd-4646-91a5-13354388d42a

📥 Commits

Reviewing files that changed from the base of the PR and between 9e9a59f and 372ab0f.

📒 Files selected for processing (20)
  • 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/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/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.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

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