From ad6d158f4340b9b163ea7290d65488b50bd2ad97 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:09:52 +0300 Subject: [PATCH] Route player HTTP traffic through Cronet for QUIC/connection migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Cronet (org.chromium.net:cronet-embedded + Google's cronet-okhttp transport adapter) as an OkHttp network interceptor on the player's two HTTP clients (cache pre-warm + main ExoPlayer data source). All existing interceptor logic — header injection, Dolby Vision manifest rewriting, localhost proxy timeouts — is untouched since Cronet only replaces the final transport, not the OkHttp API surface. This mainly benefits direct-URL/debrid streams (real TLS connections), which can now survive network handoffs (wifi<->cellular) and lossy connections without a full reconnect/rebuffer. Torrent-proxied streams stay on loopback where this has no effect either way. Falls back to plain OkHttp transparently if Cronet engine creation fails for any reason (unsupported ABI, native lib issue, etc.) so there's no regression risk. --- gradle/libs.versions.toml | 4 +++ player/build.gradle.kts | 2 ++ .../com/fluxa/app/player/CronetTransport.kt | 31 +++++++++++++++++++ .../fluxa/app/player/MediaPlayerController.kt | 2 ++ 4 files changed, 39 insertions(+) create mode 100644 player/src/androidMain/kotlin/com/fluxa/app/player/CronetTransport.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22b8a81..f615157 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,8 @@ jellyfinMedia3 = "1.9.0+1" mpv = "1.0.0" retrofit = "3.0.0" okhttp = "5.4.0" +cronetEmbedded = "143.7445.0" +cronetOkhttp = "0.1.1" coil3 = "3.4.0" room = "2.8.4" jna = "5.17.0" @@ -106,6 +108,8 @@ retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor",version.ref = "okhttp" } okhttp-doh = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps",version.ref = "okhttp" } okhttp-mockwebserver= { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" } +cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronetEmbedded" } +cronet-okhttp = { group = "com.google.net.cronet", name = "cronet-okhttp", version.ref = "cronetOkhttp" } # Coil coil3 = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil3" } diff --git a/player/build.gradle.kts b/player/build.gradle.kts index 21814de..6b27066 100644 --- a/player/build.gradle.kts +++ b/player/build.gradle.kts @@ -51,6 +51,8 @@ kotlin { implementation(libs.mpv) implementation(libs.bundles.retrofit) implementation(libs.okhttp.logging) + implementation(libs.cronet.embedded) + implementation(libs.cronet.okhttp) } } } diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/CronetTransport.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/CronetTransport.kt new file mode 100644 index 0000000..28ab6ad --- /dev/null +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/CronetTransport.kt @@ -0,0 +1,31 @@ +package com.fluxa.app.player + +import android.content.Context +import android.util.Log +import com.google.net.cronet.okhttptransport.CronetInterceptor +import okhttp3.Interceptor +import org.chromium.net.CronetEngine + +private var cronetEngine: CronetEngine? = null +private var cronetEngineInitAttempted = false + +private fun sharedCronetEngine(context: Context): CronetEngine? { + if (!cronetEngineInitAttempted) { + cronetEngineInitAttempted = true + cronetEngine = runCatching { + CronetEngine.Builder(context.applicationContext) + .enableHttp2(true) + .enableQuic(true) + .enableBrotli(true) + .build() + }.onFailure { + Log.w("CronetTransport", "Cronet engine unavailable, staying on OkHttp", it) + }.getOrNull() + } + return cronetEngine +} + +fun cronetTransportInterceptor(context: Context): Interceptor? = + sharedCronetEngine(context)?.let { engine -> + runCatching { CronetInterceptor.newBuilder(engine).build() }.getOrNull() + } diff --git a/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt b/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt index 79b6da7..5d60a89 100644 --- a/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt +++ b/player/src/androidMain/kotlin/com/fluxa/app/player/MediaPlayerController.kt @@ -196,6 +196,7 @@ class MediaPlayerController(internal val context: Context, val exoPlayer: ExoPla if (!shouldUsePlayerDiskCache(uri)) return val okHttp = OkHttpClient.Builder() .callTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .apply { cronetTransportInterceptor(context)?.let { addInterceptor(it) } } .build() val upstream = OkHttpDataSource.Factory(okHttp) .setUserAgent(StreamRequestPolicy.DEFAULT_USER_AGENT) @@ -266,6 +267,7 @@ class MediaPlayerController(internal val context: Context, val exoPlayer: ExoPla chain.proceed(chain.request()) } } + .apply { cronetTransportInterceptor(context)?.let { addInterceptor(it) } } .build() val httpDataSourceFactory = OkHttpDataSource.Factory(okHttpClient)