diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlaybackMediaItems.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlaybackMediaItems.android.kt index 038a472a..d4646342 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlaybackMediaItems.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlaybackMediaItems.android.kt @@ -7,11 +7,13 @@ import java.util.Locale internal fun playbackMediaItemFromUrl( url: String, responseHeaders: Map = 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, + 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? { if (headers.isEmpty()) return null diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt index c938387f..40acdfe6 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt @@ -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, sourceResponseHeaders: Map, + 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, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index a10af383..b93ea4a1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt index adcf6811..073ae7f7 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt index ae78fb8d..41fdf26b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEngine.kt @@ -58,6 +58,7 @@ expect fun PlatformPlayerSurface( sourceAudioUrl: String? = null, sourceHeaders: Map = emptyMap(), sourceResponseHeaders: Map = emptyMap(), + streamType: String? = null, useYoutubeChunkedPlayback: Boolean = false, modifier: Modifier = Modifier, playWhenReady: Boolean = true, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt index 8441b89d..1989a25f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt @@ -25,6 +25,7 @@ data class PlayerLaunch( val sourceAudioUrl: String? = null, val sourceHeaders: Map = emptyMap(), val sourceResponseHeaders: Map = emptyMap(), + val streamType: String? = null, val logo: String? = null, val poster: String? = null, val background: String? = null, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt index 725ecf22..431e4978 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt @@ -10,6 +10,7 @@ fun PlayerScreen( sourceAudioUrl: String? = null, sourceHeaders: Map = emptyMap(), sourceResponseHeaders: Map = 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, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt index 71705dc8..2aa779d2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenArgs.kt @@ -8,6 +8,7 @@ internal data class PlayerScreenArgs( val sourceAudioUrl: String?, val sourceHeaders: Map, val sourceResponseHeaders: Map, + val streamType: String?, val providerName: String, val streamTitle: String, val streamSubtitle: String?, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt index 886046c6..0b2d5f3e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt index 0dbc7a27..6e6501c1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeSourceActions.kt @@ -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, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt index 73145102..6b1e1562 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt @@ -32,6 +32,7 @@ internal class PlayerScreenRuntime( val sourceAudioUrl: String? get() = args.sourceAudioUrl val sourceHeaders: Map get() = args.sourceHeaders val sourceResponseHeaders: Map 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) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt index 3119c79f..d4c0bbc3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt @@ -120,6 +120,7 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() { sourceAudioUrl = activeSourceAudioUrl, sourceHeaders = activeSourceHeaders, sourceResponseHeaders = activeSourceResponseHeaders, + streamType = activeStreamType, modifier = Modifier.fillMaxSize(), playWhenReady = shouldPlay, resizeMode = resizeMode, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt index 1a733fcd..0a015dd5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt @@ -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 { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt index f218ae74..e3a7e8ef 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamLinkCacheRepository.kt @@ -18,6 +18,7 @@ data class CachedStreamLink( val fileIdx: Int? = null, val sources: List = 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 = 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) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt index 4719a89d..c87143fb 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamModels.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamParser.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamParser.kt index a27033ed..ab1df959 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamParser.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamParser.kt @@ -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"), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt index 49fc361a..b9f34927 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt @@ -915,6 +915,7 @@ private fun PluginRuntimeResult.toStreamItem( sourceName = scraper.name, addonName = addonName, addonId = addonId, + streamType = normalizeStreamType(type), behaviorHints = if (requestHeaders.isEmpty()) { StreamBehaviorHints() } else { diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/streams/StreamParserTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/streams/StreamParserTest.kt index 41519dac..d6ddf28d 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/streams/StreamParserTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/streams/StreamParserTest.kt @@ -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) + } } diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt index adfa0126..c122cacc 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt @@ -29,6 +29,7 @@ actual fun PlatformPlayerSurface( sourceAudioUrl: String?, sourceHeaders: Map, sourceResponseHeaders: Map, + streamType: String?, useYoutubeChunkedPlayback: Boolean, modifier: Modifier, playWhenReady: Boolean,