diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt index 4127799e8f4..54796768508 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt @@ -1,23 +1,41 @@ package com.lagradost.cloudstream3.network +import okhttp3.Dns import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps +import java.net.Inet4Address import java.net.InetAddress /** * Based on https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt */ +/** + * Strips IPv6 addresses from any DNS result, returning IPv4-only. + * Android TV firmware often has a broken IPv6 stack — looks available but + * connections silently fail or hang, causing ExoPlayer errors 2004/2001 + * while the same stream works fine on mobile over the same Wi-Fi. + */ +class IPv4OnlyDns(private val delegate: Dns = Dns.SYSTEM) : Dns { + override fun lookup(hostname: String): List { + val addresses = delegate.lookup(hostname) + val ipv4Only = addresses.filterIsInstance() + return ipv4Only.ifEmpty { addresses } + } +} + fun OkHttpClient.Builder.addGenericDns(url: String, ips: List) = dns( - DnsOverHttps - .Builder() - .client(build()) - .url(url.toHttpUrl()) - .bootstrapDnsHosts( - ips.map { InetAddress.getByName(it) } - ) - .build() + IPv4OnlyDns( + DnsOverHttps + .Builder() + .client(build()) + .url(url.toHttpUrl()) + .bootstrapDnsHosts( + ips.map { InetAddress.getByName(it) } + ) + .build() + ) ) fun OkHttpClient.Builder.addGoogleDns() = ( diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index e4924590458..4bba7615c51 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -41,6 +41,11 @@ fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClie val baseClient = OkHttpClient.Builder() .followRedirects(true) .followSslRedirects(true) + // Explicit timeouts — OkHttp's 10s default is too short for slower + // TV network stacks, a known contributor to ExoPlayer error 2004/2001. + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .apply { if (ignoreSSL) { ignoreAllSSLErrors() @@ -63,6 +68,9 @@ fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClie 6 -> addQuad9Dns() 7 -> addDnsSbDns() 8 -> addCanadianShieldDns() + // Default (system DNS): still wrap with IPv4OnlyDns so Android TV's + // often-broken IPv6 stack doesn't get tried even with no DoH selected. + else -> dns(IPv4OnlyDns()) } } // Needs to be build as otherwise the other builders will change this object @@ -87,4 +95,4 @@ fun getHeaders( }) else mapOf() val tempHeaders = (DEFAULT_HEADERS + headers + cookieMap) return tempHeaders.toHeaders() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index b63191c19a9..e37a46a5094 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -93,6 +93,7 @@ import com.lagradost.cloudstream3.ui.player.live.LiveHelper import com.lagradost.cloudstream3.ui.player.live.PREFERRED_LIVE_OFFSET import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle @@ -669,7 +670,9 @@ class CS3IPlayer : IPlayer { } companion object { - private const val CRONET_TIMEOUT_MS = 15_000 + // 30s instead of 15s — TV network stacks are slower than phones + // and were timing out before the stream could even start. + private const val CRONET_TIMEOUT_MS = 30_000 /** * Single shared engine, to minimize the overhead of maintaining many as: @@ -765,6 +768,18 @@ class CS3IPlayer : IPlayer { } fun tryCreateEngine(context: Context, diskCacheSize: Long): CronetEngine? { + // Android TV: skip Cronet entirely and fall back to OkHttp instead. + // Cronet manages its own native networking stack (its own DNS resolution, + // its own IPv6 handling, its own QUIC/UDP transport) completely independent + // of OkHttp — none of OkHttp's IPv4-only DNS fix or timeout settings reach + // Cronet at all. Many Android TV boxes have broken IPv6 and/or block UDP, + // which Cronet has no public API to work around, but OkHttp (via + // IPv4OnlyDns in RequestsHelper/DohProviders) already handles correctly. + // This is the root cause behind ExoPlayer errors 2004/2001 on TV. + if (isLayout(TV)) { + return null + } + // Fast case, no need to recreate it cronetEngine?.let { return it @@ -780,7 +795,10 @@ class CS3IPlayer : IPlayer { CronetEngine.Builder(context) .enableBrotli(true) .enableHttp2(true) - .enableQuic(true) + // QUIC runs over UDP, which many routers/ISPs throttle or block, + // and which Cronet handles with no app-level override. Disabled as + // defense-in-depth in case TV detection above ever misses a device. + .enableQuic(false) .setStoragePath(cacheDirectory.absolutePath) .setLibraryLoader(null) .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, diskCacheSize)