From 73c29ccab8dc1cbde105f49bf4ddd4911f3aca89 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:44:31 +0530 Subject: [PATCH] ref: playbackDataSourceFactory to be in parity with Nuvio TV --- ...atformPlaybackDataSourceFactory.android.kt | 15 +-- .../player/PlayerPlaybackNetworking.kt | 104 ++++++++++++++++++ ...atformPlaybackDataSourceFactory.android.kt | 15 +-- 3 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlaybackNetworking.kt diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt index 513beda6..5c24c3ce 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt @@ -3,18 +3,9 @@ package com.nuvio.app.features.player import android.content.Context import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultDataSource -import androidx.media3.datasource.DefaultHttpDataSource import com.nuvio.app.features.trailer.YoutubeChunkedDataSourceFactory internal object PlatformPlaybackDataSourceFactory { - - private val DEFAULT_STREAM_HEADERS = mapOf( - "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/120.0.0.0 Safari/537.36", - "Referer" to "https://www.strem.io/" - ) - fun create( context: Context, defaultRequestHeaders: Map, @@ -24,9 +15,7 @@ internal object PlatformPlaybackDataSourceFactory { val networkFactory: DataSource.Factory = if (useYoutubeChunkedPlayback) { YoutubeChunkedDataSourceFactory(defaultRequestHeaders = defaultRequestHeaders) } else { - DefaultHttpDataSource.Factory() - .setAllowCrossProtocolRedirects(true) - .setDefaultRequestProperties(DEFAULT_STREAM_HEADERS + defaultRequestHeaders) + PlayerPlaybackNetworking.createHttpDataSourceFactory(defaultRequestHeaders) } val baseFactory: DataSource.Factory = DefaultDataSource.Factory(context, networkFactory) return if (defaultResponseHeaders.isEmpty()) { @@ -38,4 +27,4 @@ internal object PlatformPlaybackDataSourceFactory { ) } } -} +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlaybackNetworking.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlaybackNetworking.kt new file mode 100644 index 00000000..01653e47 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlaybackNetworking.kt @@ -0,0 +1,104 @@ +package com.nuvio.app.features.player + +import android.content.Context +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.okhttp.OkHttpDataSource +import com.nuvio.app.core.network.IPv4FirstDns +import okhttp3.OkHttpClient +import java.net.HttpURLConnection +import java.net.URL +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +internal object PlayerPlaybackNetworking { + private val DEFAULT_STREAM_HEADERS = mapOf( + "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/120.0.0.0 Safari/537.36", + + ) + + internal const val DEFAULT_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + + private val trustAllManager = object : X509TrustManager { + override fun checkClientTrusted(chain: Array?, authType: String?) = Unit + + override fun checkServerTrusted(chain: Array?, authType: String?) = Unit + + override fun getAcceptedIssuers(): Array = emptyArray() + } + + private val playbackHostnameVerifier = HostnameVerifier { _, _ -> true } + + private val sslContext: SSLContext by lazy { + SSLContext.getInstance("TLS").apply { + init(null, arrayOf(trustAllManager), SecureRandom()) + } + } + + private val playbackHttpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .dns(IPv4FirstDns()) + .sslSocketFactory(sslContext.socketFactory, trustAllManager) + .hostnameVerifier(playbackHostnameVerifier) + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .retryOnConnectionFailure(true) + .build() + } + + fun createHttpDataSourceFactory(defaultHeaders: Map = emptyMap()): DataSource.Factory { + val mergedHeaders = DEFAULT_STREAM_HEADERS + defaultHeaders + return OkHttpDataSource.Factory(playbackHttpClient).apply { + setDefaultRequestProperties(mergedHeaders) + setUserAgent(DEFAULT_USER_AGENT) + } + } + + fun createDataSourceFactory( + context: Context, + defaultHeaders: Map = emptyMap(), + ): DataSource.Factory { + return DefaultDataSource.Factory(context, createHttpDataSourceFactory(defaultHeaders)) + } + + fun openConnection( + url: String, + headers: Map, + method: String, + connectTimeoutMs: Int, + readTimeoutMs: Int, + range: String? = null, + ): HttpURLConnection { + val mergedHeaders = DEFAULT_STREAM_HEADERS + headers + return (URL(url).openConnection() as HttpURLConnection).apply { + if (this is HttpsURLConnection) { + sslSocketFactory = sslContext.socketFactory + hostnameVerifier = playbackHostnameVerifier + } + instanceFollowRedirects = true + connectTimeout = connectTimeoutMs + readTimeout = readTimeoutMs + requestMethod = method + setRequestProperty("User-Agent", mergedHeaders["User-Agent"] ?: DEFAULT_USER_AGENT) + mergedHeaders.forEach { (key, value) -> + if (key.equals("Range", ignoreCase = true)) return@forEach + if (key.equals("User-Agent", ignoreCase = true)) return@forEach + setRequestProperty(key, value) + } + range?.let { setRequestProperty("Range", it) } + } + } +} diff --git a/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt b/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt index 29f34359..60a7a124 100644 --- a/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt +++ b/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/features/player/PlatformPlaybackDataSourceFactory.android.kt @@ -3,26 +3,15 @@ package com.nuvio.app.features.player import android.content.Context import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultDataSource -import androidx.media3.datasource.DefaultHttpDataSource internal object PlatformPlaybackDataSourceFactory { - - private val DEFAULT_STREAM_HEADERS = mapOf( - "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/120.0.0.0 Safari/537.36", - "Referer" to "https://www.strem.io/" - ) - fun create( context: Context, defaultRequestHeaders: Map, defaultResponseHeaders: Map, useYoutubeChunkedPlayback: Boolean, ): DataSource.Factory { - val httpFactory = DefaultHttpDataSource.Factory() - .setAllowCrossProtocolRedirects(true) - .setDefaultRequestProperties(DEFAULT_STREAM_HEADERS + defaultRequestHeaders) + val httpFactory = PlayerPlaybackNetworking.createHttpDataSourceFactory(defaultRequestHeaders) val baseFactory: DataSource.Factory = DefaultDataSource.Factory(context, httpFactory) return if (defaultResponseHeaders.isEmpty()) { baseFactory @@ -33,4 +22,4 @@ internal object PlatformPlaybackDataSourceFactory { ) } } -} +} \ No newline at end of file