-
Notifications
You must be signed in to change notification settings - Fork 313
Add support for intercepting CDN file requests #6295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aaf899c
4504038
6b5efa8
2f81550
1f940a0
9f691b9
d4f27af
799a1a7
8006ac2
d6e962c
ea1df0a
788a812
5e33e08
b884838
c074641
2dbdd22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| 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
|
||
| 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
|
||
| 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." | ||
| } | ||
VelikovPetar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| CDNRequest(originalUrl) | ||
| } | ||
| val request = chain.request().newBuilder() | ||
| .url(url) | ||
| .apply { | ||
| headers?.forEach { | ||
| header(it.key, it.value) | ||
| } | ||
| } | ||
| .build() | ||
| return chain.proceed(request) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.