mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-30 16:19:25 +00:00
Merge remote-tracking branch 'origin/pr/1326' into cmp-rewrite
# Conflicts: # composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt # composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/PlayerEngine.desktop.kt
This commit is contained in:
commit
e897ec2e9a
19 changed files with 144 additions and 2 deletions
|
|
@ -7,11 +7,13 @@ import java.util.Locale
|
|||
internal fun playbackMediaItemFromUrl(
|
||||
url: String,
|
||||
responseHeaders: Map<String, String> = emptyMap(),
|
||||
streamType: String? = null,
|
||||
): MediaItem {
|
||||
val builder = MediaItem.Builder().setUri(url)
|
||||
inferPlaybackMimeType(
|
||||
url = url,
|
||||
responseHeaders = responseHeaders,
|
||||
streamType = streamType,
|
||||
)?.let(builder::setMimeType)
|
||||
return builder.build()
|
||||
}
|
||||
|
|
@ -19,10 +21,26 @@ internal fun playbackMediaItemFromUrl(
|
|||
private fun inferPlaybackMimeType(
|
||||
url: String,
|
||||
responseHeaders: Map<String, String>,
|
||||
streamType: String?,
|
||||
): String? =
|
||||
inferMimeTypeFromResponseHeaders(responseHeaders)
|
||||
inferMimeTypeFromStreamType(streamType)
|
||||
?: inferMimeTypeFromResponseHeaders(responseHeaders)
|
||||
?: inferMimeTypeFromPath(url)
|
||||
|
||||
private fun inferMimeTypeFromStreamType(streamType: String?): String? {
|
||||
val normalized = streamType
|
||||
?.trim()
|
||||
?.lowercase(Locale.US)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: return null
|
||||
return when (normalized) {
|
||||
"hls", "m3u8" -> MimeTypes.APPLICATION_M3U8
|
||||
"dash", "mpd" -> MimeTypes.APPLICATION_MPD
|
||||
"smoothstreaming", "ss" -> MimeTypes.APPLICATION_SS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferMimeTypeFromResponseHeaders(headers: Map<String, String>): String? {
|
||||
if (headers.isEmpty()) return null
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import androidx.media3.ui.PlayerView
|
|||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.CaptionStyleCompat
|
||||
import com.nuvio.app.R
|
||||
import com.nuvio.app.features.streams.normalizeStreamType
|
||||
import io.github.peerless2012.ass.media.widget.AssSubtitleView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -75,6 +76,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
streamType: String?,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
|
|
@ -101,6 +103,9 @@ actual fun PlatformPlayerSurface(
|
|||
val sanitizedSourceResponseHeaders = remember(sourceResponseHeaders) {
|
||||
sanitizePlaybackResponseHeaders(sourceResponseHeaders)
|
||||
}
|
||||
val normalizedStreamType = remember(streamType) {
|
||||
normalizeStreamType(streamType)
|
||||
}
|
||||
val useLibass = playerSettings.useLibass
|
||||
val libassRenderType = runCatching {
|
||||
LibassRenderType.valueOf(playerSettings.libassRenderType)
|
||||
|
|
@ -110,6 +115,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceAudioUrl.orEmpty(),
|
||||
sanitizedSourceHeaders,
|
||||
sanitizedSourceResponseHeaders,
|
||||
normalizedStreamType.orEmpty(),
|
||||
useYoutubeChunkedPlayback,
|
||||
)
|
||||
var subtitleDelayMs by remember(playerSourceKey) { mutableStateOf(0) }
|
||||
|
|
@ -162,6 +168,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceAudioUrl,
|
||||
sanitizedSourceHeaders,
|
||||
sanitizedSourceResponseHeaders,
|
||||
normalizedStreamType,
|
||||
useYoutubeChunkedPlayback,
|
||||
effectiveDecoderPriority,
|
||||
) {
|
||||
|
|
@ -226,6 +233,7 @@ actual fun PlatformPlayerSurface(
|
|||
videoMediaItem = playbackMediaItemFromUrl(
|
||||
url = sourceUrl,
|
||||
responseHeaders = sanitizedSourceResponseHeaders,
|
||||
streamType = normalizedStreamType,
|
||||
),
|
||||
startPositionMs = fallbackStartPositionMs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1770,6 +1770,7 @@ private fun MainAppContent(
|
|||
sourceUrl = sentinelUrl,
|
||||
sourceHeaders = emptyMap(),
|
||||
sourceResponseHeaders = emptyMap(),
|
||||
streamType = stream.streamType,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -1889,6 +1890,7 @@ private fun MainAppContent(
|
|||
sourceUrl = cached.url,
|
||||
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(cached.responseHeaders),
|
||||
streamType = cached.streamType,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -2014,6 +2016,7 @@ private fun MainAppContent(
|
|||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
streamType = stream.streamType,
|
||||
)
|
||||
}
|
||||
val playerLaunch = PlayerLaunch(
|
||||
|
|
@ -2021,6 +2024,7 @@ private fun MainAppContent(
|
|||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
streamType = stream.streamType,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -2138,6 +2142,7 @@ private fun MainAppContent(
|
|||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
streamType = stream.streamType,
|
||||
)
|
||||
}
|
||||
val playerLaunch = PlayerLaunch(
|
||||
|
|
@ -2145,6 +2150,7 @@ private fun MainAppContent(
|
|||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
streamType = stream.streamType,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -2303,6 +2309,7 @@ private fun MainAppContent(
|
|||
sourceAudioUrl = launch.sourceAudioUrl,
|
||||
sourceHeaders = launch.sourceHeaders,
|
||||
sourceResponseHeaders = launch.sourceResponseHeaders,
|
||||
streamType = launch.streamType,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.details
|
|||
import com.nuvio.app.features.streams.StreamBehaviorHints
|
||||
import com.nuvio.app.features.streams.StreamItem
|
||||
import com.nuvio.app.features.streams.StreamProxyHeaders
|
||||
import com.nuvio.app.features.streams.normalizeStreamType
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
|
|
@ -288,6 +289,7 @@ internal object MetaDetailsParser {
|
|||
externalUrl = externalUrl,
|
||||
addonName = addonName,
|
||||
addonId = "embedded",
|
||||
streamType = normalizeStreamType(obj.string("type")),
|
||||
behaviorHints = StreamBehaviorHints(
|
||||
bingeGroup = hintsObj?.string("bingeGroup"),
|
||||
notWebReady = (hintsObj?.boolean("notWebReady") ?: false) || proxyHeaders != null,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ expect fun PlatformPlayerSurface(
|
|||
sourceAudioUrl: String? = null,
|
||||
sourceHeaders: Map<String, String> = emptyMap(),
|
||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
streamType: String? = null,
|
||||
useYoutubeChunkedPlayback: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
playWhenReady: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ data class PlayerLaunch(
|
|||
val sourceAudioUrl: String? = null,
|
||||
val sourceHeaders: Map<String, String> = emptyMap(),
|
||||
val sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
val streamType: String? = null,
|
||||
val logo: String? = null,
|
||||
val poster: String? = null,
|
||||
val background: String? = null,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ fun PlayerScreen(
|
|||
sourceAudioUrl: String? = null,
|
||||
sourceHeaders: Map<String, String> = emptyMap(),
|
||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
streamType: String? = null,
|
||||
providerName: String,
|
||||
streamTitle: String,
|
||||
streamSubtitle: String?,
|
||||
|
|
@ -44,6 +45,7 @@ fun PlayerScreen(
|
|||
sourceAudioUrl = sourceAudioUrl,
|
||||
sourceHeaders = sourceHeaders,
|
||||
sourceResponseHeaders = sourceResponseHeaders,
|
||||
streamType = streamType,
|
||||
providerName = providerName,
|
||||
streamTitle = streamTitle,
|
||||
streamSubtitle = streamSubtitle,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ internal data class PlayerScreenArgs(
|
|||
val sourceAudioUrl: String?,
|
||||
val sourceHeaders: Map<String, String>,
|
||||
val sourceResponseHeaders: Map<String, String>,
|
||||
val streamType: String?,
|
||||
val providerName: String,
|
||||
val streamTitle: String,
|
||||
val streamSubtitle: String?,
|
||||
|
|
|
|||
|
|
@ -555,6 +555,7 @@ internal fun PlayerScreenRuntime.tryRefreshCredentialedSourceAfterError(message:
|
|||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
||||
activeSourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response)
|
||||
activeStreamType = stream.streamType
|
||||
activeStreamTitle = stream.streamLabel
|
||||
activeStreamSubtitle = stream.streamSubtitle
|
||||
activeProviderName = stream.addonName
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ internal fun PlayerScreenRuntime.switchToP2pSourceStream(stream: StreamItem) {
|
|||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = emptyMap()
|
||||
activeSourceResponseHeaders = emptyMap()
|
||||
activeStreamType = null
|
||||
activeTorrentInfoHash = infoHash
|
||||
activeTorrentFileIdx = stream.fileIdx
|
||||
activeTorrentFilename = stream.behaviorHints.filename
|
||||
|
|
@ -153,6 +154,7 @@ internal fun PlayerScreenRuntime.switchToP2pEpisodeStream(
|
|||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = emptyMap()
|
||||
activeSourceResponseHeaders = emptyMap()
|
||||
activeStreamType = null
|
||||
activeTorrentInfoHash = infoHash
|
||||
activeTorrentFileIdx = stream.fileIdx
|
||||
activeTorrentFilename = stream.behaviorHints.filename
|
||||
|
|
@ -186,7 +188,11 @@ internal fun PlayerScreenRuntime.switchToSource(stream: StreamItem) {
|
|||
return
|
||||
}
|
||||
val url = stream.playableDirectUrl ?: return
|
||||
if (url == activeSourceUrl) return
|
||||
val sourceIdentityKey = stream.playerSourceIdentityKey()
|
||||
if (url == activeSourceUrl) {
|
||||
activeSourceIdentityKey = sourceIdentityKey ?: activeSourceIdentityKey
|
||||
return
|
||||
}
|
||||
val currentPositionMs = playbackSnapshot.positionMs.coerceAtLeast(0L)
|
||||
flushWatchProgress()
|
||||
stopActiveP2pStream()
|
||||
|
|
@ -198,6 +204,8 @@ internal fun PlayerScreenRuntime.switchToSource(stream: StreamItem) {
|
|||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
||||
activeSourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response)
|
||||
activeStreamType = stream.streamType
|
||||
activeSourceIdentityKey = sourceIdentityKey
|
||||
activeStreamTitle = stream.streamLabel
|
||||
activeStreamSubtitle = stream.streamSubtitle
|
||||
activeProviderName = stream.addonName
|
||||
|
|
@ -244,6 +252,7 @@ internal fun PlayerScreenRuntime.switchToEpisodeStream(stream: StreamItem, episo
|
|||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
||||
activeSourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response)
|
||||
activeStreamType = stream.streamType
|
||||
applyEpisodeStreamMetadata(stream, episode, resume)
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +280,8 @@ internal fun PlayerScreenRuntime.switchToDownloadedEpisode(downloadItem: Downloa
|
|||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = emptyMap()
|
||||
activeSourceResponseHeaders = emptyMap()
|
||||
activeStreamType = null
|
||||
activeSourceIdentityKey = null
|
||||
activeStreamTitle = downloadItem.streamTitle.ifBlank {
|
||||
episode.title.ifBlank { title }
|
||||
}
|
||||
|
|
@ -416,5 +427,6 @@ private fun PlayerScreenRuntime.saveDirectStreamForReuse(
|
|||
filename = stream.behaviorHints.filename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
streamType = stream.streamType,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ internal class PlayerScreenRuntime(
|
|||
val sourceAudioUrl: String? get() = args.sourceAudioUrl
|
||||
val sourceHeaders: Map<String, String> get() = args.sourceHeaders
|
||||
val sourceResponseHeaders: Map<String, String> get() = args.sourceResponseHeaders
|
||||
val streamType: String? get() = args.streamType
|
||||
val providerName: String get() = args.providerName
|
||||
val streamTitle: String get() = args.streamTitle
|
||||
val streamSubtitle: String? get() = args.streamSubtitle
|
||||
|
|
@ -95,6 +96,7 @@ internal class PlayerScreenRuntime(
|
|||
var activeSourceAudioUrl by mutableStateOf(sourceAudioUrl)
|
||||
var activeSourceHeaders by mutableStateOf(sanitizePlaybackHeaders(sourceHeaders))
|
||||
var activeSourceResponseHeaders by mutableStateOf(sanitizePlaybackResponseHeaders(sourceResponseHeaders))
|
||||
var activeStreamType by mutableStateOf(streamType)
|
||||
var activeTorrentInfoHash by mutableStateOf(torrentInfoHash)
|
||||
var activeTorrentFileIdx by mutableStateOf(torrentFileIdx)
|
||||
var activeTorrentFilename by mutableStateOf(torrentFilename)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
|||
sourceAudioUrl = activeSourceAudioUrl,
|
||||
sourceHeaders = activeSourceHeaders,
|
||||
sourceResponseHeaders = activeSourceResponseHeaders,
|
||||
streamType = activeStreamType,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
playWhenReady = shouldPlay,
|
||||
resizeMode = resizeMode,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.nuvio.app.features.streams.StreamBadgeSettingsRepository
|
|||
import com.nuvio.app.features.streams.StreamItem
|
||||
import com.nuvio.app.features.streams.StreamParser
|
||||
import com.nuvio.app.features.streams.StreamsUiState
|
||||
import com.nuvio.app.features.streams.normalizeStreamType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -455,6 +456,7 @@ private fun PluginRuntimeResult.toStreamItem(scraper: PluginScraper): StreamItem
|
|||
infoHash = infoHash,
|
||||
addonName = scraper.name,
|
||||
addonId = "plugin:${scraper.id}",
|
||||
streamType = normalizeStreamType(type),
|
||||
behaviorHints = if (requestHeaders.isEmpty()) {
|
||||
com.nuvio.app.features.streams.StreamBehaviorHints()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ data class CachedStreamLink(
|
|||
val fileIdx: Int? = null,
|
||||
val sources: List<String> = emptyList(),
|
||||
val bingeGroup: String? = null,
|
||||
val streamType: String? = null,
|
||||
)
|
||||
|
||||
internal expect fun epochMs(): Long
|
||||
|
|
@ -54,6 +55,7 @@ object StreamLinkCacheRepository {
|
|||
fileIdx: Int? = null,
|
||||
sources: List<String> = emptyList(),
|
||||
bingeGroup: String? = null,
|
||||
streamType: String? = null,
|
||||
) {
|
||||
if (url.isNotBlank() && url.hasLikelyExpiringPlaybackCredentials()) {
|
||||
remove(contentKey)
|
||||
|
|
@ -74,6 +76,7 @@ object StreamLinkCacheRepository {
|
|||
fileIdx = fileIdx,
|
||||
sources = sources,
|
||||
bingeGroup = bingeGroup,
|
||||
streamType = streamType,
|
||||
)
|
||||
val payload = json.encodeToString(CachedStreamLink.serializer(), entry)
|
||||
StreamLinkCacheStorage.saveEntry(hashedKey(contentKey), payload)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ data class StreamItem(
|
|||
val addonName: String,
|
||||
val addonId: String,
|
||||
val addonLogo: String? = null,
|
||||
val streamType: String? = null,
|
||||
val behaviorHints: StreamBehaviorHints = StreamBehaviorHints(),
|
||||
val clientResolve: StreamClientResolve? = null,
|
||||
val debridCacheStatus: StreamDebridCacheStatus? = null,
|
||||
|
|
@ -89,6 +90,9 @@ data class StreamBadge(
|
|||
val borderColor: String = "",
|
||||
)
|
||||
|
||||
fun normalizeStreamType(raw: String?): String? =
|
||||
raw?.trim()?.lowercase()?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun String?.isMagnetLink(): Boolean =
|
||||
this?.trimStart()?.startsWith("magnet:", ignoreCase = true) == true
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ object StreamParser {
|
|||
addonName = addonName,
|
||||
addonId = addonId,
|
||||
addonLogo = addonLogo,
|
||||
streamType = normalizeStreamType(obj.string("type")),
|
||||
clientResolve = clientResolve,
|
||||
behaviorHints = StreamBehaviorHints(
|
||||
bingeGroup = hintsObj?.string("bingeGroup"),
|
||||
|
|
|
|||
|
|
@ -915,6 +915,7 @@ private fun PluginRuntimeResult.toStreamItem(
|
|||
sourceName = scraper.name,
|
||||
addonName = addonName,
|
||||
addonId = addonId,
|
||||
streamType = normalizeStreamType(type),
|
||||
behaviorHints = if (requestHeaders.isEmpty()) {
|
||||
StreamBehaviorHints()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -171,4 +171,78 @@ class StreamParserTest {
|
|||
assertEquals("2160p", stream.clientResolve?.stream?.raw?.parsed?.resolution)
|
||||
assertEquals(listOf(1, 2), stream.clientResolve?.stream?.raw?.parsed?.episodes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse keeps addon-declared streamType`() {
|
||||
val streams = StreamParser.parse(
|
||||
payload =
|
||||
"""
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"url": "https://cdn.example.com/playlist?token=abc",
|
||||
"name": "1080p",
|
||||
"type": "hls"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent(),
|
||||
addonName = "Addon",
|
||||
addonId = "addon.id",
|
||||
)
|
||||
|
||||
assertEquals("hls", streams.single().streamType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse normalizes streamType casing and whitespace`() {
|
||||
val streams = StreamParser.parse(
|
||||
payload =
|
||||
"""
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"url": "https://cdn.example.com/playlist?token=abc",
|
||||
"name": "1080p",
|
||||
"type": " HLS "
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent(),
|
||||
addonName = "Addon",
|
||||
addonId = "addon.id",
|
||||
)
|
||||
|
||||
assertEquals("hls", streams.single().streamType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizeStreamType trims lowercases and blanks to null`() {
|
||||
assertEquals("hls", normalizeStreamType(" Hls "))
|
||||
assertEquals("dash", normalizeStreamType("DASH"))
|
||||
assertEquals(null, normalizeStreamType(" "))
|
||||
assertEquals(null, normalizeStreamType(""))
|
||||
assertEquals(null, normalizeStreamType(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse leaves streamType null when addon omits it`() {
|
||||
val streams = StreamParser.parse(
|
||||
payload =
|
||||
"""
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"url": "https://example.com/video.mp4",
|
||||
"name": "1080p"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent(),
|
||||
addonName = "Addon",
|
||||
addonId = "addon.id",
|
||||
)
|
||||
|
||||
assertEquals(null, streams.single().streamType)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
streamType: String?,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
|
|
|
|||
Loading…
Reference in a new issue