Route player HTTP traffic through Cronet for QUIC/connection migration

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.
This commit is contained in:
KhooLy 2026-07-19 17:09:52 +03:00
parent 35f9c67092
commit ad6d158f43
4 changed files with 39 additions and 0 deletions

View file

@ -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" }

View file

@ -51,6 +51,8 @@ kotlin {
implementation(libs.mpv)
implementation(libs.bundles.retrofit)
implementation(libs.okhttp.logging)
implementation(libs.cronet.embedded)
implementation(libs.cronet.okhttp)
}
}
}

View file

@ -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()
}

View file

@ -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)