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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ interface MockedChatClientTest {
whenever(MockChatClient.clientState) doReturn MockClientState
whenever(MockChatClient.inheritScope(any())) doReturn
TestScope() + CoroutineExceptionHandler { _, _ -> }
// RETURNS_MOCKS would otherwise hand out mock instances for nullable getters that
// production code reads opportunistically (e.g. opt-in features). Stub them to null
// so default behaviour matches the unconfigured path.
whenever(MockChatClient.videoCache) doReturn null
}

@After
Expand Down
40 changes: 37 additions & 3 deletions stream-chat-android-client/api/stream-chat-android-client.api
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,10 @@ public final class io/getstream/chat/android/client/api/ChatClientConfig {
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;)V
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;Z)V
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZ)V
public synthetic fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZLio/getstream/chat/android/client/cache/StreamCacheConfig;)V
public synthetic fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZLio/getstream/chat/android/client/cache/StreamCacheConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Z
public final fun component10 ()Lio/getstream/chat/android/client/cache/StreamCacheConfig;
public final fun component2 ()Ljava/util/Set;
public final fun component3 ()Z
public final fun component4 ()Z
Expand All @@ -364,9 +366,10 @@ public final class io/getstream/chat/android/client/api/ChatClientConfig {
public final fun component7 ()Lio/getstream/chat/android/client/api/MessageLimitConfig;
public final fun component8 ()Z
public final fun component9 ()Z
public final fun copy (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZ)Lio/getstream/chat/android/client/api/ChatClientConfig;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/api/ChatClientConfig;ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZILjava/lang/Object;)Lio/getstream/chat/android/client/api/ChatClientConfig;
public final fun copy (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZLio/getstream/chat/android/client/cache/StreamCacheConfig;)Lio/getstream/chat/android/client/api/ChatClientConfig;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/api/ChatClientConfig;ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZLio/getstream/chat/android/client/cache/StreamCacheConfig;ILjava/lang/Object;)Lio/getstream/chat/android/client/api/ChatClientConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getCacheConfig ()Lio/getstream/chat/android/client/cache/StreamCacheConfig;
public final fun getFastEventParsing ()Z
public final fun getIgnoredOfflineChannelTypes ()Ljava/util/Set;
public final fun getMessageLimitConfig ()Lio/getstream/chat/android/client/api/MessageLimitConfig;
Expand Down Expand Up @@ -1001,6 +1004,37 @@ public final class io/getstream/chat/android/client/audio/WaveformExtractorKt {
public static final fun isEof (Landroid/media/MediaCodec$BufferInfo;)Z
}

public final class io/getstream/chat/android/client/cache/StreamCacheConfig {
public fun <init> ()V
public fun <init> (Lio/getstream/chat/android/client/cache/VideoCacheConfig;)V
public synthetic fun <init> (Lio/getstream/chat/android/client/cache/VideoCacheConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lio/getstream/chat/android/client/cache/VideoCacheConfig;
public final fun copy (Lio/getstream/chat/android/client/cache/VideoCacheConfig;)Lio/getstream/chat/android/client/cache/StreamCacheConfig;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/cache/StreamCacheConfig;Lio/getstream/chat/android/client/cache/VideoCacheConfig;ILjava/lang/Object;)Lio/getstream/chat/android/client/cache/StreamCacheConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getVideo ()Lio/getstream/chat/android/client/cache/VideoCacheConfig;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/client/cache/VideoCacheConfig {
public static final field Companion Lio/getstream/chat/android/client/cache/VideoCacheConfig$Companion;
public static final field DEFAULT_MAX_SIZE_BYTES J
public fun <init> ()V
public fun <init> (J)V
public synthetic fun <init> (JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()J
public final fun copy (J)Lio/getstream/chat/android/client/cache/VideoCacheConfig;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/cache/VideoCacheConfig;JILjava/lang/Object;)Lio/getstream/chat/android/client/cache/VideoCacheConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getMaxSizeBytes ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/client/cache/VideoCacheConfig$Companion {
}

public abstract interface class io/getstream/chat/android/client/cdn/CDN {
public fun fileRequest (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun fileRequest$suspendImpl (Lio/getstream/chat/android/client/cdn/CDN;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import io.getstream.chat.android.client.attachment.prepareForUpload
import io.getstream.chat.android.client.audio.AudioPlayer
import io.getstream.chat.android.client.audio.NativeMediaPlayerImpl
import io.getstream.chat.android.client.audio.StreamAudioPlayer
import io.getstream.chat.android.client.cache.internal.VideoMediaCache
import io.getstream.chat.android.client.cdn.CDN
import io.getstream.chat.android.client.cdn.internal.StreamMediaDataSource
import io.getstream.chat.android.client.channel.ChannelClient
Expand Down Expand Up @@ -304,6 +305,8 @@ internal constructor(
internal val messageReceiptManager: MessageReceiptManager,
@InternalStreamChatApi
public val cdn: CDN? = null,
@InternalStreamChatApi
public val videoCache: VideoMediaCache? = null,
) {
private val logger by taggedLogger(TAG)
private val fileManager = StreamFileManager()
Expand Down Expand Up @@ -1511,12 +1514,22 @@ internal constructor(
public fun clearCacheAndTemporaryFiles(context: Context): Call<Unit> =
CoroutineCall(clientScope) {
logger.d { "[clearCacheAndTemporaryFiles] Clearing all cache and temporary files" }
// Clear the video cache for its directory atomically: if a live cache owns the
// directory, clear it in place (keeps the SimpleCache alive so playback continues to
// work); otherwise delete the directory from disk. Deciding and deleting under the
// registry lock prevents a concurrent client build from registering a new cache for the
// directory mid-delete, which would corrupt Media3's on-disk index and lock.
val videoCacheResult = VideoMediaCache.clearOrDelete(
cacheDir = fileManager.getVideoCache(context),
deleteOnDisk = { 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
cacheResult is Result.Failure -> cacheResult
externalStorageResult is Result.Failure -> externalStorageResult
else -> Result.Success(Unit)
Expand Down Expand Up @@ -5274,7 +5287,10 @@ internal constructor(
val api = module.api()
val appSettingsManager = AppSettingManager(api)

val mediaDataSourceFactory = StreamMediaDataSource.factory(appContext, cdn)
val videoCache = chatClientConfig.cacheConfig.video?.let {
VideoMediaCache.create(appContext, StreamFileManager().getVideoCache(appContext), it)
}
val mediaDataSourceFactory = StreamMediaDataSource.factory(appContext, cdn, videoCache)
val audioPlayer: AudioPlayer = StreamAudioPlayer(
mediaPlayer = NativeMediaPlayerImpl(mediaDataSourceFactory) {
ExoPlayer.Builder(appContext)
Expand Down Expand Up @@ -5332,6 +5348,7 @@ internal constructor(
api = api,
),
cdn = cdn,
videoCache = videoCache,
).apply {
attachmentsSender = AttachmentsSender(
context = appContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.getstream.chat.android.client.api

import io.getstream.chat.android.client.ChatClient
import io.getstream.chat.android.client.cache.StreamCacheConfig
import io.getstream.chat.android.client.setup.state.ClientState
import io.getstream.chat.android.models.TimeDuration

Expand Down Expand Up @@ -85,6 +86,9 @@ import io.getstream.chat.android.models.TimeDuration
* fall back to the default DTO-based parser, so behavior is preserved for events the fast path does not yet handle.
* Currently supported event types: `message.new`. Disabled by default. The set of supported event types may grow over
* time.
*
* @param cacheConfig Configuration for the SDK's user-configurable on-disk caches. By default all caches are off. See
* [StreamCacheConfig] for the available cache types and their options.
*/
public data class ChatClientConfig @JvmOverloads constructor(
public val offlineEnabled: Boolean = true,
Expand All @@ -96,6 +100,7 @@ public data class ChatClientConfig @JvmOverloads constructor(
public val messageLimitConfig: MessageLimitConfig = MessageLimitConfig(),
public val useLegacyChannelLogic: Boolean = false,
public val fastEventParsing: Boolean = false,
public val cacheConfig: StreamCacheConfig = StreamCacheConfig(),
)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.cache

/**
* Bundles the per-cache configurations exposed by the Stream Chat SDK.
*
* Set this on [io.getstream.chat.android.client.api.ChatClientConfig.cacheConfig] to configure the
* on-disk caches.
*
* @param video Configuration for the video playback cache used by SDK.
*/
public data class StreamCacheConfig(
public val video: VideoCacheConfig? = null,
)

/**
* Configuration for the on-disk cache used when streaming video attachments.
*
* 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.
Comment thread
andremion marked this conversation as resolved.
*
* 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.
*/
public data class VideoCacheConfig(
public val maxSizeBytes: Long = DEFAULT_MAX_SIZE_BYTES,
) {
init {
require(maxSizeBytes > 0) { "maxSizeBytes must be > 0, got $maxSizeBytes" }
}

public companion object {
/** Default cap of 150 MB. */
public const val DEFAULT_MAX_SIZE_BYTES: Long = 150L * 1024 * 1024
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.cache.internal

import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.cache.CacheDataSource
import io.getstream.chat.android.client.cdn.internal.CDNDataSourceFactory

/**
* A [DataSource.Factory] that serves video bytes from a [VideoMediaCache] on hit and delegates
* to [upstreamFactory] on miss, writing the fetched bytes back into the cache.
*
* Cache entries are keyed by the URI with its query stripped, so rotating signature/expiry
* parameters on the same path resolve to the same cache entry. The full [DataSpec] still flows
* to [upstreamFactory] on a miss, so a custom CDN sees the original URL and can re-sign or
* rewrite it. A caller-supplied [DataSpec.key] takes precedence over the URI-derived key.
*
* @param videoCache The cache that holds the cached video spans.
* @param upstreamFactory Factory invoked on cache miss (typically the [CDNDataSourceFactory] when
* a custom CDN is configured, or the base data source otherwise).
*/
@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()

Check warning on line 51 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cache/internal/VideoCacheDataSourceFactory.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with interface delegation using "by" in the class header.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AZ8oOFYIsZkITzP56m5l&open=AZ8oOFYIsZkITzP56m5l&pullRequest=6542

/**
* 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()
}
Loading
Loading