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
22 changes: 22 additions & 0 deletions stream-chat-android-client/api/stream-chat-android-client.api
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ public final class io/getstream/chat/android/client/ChatClient$Builder : io/gets
public final fun appVersion (Ljava/lang/String;)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun baseUrl (Ljava/lang/String;)Lio/getstream/chat/android/client/ChatClient$Builder;
public fun build ()Lio/getstream/chat/android/client/ChatClient;
public final fun cdn (Lio/getstream/chat/android/client/cdn/CDN;)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun cdnUrl (Ljava/lang/String;)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun clientDebugger (Lio/getstream/chat/android/client/debugger/ChatClientDebugger;)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun credentialStorage (Lio/getstream/chat/android/client/user/storage/UserCredentialStorage;)Lio/getstream/chat/android/client/ChatClient$Builder;
Expand Down Expand Up @@ -711,6 +712,27 @@ public final class io/getstream/chat/android/client/audio/WaveformExtractorKt {
public static final fun isEof (Landroid/media/MediaCodec$BufferInfo;)Z
}

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;
public fun imageRequest (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun imageRequest$suspendImpl (Lio/getstream/chat/android/client/cdn/CDN;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/getstream/chat/android/client/cdn/CDNRequest {
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lio/getstream/chat/android/client/cdn/CDNRequest;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/cdn/CDNRequest;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/getstream/chat/android/client/cdn/CDNRequest;
public fun equals (Ljava/lang/Object;)Z
public final fun getHeaders ()Ljava/util/Map;
public final fun getUrl ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/client/channel/ChannelClient {
public final fun acceptInvite (Ljava/lang/String;)Lio/getstream/result/call/Call;
public final fun addMembers (Lio/getstream/chat/android/client/query/AddMembersParams;)Lio/getstream/result/call/Call;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
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.cdn.CDN
import io.getstream.chat.android.client.cdn.internal.StreamMediaDataSource
import io.getstream.chat.android.client.channel.ChannelClient
import io.getstream.chat.android.client.channel.state.ChannelStateLogicProvider
import io.getstream.chat.android.client.clientstate.DisconnectCause
Expand Down Expand Up @@ -293,6 +295,8 @@
private val repository: ChatClientRepository,
private val messageReceiptReporter: MessageReceiptReporter,
internal val messageReceiptManager: MessageReceiptManager,
@InternalStreamChatApi
public val cdn: CDN? = null,
) {
private val logger by taggedLogger(TAG)
private val fileManager = StreamFileManager()
Expand Down Expand Up @@ -4731,6 +4735,7 @@
private var uploadAttachmentsNetworkType = UploadAttachmentsNetworkType.CONNECTED
private var fileTransformer: FileTransformer = NoOpFileTransformer
private var apiModelTransformers: ApiModelTransformers = ApiModelTransformers()
private var cdn: CDN? = null
private var appName: String? = null
private var appVersion: String? = null

Expand Down Expand Up @@ -4859,8 +4864,12 @@
*
* @param shareFileDownloadRequestInterceptor Your [Interceptor] implementation for the share file download
* call.
* @deprecated Use [io.getstream.chat.android.client.cdn.CDN] instead. Configure a custom CDN via
* [io.getstream.chat.android.client.ChatClient.Builder.cdn] to provide headers and transform URLs
* for all image, file, and download requests.
*/
@Deprecated("Use CDN instead. Configure via ChatClient.Builder.cdn().")
public fun shareFileDownloadRequestInterceptor(shareFileDownloadRequestInterceptor: Interceptor): Builder {

Check warning on line 4872 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AZ0l0WMawFGn-mp-IXH9&open=AZ0l0WMawFGn-mp-IXH9&pullRequest=6295
this.shareFileDownloadRequestInterceptor = shareFileDownloadRequestInterceptor
return this
}
Expand Down Expand Up @@ -4930,6 +4939,15 @@
forceWsUrl = value
}

/**
* Sets a custom [CDN] implementation to be used by the client.
*
* @param cdn The custom CDN implementation.
*/
public fun cdn(cdn: CDN): Builder = apply {
this.cdn = cdn
}

/**
* Sets the CDN URL to be used by the client.
*/
Expand Down Expand Up @@ -5078,6 +5096,7 @@
fileUploader = fileUploader,
sendMessageInterceptor = sendMessageInterceptor,
shareFileDownloadRequestInterceptor = shareFileDownloadRequestInterceptor,
cdn = cdn,
tokenManager = tokenManager,
customOkHttpClient = customOkHttpClient,
clientDebugger = clientDebugger,
Expand All @@ -5090,8 +5109,9 @@
val api = module.api()
val appSettingsManager = AppSettingManager(api)

val mediaDataSourceFactory = StreamMediaDataSource.factory(appContext, cdn)
val audioPlayer: AudioPlayer = StreamAudioPlayer(
mediaPlayer = NativeMediaPlayerImpl(appContext) {
mediaPlayer = NativeMediaPlayerImpl(mediaDataSourceFactory) {
ExoPlayer.Builder(appContext)
.setAudioAttributes(
AudioAttributes.Builder()
Expand Down Expand Up @@ -5143,6 +5163,7 @@
messageReceiptRepository = repository,
api = api,
),
cdn = cdn,
).apply {
attachmentsSender = AttachmentsSender(
context = appContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@

package io.getstream.chat.android.client.audio

import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
Expand Down Expand Up @@ -358,12 +357,12 @@ public enum class NativeMediaPlayerState {
/**
* Default implementation of [NativeMediaPlayer] based on ExoPlayer.
*
* @param context The context.
* @param dataSourceFactory The data source factory used for creating media sources.
* @param builder A builder function to create an [ExoPlayer] instance.
*/
@OptIn(UnstableApi::class)
internal class NativeMediaPlayerImpl(
context: Context,
dataSourceFactory: DataSource.Factory,
private val builder: () -> ExoPlayer,
) : NativeMediaPlayer {

Expand Down Expand Up @@ -392,7 +391,7 @@ internal class NativeMediaPlayerImpl(
* For more info see [ExoPlayer Progressive](https://developer.android.com/media/media3/exoplayer/progressive).
*/
private val mediaSourceFactory: MediaSource.Factory = ProgressiveMediaSource.Factory(
DefaultDataSource.Factory(context),
dataSourceFactory,
DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.cdn

/**
* Class defining a CDN (Content Delivery Network) interface.
* Override to transform requests loading images/files from the custom CDN.
*/
public interface CDN {

/**
* Transforms a request for loading an image from the CDN.
*
* Implementations that perform blocking or network I/O must use `withContext` to switch to the
* appropriate dispatcher (e.g. `Dispatchers.IO`).
*
* @param url Original CDN url for the image.
* @return A [CDNRequest] holding the modified request URL and/or custom headers to include with the request.
*/
public suspend fun imageRequest(url: String): CDNRequest = CDNRequest(url)

/**
* Transforms a request for loading a non-image file from the CDN.
*
* Implementations that perform blocking or network I/O must use `withContext` to switch to the
* appropriate dispatcher (e.g. `Dispatchers.IO`).
*
* @param url Original CDN url for the file.
* @return A [CDNRequest] holding the modified request URL and/or custom headers to include with the request.
*/
public suspend fun fileRequest(url: String): CDNRequest = CDNRequest(url)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.cdn

/**
* Model representing the request for loading a file from a CDN.
*
* @param url Url of the file to load.
* @param headers Map of headers added to the request.
*/
public data class CDNRequest(
val url: String,
val headers: Map<String, String>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.cdn.internal

import android.net.Uri
import androidx.core.net.toUri
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.TransferListener
import io.getstream.chat.android.client.cdn.CDN
import io.getstream.chat.android.client.cdn.CDNRequest
import io.getstream.log.taggedLogger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking

/**
* A [DataSource.Factory] that creates [CDNDataSource] instances which transform
* media requests through the [CDN.fileRequest] method before delegating to an upstream data source.
*
* @param cdn The CDN used to transform file request URLs and headers.
* @param upstreamFactory The factory for creating the upstream data source that performs the actual HTTP requests.
*/
@UnstableApi
internal class CDNDataSourceFactory(
private val cdn: CDN,
private val upstreamFactory: DataSource.Factory = DefaultHttpDataSource.Factory(),
) : DataSource.Factory {
override fun createDataSource(): DataSource {
return CDNDataSource(cdn, upstreamFactory.createDataSource())
}
}

/**
* A [DataSource] that transforms media requests through [CDN.fileRequest] before
* delegating to an upstream data source. This allows custom CDN implementations
* to rewrite URLs and inject headers for video/audio/voice recording playback via ExoPlayer.
*
* [CDN.fileRequest] is a suspend function and is called via [runBlocking] on [Dispatchers.IO].
* This is safe because ExoPlayer always calls [open] from its loader thread, never the main thread.
*/
@UnstableApi
private class CDNDataSource(
private val cdn: CDN,
private val upstream: DataSource,
) : DataSource {

private val logger by taggedLogger("Chat:CDNDataSource")

override fun open(dataSpec: DataSpec): Long {
val scheme = dataSpec.uri.scheme
if (scheme != "http" && scheme != "https") {
return upstream.open(dataSpec)
}
val url = dataSpec.uri.toString()
val cdnRequest = try {
runBlocking {
cdn.fileRequest(url)
}
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
logger.e(e) { "[open] CDN.fileRequest() failed for url: $url. Falling back to original request." }
CDNRequest(url)
}
val mergedHeaders = buildMap {
putAll(dataSpec.httpRequestHeaders)
cdnRequest.headers?.let { putAll(it) }
}
val transformedSpec = dataSpec.buildUpon()
.setUri(cdnRequest.url.toUri())
.setHttpRequestHeaders(mergedHeaders)
.build()
return upstream.open(transformedSpec)
}

override fun read(buffer: ByteArray, offset: Int, length: Int): Int =

Check warning on line 90 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/CDNDataSourceFactory.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=AZ0l0WG7wFGn-mp-IXH4&open=AZ0l0WG7wFGn-mp-IXH4&pullRequest=6295
upstream.read(buffer, offset, length)

override fun close() {

Check warning on line 93 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/CDNDataSourceFactory.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=AZ0l0WG7wFGn-mp-IXH5&open=AZ0l0WG7wFGn-mp-IXH5&pullRequest=6295
upstream.close()
}

override fun getUri(): Uri? = upstream.uri

override fun getResponseHeaders(): Map<String, List<String>> = upstream.responseHeaders

override fun addTransferListener(transferListener: TransferListener) {

Check warning on line 101 in stream-chat-android-client/src/main/java/io/getstream/chat/android/client/cdn/internal/CDNDataSourceFactory.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=AZ0l0WG7wFGn-mp-IXH6&open=AZ0l0WG7wFGn-mp-IXH6&pullRequest=6295
upstream.addTransferListener(transferListener)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.cdn.internal

import io.getstream.chat.android.client.cdn.CDN
import io.getstream.chat.android.client.cdn.CDNRequest
import io.getstream.log.taggedLogger
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response

/**
* OkHttp interceptor applying transformations to CDN requests.
*/
internal class CDNOkHttpInterceptor(private val cdn: CDN) : Interceptor {

private val logger by taggedLogger("Chat:CDNOkHttpInterceptor")

override fun intercept(chain: Interceptor.Chain): Response {
val originalUrl = chain.request().url.toString()
val (url, headers) = try {
runBlocking {
cdn.fileRequest(originalUrl)
}
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
logger.e(e) {
"[intercept] CDN.fileRequest() failed for url: $originalUrl. " +
"Falling back to original request."
}
CDNRequest(originalUrl)
}
val request = chain.request().newBuilder()
.url(url)
.apply {
headers?.forEach {
header(it.key, it.value)
}
}
.build()
return chain.proceed(request)
}
}
Loading
Loading