mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-02 13:44:54 +00:00
feat: add support for response headers in playback data source handling
This commit is contained in:
parent
c4d4f59db0
commit
42a4ee2d66
11 changed files with 158 additions and 6 deletions
|
|
@ -7,11 +7,21 @@ import com.nuvio.app.features.trailer.YoutubeChunkedDataSourceFactory
|
|||
internal object PlatformPlaybackDataSourceFactory {
|
||||
fun create(
|
||||
defaultRequestHeaders: Map<String, String>,
|
||||
defaultResponseHeaders: Map<String, String>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
): DataSource.Factory =
|
||||
if (useYoutubeChunkedPlayback) {
|
||||
): DataSource.Factory {
|
||||
val baseFactory: DataSource.Factory = if (useYoutubeChunkedPlayback) {
|
||||
YoutubeChunkedDataSourceFactory(defaultRequestHeaders = defaultRequestHeaders)
|
||||
} else {
|
||||
DefaultHttpDataSource.Factory().setDefaultRequestProperties(defaultRequestHeaders)
|
||||
}
|
||||
return if (defaultResponseHeaders.isEmpty()) {
|
||||
baseFactory
|
||||
} else {
|
||||
ResponseHeaderOverridingDataSourceFactory(
|
||||
upstreamFactory = baseFactory,
|
||||
defaultResponseHeaders = defaultResponseHeaders,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceUrl: String,
|
||||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
|
|
@ -86,8 +87,11 @@ actual fun PlatformPlayerSurface(
|
|||
val sanitizedSourceHeaders = remember(sourceHeaders) {
|
||||
sanitizePlaybackHeaders(sourceHeaders)
|
||||
}
|
||||
val sanitizedSourceResponseHeaders = remember(sourceResponseHeaders) {
|
||||
sanitizePlaybackResponseHeaders(sourceResponseHeaders)
|
||||
}
|
||||
|
||||
val exoPlayer = remember(sourceUrl, sourceAudioUrl, sanitizedSourceHeaders) {
|
||||
val exoPlayer = remember(sourceUrl, sourceAudioUrl, sanitizedSourceHeaders, sanitizedSourceResponseHeaders) {
|
||||
val renderersFactory = DefaultRenderersFactory(context)
|
||||
.setExtensionRendererMode(playerSettings.decoderPriority)
|
||||
.setMapDV7ToHevc(playerSettings.mapDV7ToHevc)
|
||||
|
|
@ -119,6 +123,7 @@ actual fun PlatformPlayerSurface(
|
|||
val mediaSourceFactory = DefaultMediaSourceFactory(
|
||||
PlatformPlaybackDataSourceFactory.create(
|
||||
defaultRequestHeaders = sanitizedSourceHeaders,
|
||||
defaultResponseHeaders = sanitizedSourceResponseHeaders,
|
||||
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
|
||||
),
|
||||
extractorsFactory,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DataSpec
|
||||
import androidx.media3.datasource.TransferListener
|
||||
|
||||
internal class ResponseHeaderOverridingDataSourceFactory(
|
||||
private val upstreamFactory: DataSource.Factory,
|
||||
private val defaultResponseHeaders: Map<String, String>,
|
||||
) : DataSource.Factory {
|
||||
override fun createDataSource(): DataSource =
|
||||
ResponseHeaderOverridingDataSource(
|
||||
upstream = upstreamFactory.createDataSource(),
|
||||
defaultResponseHeaders = defaultResponseHeaders,
|
||||
)
|
||||
}
|
||||
|
||||
private class ResponseHeaderOverridingDataSource(
|
||||
private val upstream: DataSource,
|
||||
private val defaultResponseHeaders: Map<String, String>,
|
||||
) : DataSource {
|
||||
|
||||
override fun addTransferListener(transferListener: TransferListener) {
|
||||
upstream.addTransferListener(transferListener)
|
||||
}
|
||||
|
||||
override fun open(dataSpec: DataSpec): Long = upstream.open(dataSpec)
|
||||
|
||||
override fun read(buffer: ByteArray, offset: Int, length: Int): Int =
|
||||
upstream.read(buffer, offset, length)
|
||||
|
||||
override fun getUri(): Uri? = upstream.uri
|
||||
|
||||
override fun getResponseHeaders(): Map<String, List<String>> {
|
||||
val upstreamHeaders = upstream.responseHeaders
|
||||
if (defaultResponseHeaders.isEmpty()) return upstreamHeaders
|
||||
|
||||
val merged = LinkedHashMap<String, List<String>>(upstreamHeaders.size + defaultResponseHeaders.size)
|
||||
merged.putAll(upstreamHeaders)
|
||||
defaultResponseHeaders.forEach { (key, value) ->
|
||||
merged[key] = listOf(value)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
upstream.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,18 @@ import androidx.media3.datasource.DefaultHttpDataSource
|
|||
internal object PlatformPlaybackDataSourceFactory {
|
||||
fun create(
|
||||
defaultRequestHeaders: Map<String, String>,
|
||||
defaultResponseHeaders: Map<String, String>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
): DataSource.Factory =
|
||||
DefaultHttpDataSource.Factory().setDefaultRequestProperties(defaultRequestHeaders)
|
||||
): DataSource.Factory {
|
||||
val baseFactory = DefaultHttpDataSource.Factory()
|
||||
.setDefaultRequestProperties(defaultRequestHeaders)
|
||||
return if (defaultResponseHeaders.isEmpty()) {
|
||||
baseFactory
|
||||
} else {
|
||||
ResponseHeaderOverridingDataSourceFactory(
|
||||
upstreamFactory = baseFactory,
|
||||
defaultResponseHeaders = defaultResponseHeaders,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,7 @@ import com.nuvio.app.features.player.PlayerLaunchStore
|
|||
import com.nuvio.app.features.player.PlayerRoute
|
||||
import com.nuvio.app.features.player.PlayerScreen
|
||||
import com.nuvio.app.features.player.sanitizePlaybackHeaders
|
||||
import com.nuvio.app.features.player.sanitizePlaybackResponseHeaders
|
||||
import com.nuvio.app.features.profiles.NuvioProfile
|
||||
import com.nuvio.app.features.profiles.ProfileEditScreen
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
|
|
@ -901,6 +902,8 @@ private fun MainAppContent(
|
|||
PlayerLaunch(
|
||||
title = route.title,
|
||||
sourceUrl = cached.url,
|
||||
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(cached.responseHeaders),
|
||||
logo = route.logo,
|
||||
poster = route.poster,
|
||||
background = route.background,
|
||||
|
|
@ -947,6 +950,8 @@ private fun MainAppContent(
|
|||
streamName = stream.streamLabel,
|
||||
addonName = stream.addonName,
|
||||
addonId = stream.addonId,
|
||||
requestHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
responseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
|
|
@ -957,6 +962,7 @@ private fun MainAppContent(
|
|||
title = route.title,
|
||||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
logo = route.logo,
|
||||
poster = route.poster,
|
||||
background = route.background,
|
||||
|
|
@ -1024,6 +1030,8 @@ private fun MainAppContent(
|
|||
streamName = stream.streamLabel,
|
||||
addonName = stream.addonName,
|
||||
addonId = stream.addonId,
|
||||
requestHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
responseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
|
|
@ -1034,6 +1042,7 @@ private fun MainAppContent(
|
|||
title = route.title,
|
||||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
logo = route.logo,
|
||||
poster = route.poster,
|
||||
background = route.background,
|
||||
|
|
@ -1097,6 +1106,7 @@ private fun MainAppContent(
|
|||
sourceUrl = launch.sourceUrl,
|
||||
sourceAudioUrl = launch.sourceAudioUrl,
|
||||
sourceHeaders = launch.sourceHeaders,
|
||||
sourceResponseHeaders = launch.sourceResponseHeaders,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
|
|||
|
|
@ -35,11 +35,26 @@ internal fun sanitizePlaybackHeaders(headers: Map<String, String>?): Map<String,
|
|||
return sanitized
|
||||
}
|
||||
|
||||
internal fun sanitizePlaybackResponseHeaders(headers: Map<String, String>?): Map<String, String> {
|
||||
val rawHeaders = headers ?: return emptyMap()
|
||||
if (rawHeaders.isEmpty()) return emptyMap()
|
||||
|
||||
val sanitized = LinkedHashMap<String, String>(rawHeaders.size)
|
||||
rawHeaders.forEach { (rawKey, rawValue) ->
|
||||
val key = rawKey.trim()
|
||||
val value = rawValue.trim()
|
||||
if (key.isEmpty() || value.isEmpty()) return@forEach
|
||||
sanitized[key] = value
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun PlatformPlayerSurface(
|
||||
sourceUrl: String,
|
||||
sourceAudioUrl: String? = null,
|
||||
sourceHeaders: Map<String, String> = emptyMap(),
|
||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
useYoutubeChunkedPlayback: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
playWhenReady: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ data class PlayerLaunch(
|
|||
val sourceUrl: String,
|
||||
val sourceAudioUrl: String? = null,
|
||||
val sourceHeaders: Map<String, String> = emptyMap(),
|
||||
val sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
val logo: String? = null,
|
||||
val poster: String? = null,
|
||||
val background: String? = null,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ fun PlayerScreen(
|
|||
sourceUrl: String,
|
||||
sourceAudioUrl: String? = null,
|
||||
sourceHeaders: Map<String, String> = emptyMap(),
|
||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
providerName: String,
|
||||
streamTitle: String,
|
||||
streamSubtitle: String?,
|
||||
|
|
@ -132,6 +133,9 @@ fun PlayerScreen(
|
|||
var activeSourceHeaders by remember(sourceUrl, sourceHeaders) {
|
||||
mutableStateOf(sanitizePlaybackHeaders(sourceHeaders))
|
||||
}
|
||||
var activeSourceResponseHeaders by remember(sourceUrl, sourceResponseHeaders) {
|
||||
mutableStateOf(sanitizePlaybackResponseHeaders(sourceResponseHeaders))
|
||||
}
|
||||
var activeStreamTitle by rememberSaveable { mutableStateOf(streamTitle) }
|
||||
var activeStreamSubtitle by rememberSaveable { mutableStateOf(streamSubtitle) }
|
||||
var activeProviderName by rememberSaveable { mutableStateOf(providerName) }
|
||||
|
|
@ -522,6 +526,8 @@ fun PlayerScreen(
|
|||
streamName = stream.streamLabel,
|
||||
addonName = stream.addonName,
|
||||
addonId = stream.addonId,
|
||||
requestHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
responseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
|
|
@ -530,6 +536,7 @@ fun PlayerScreen(
|
|||
activeSourceUrl = url
|
||||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
||||
activeSourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response)
|
||||
activeStreamTitle = stream.streamLabel
|
||||
activeStreamSubtitle = stream.streamSubtitle
|
||||
activeProviderName = stream.addonName
|
||||
|
|
@ -577,6 +584,8 @@ fun PlayerScreen(
|
|||
streamName = stream.streamLabel,
|
||||
addonName = stream.addonName,
|
||||
addonId = stream.addonId,
|
||||
requestHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
responseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
|
|
@ -585,6 +594,7 @@ fun PlayerScreen(
|
|||
activeSourceUrl = url
|
||||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
||||
activeSourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response)
|
||||
activeStreamTitle = stream.streamLabel
|
||||
activeStreamSubtitle = stream.streamSubtitle
|
||||
activeProviderName = stream.addonName
|
||||
|
|
@ -740,7 +750,7 @@ fun PlayerScreen(
|
|||
controlsVisible = false
|
||||
}
|
||||
|
||||
LaunchedEffect(activeSourceUrl, activeSourceAudioUrl, activeSourceHeaders) {
|
||||
LaunchedEffect(activeSourceUrl, activeSourceAudioUrl, activeSourceHeaders, activeSourceResponseHeaders) {
|
||||
errorMessage = null
|
||||
playerController = null
|
||||
playerControllerSourceUrl = null
|
||||
|
|
@ -1103,6 +1113,7 @@ fun PlayerScreen(
|
|||
sourceUrl = activeSourceUrl,
|
||||
sourceAudioUrl = activeSourceAudioUrl,
|
||||
sourceHeaders = activeSourceHeaders,
|
||||
sourceResponseHeaders = activeSourceResponseHeaders,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
playWhenReady = shouldPlay,
|
||||
resizeMode = resizeMode,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ data class CachedStreamLink(
|
|||
val addonName: String,
|
||||
val addonId: String,
|
||||
val cachedAtMs: Long,
|
||||
val requestHeaders: Map<String, String> = emptyMap(),
|
||||
val responseHeaders: Map<String, String> = emptyMap(),
|
||||
val filename: String? = null,
|
||||
val videoSize: Long? = null,
|
||||
val bingeGroup: String? = null,
|
||||
|
|
@ -29,6 +31,8 @@ object StreamLinkCacheRepository {
|
|||
streamName: String,
|
||||
addonName: String,
|
||||
addonId: String,
|
||||
requestHeaders: Map<String, String> = emptyMap(),
|
||||
responseHeaders: Map<String, String> = emptyMap(),
|
||||
filename: String? = null,
|
||||
videoSize: Long? = null,
|
||||
bingeGroup: String? = null,
|
||||
|
|
@ -39,6 +43,8 @@ object StreamLinkCacheRepository {
|
|||
addonName = addonName,
|
||||
addonId = addonId,
|
||||
cachedAtMs = epochMs(),
|
||||
requestHeaders = requestHeaders,
|
||||
responseHeaders = responseHeaders,
|
||||
filename = filename,
|
||||
videoSize = videoSize,
|
||||
bingeGroup = bingeGroup,
|
||||
|
|
|
|||
|
|
@ -88,4 +88,35 @@ class StreamParserTest {
|
|||
val stream = streams.single()
|
||||
assertFalse(stream.behaviorHints.notWebReady)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse keeps proxy response headers`() {
|
||||
val streams = StreamParser.parse(
|
||||
payload =
|
||||
"""
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"url": "https://example.com/video.mp4",
|
||||
"behaviorHints": {
|
||||
"proxyHeaders": {
|
||||
"response": {
|
||||
"content-type": "video/mp4",
|
||||
"x-test": "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent(),
|
||||
addonName = "Addon",
|
||||
addonId = "addon.id",
|
||||
)
|
||||
|
||||
val responseHeaders = streams.single().behaviorHints.proxyHeaders?.response
|
||||
assertNotNull(responseHeaders)
|
||||
assertEquals("video/mp4", responseHeaders["content-type"])
|
||||
assertEquals("ok", responseHeaders["x-test"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceUrl: String,
|
||||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
|
|
@ -32,6 +33,7 @@ actual fun PlatformPlayerSurface(
|
|||
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
sanitizePlaybackResponseHeaders(sourceResponseHeaders)
|
||||
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
|
||||
val latestOnError = rememberUpdatedState(onError)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue