mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
fix: ExoPlayer HLS auto-detection for plugin streams (#1057)
This commit is contained in:
parent
70d3eee9d2
commit
16a271ebd9
10 changed files with 27 additions and 5 deletions
|
|
@ -66,6 +66,7 @@ private const val TAG = "NuvioPlayer"
|
||||||
actual fun PlatformPlayerSurface(
|
actual fun PlatformPlayerSurface(
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
sourceAudioUrl: String?,
|
sourceAudioUrl: String?,
|
||||||
|
streamType: String?,
|
||||||
sourceHeaders: Map<String, String>,
|
sourceHeaders: Map<String, String>,
|
||||||
sourceResponseHeaders: Map<String, String>,
|
sourceResponseHeaders: Map<String, String>,
|
||||||
useYoutubeChunkedPlayback: Boolean,
|
useYoutubeChunkedPlayback: Boolean,
|
||||||
|
|
@ -163,11 +164,11 @@ actual fun PlatformPlayerSurface(
|
||||||
player.apply {
|
player.apply {
|
||||||
if (!sourceAudioUrl.isNullOrBlank()) {
|
if (!sourceAudioUrl.isNullOrBlank()) {
|
||||||
val msf = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
val msf = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||||
val videoSource = msf.createMediaSource(MediaItem.fromUri(sourceUrl))
|
val videoSource = msf.createMediaSource(MediaItem.Builder().setUri(sourceUrl).apply { if ("hls".equals(streamType, ignoreCase = true) || sourceUrl.isHlsUrl()) setMimeType(MimeTypes.APPLICATION_M3U8) }.build())
|
||||||
val audioSource = msf.createMediaSource(MediaItem.fromUri(sourceAudioUrl))
|
val audioSource = msf.createMediaSource(MediaItem.Builder().setUri(sourceAudioUrl).apply { if ("hls".equals(streamType, ignoreCase = true) || sourceAudioUrl.isHlsUrl()) setMimeType(MimeTypes.APPLICATION_M3U8) }.build())
|
||||||
setMediaSource(MergingMediaSource(videoSource, audioSource))
|
setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||||
} else {
|
} else {
|
||||||
setMediaItem(MediaItem.fromUri(sourceUrl))
|
setMediaItem(MediaItem.Builder().setUri(sourceUrl).apply { if ("hls".equals(streamType, ignoreCase = true) || sourceUrl.isHlsUrl()) setMimeType(MimeTypes.APPLICATION_M3U8) }.build())
|
||||||
}
|
}
|
||||||
prepare()
|
prepare()
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
|
|
@ -736,3 +737,10 @@ private fun guessSubtitleMime(url: String): String {
|
||||||
else -> MimeTypes.TEXT_VTT
|
else -> MimeTypes.TEXT_VTT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.isHlsUrl(): Boolean =
|
||||||
|
endsWith(".m3u8", ignoreCase = true) ||
|
||||||
|
contains(".m3u8?", ignoreCase = true) ||
|
||||||
|
contains("/playlist/", ignoreCase = true) ||
|
||||||
|
contains("/master/", ignoreCase = true) ||
|
||||||
|
contains("/chunklist/", ignoreCase = true)
|
||||||
|
|
|
||||||
|
|
@ -1437,7 +1437,7 @@ private fun MainAppContent(
|
||||||
bingeGroup = cached.bingeGroup,
|
bingeGroup = cached.bingeGroup,
|
||||||
pauseDescription = pauseDescription,
|
pauseDescription = pauseDescription,
|
||||||
providerName = cached.addonName,
|
providerName = cached.addonName,
|
||||||
providerAddonId = cached.addonId,
|
providerAddonId = cached.addonId,
|
||||||
contentType = launch.type,
|
contentType = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
|
|
@ -1544,6 +1544,7 @@ private fun MainAppContent(
|
||||||
pauseDescription = pauseDescription,
|
pauseDescription = pauseDescription,
|
||||||
providerName = stream.addonName,
|
providerName = stream.addonName,
|
||||||
providerAddonId = stream.addonId,
|
providerAddonId = stream.addonId,
|
||||||
|
streamType = stream.streamType,
|
||||||
contentType = launch.type,
|
contentType = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
|
|
@ -1654,6 +1655,7 @@ private fun MainAppContent(
|
||||||
pauseDescription = pauseDescription,
|
pauseDescription = pauseDescription,
|
||||||
providerName = stream.addonName,
|
providerName = stream.addonName,
|
||||||
providerAddonId = stream.addonId,
|
providerAddonId = stream.addonId,
|
||||||
|
streamType = stream.streamType,
|
||||||
contentType = launch.type,
|
contentType = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
|
|
@ -1769,6 +1771,7 @@ private fun MainAppContent(
|
||||||
title = launch.title,
|
title = launch.title,
|
||||||
sourceUrl = launch.sourceUrl,
|
sourceUrl = launch.sourceUrl,
|
||||||
sourceAudioUrl = launch.sourceAudioUrl,
|
sourceAudioUrl = launch.sourceAudioUrl,
|
||||||
|
streamType = launch.streamType,
|
||||||
sourceHeaders = launch.sourceHeaders,
|
sourceHeaders = launch.sourceHeaders,
|
||||||
sourceResponseHeaders = launch.sourceResponseHeaders,
|
sourceResponseHeaders = launch.sourceResponseHeaders,
|
||||||
logo = launch.logo,
|
logo = launch.logo,
|
||||||
|
|
@ -1874,7 +1877,7 @@ private fun MainAppContent(
|
||||||
streamTitle = item.streamTitle,
|
streamTitle = item.streamTitle,
|
||||||
streamSubtitle = item.streamSubtitle,
|
streamSubtitle = item.streamSubtitle,
|
||||||
providerName = item.providerName,
|
providerName = item.providerName,
|
||||||
providerAddonId = item.providerAddonId,
|
providerAddonId = item.providerAddonId,
|
||||||
contentType = item.contentType,
|
contentType = item.contentType,
|
||||||
videoId = item.videoId,
|
videoId = item.videoId,
|
||||||
parentMetaId = item.parentMetaId,
|
parentMetaId = item.parentMetaId,
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,7 @@ internal object MetaDetailsParser {
|
||||||
externalUrl = externalUrl,
|
externalUrl = externalUrl,
|
||||||
addonName = addonName,
|
addonName = addonName,
|
||||||
addonId = "embedded",
|
addonId = "embedded",
|
||||||
|
streamType = obj.string("type"),
|
||||||
behaviorHints = StreamBehaviorHints(
|
behaviorHints = StreamBehaviorHints(
|
||||||
bingeGroup = hintsObj?.string("bingeGroup"),
|
bingeGroup = hintsObj?.string("bingeGroup"),
|
||||||
notWebReady = (hintsObj?.boolean("notWebReady") ?: false) || proxyHeaders != null,
|
notWebReady = (hintsObj?.boolean("notWebReady") ?: false) || proxyHeaders != null,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ internal fun sanitizePlaybackResponseHeaders(headers: Map<String, String>?): Map
|
||||||
expect fun PlatformPlayerSurface(
|
expect fun PlatformPlayerSurface(
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
sourceAudioUrl: String? = null,
|
sourceAudioUrl: String? = null,
|
||||||
|
streamType: String? = null,
|
||||||
sourceHeaders: Map<String, String> = emptyMap(),
|
sourceHeaders: Map<String, String> = emptyMap(),
|
||||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||||
useYoutubeChunkedPlayback: Boolean = false,
|
useYoutubeChunkedPlayback: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ data class PlayerLaunch(
|
||||||
val title: String,
|
val title: String,
|
||||||
val sourceUrl: String,
|
val sourceUrl: String,
|
||||||
val sourceAudioUrl: String? = null,
|
val sourceAudioUrl: String? = null,
|
||||||
|
val streamType: String? = null,
|
||||||
val sourceHeaders: Map<String, String> = emptyMap(),
|
val sourceHeaders: Map<String, String> = emptyMap(),
|
||||||
val sourceResponseHeaders: Map<String, String> = emptyMap(),
|
val sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||||
val logo: String? = null,
|
val logo: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ fun PlayerScreen(
|
||||||
title: String,
|
title: String,
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
sourceAudioUrl: String? = null,
|
sourceAudioUrl: String? = null,
|
||||||
|
streamType: String? = null,
|
||||||
sourceHeaders: Map<String, String> = emptyMap(),
|
sourceHeaders: Map<String, String> = emptyMap(),
|
||||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||||
providerName: String,
|
providerName: String,
|
||||||
|
|
@ -187,6 +188,7 @@ fun PlayerScreen(
|
||||||
// Active playback state (mutable to support source/episode switching)
|
// Active playback state (mutable to support source/episode switching)
|
||||||
var activeSourceUrl by rememberSaveable { mutableStateOf(sourceUrl) }
|
var activeSourceUrl by rememberSaveable { mutableStateOf(sourceUrl) }
|
||||||
var activeSourceAudioUrl by rememberSaveable { mutableStateOf(sourceAudioUrl) }
|
var activeSourceAudioUrl by rememberSaveable { mutableStateOf(sourceAudioUrl) }
|
||||||
|
var activeStreamType by rememberSaveable { mutableStateOf(streamType) }
|
||||||
var activeSourceHeaders by remember(sourceUrl, sourceHeaders) {
|
var activeSourceHeaders by remember(sourceUrl, sourceHeaders) {
|
||||||
mutableStateOf(sanitizePlaybackHeaders(sourceHeaders))
|
mutableStateOf(sanitizePlaybackHeaders(sourceHeaders))
|
||||||
}
|
}
|
||||||
|
|
@ -1633,6 +1635,7 @@ fun PlayerScreen(
|
||||||
PlatformPlayerSurface(
|
PlatformPlayerSurface(
|
||||||
sourceUrl = activeSourceUrl,
|
sourceUrl = activeSourceUrl,
|
||||||
sourceAudioUrl = activeSourceAudioUrl,
|
sourceAudioUrl = activeSourceAudioUrl,
|
||||||
|
streamType = activeStreamType,
|
||||||
sourceHeaders = activeSourceHeaders,
|
sourceHeaders = activeSourceHeaders,
|
||||||
sourceResponseHeaders = activeSourceResponseHeaders,
|
sourceResponseHeaders = activeSourceResponseHeaders,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,7 @@ private fun PluginRuntimeResult.toStreamItem(scraper: PluginScraper): StreamItem
|
||||||
infoHash = infoHash,
|
infoHash = infoHash,
|
||||||
addonName = scraper.name,
|
addonName = scraper.name,
|
||||||
addonId = "plugin:${scraper.id}",
|
addonId = "plugin:${scraper.id}",
|
||||||
|
streamType = type,
|
||||||
behaviorHints = if (requestHeaders.isEmpty()) {
|
behaviorHints = if (requestHeaders.isEmpty()) {
|
||||||
com.nuvio.app.features.streams.StreamBehaviorHints()
|
com.nuvio.app.features.streams.StreamBehaviorHints()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ data class StreamItem(
|
||||||
val sourceName: String? = null,
|
val sourceName: String? = null,
|
||||||
val addonName: String,
|
val addonName: String,
|
||||||
val addonId: String,
|
val addonId: String,
|
||||||
|
val streamType: String? = null,
|
||||||
val behaviorHints: StreamBehaviorHints = StreamBehaviorHints(),
|
val behaviorHints: StreamBehaviorHints = StreamBehaviorHints(),
|
||||||
val clientResolve: StreamClientResolve? = null,
|
val clientResolve: StreamClientResolve? = null,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ object StreamParser {
|
||||||
val infoHash = obj.string("infoHash")
|
val infoHash = obj.string("infoHash")
|
||||||
val externalUrl = obj.string("externalUrl")
|
val externalUrl = obj.string("externalUrl")
|
||||||
val clientResolve = obj.objectValue("clientResolve")?.toClientResolve()
|
val clientResolve = obj.objectValue("clientResolve")?.toClientResolve()
|
||||||
|
val streamType = obj.string("type")
|
||||||
|
|
||||||
// Must have at least one playable source
|
// Must have at least one playable source
|
||||||
if (url == null && infoHash == null && externalUrl == null && clientResolve == null) return@mapNotNull null
|
if (url == null && infoHash == null && externalUrl == null && clientResolve == null) return@mapNotNull null
|
||||||
|
|
@ -44,6 +45,7 @@ object StreamParser {
|
||||||
fileIdx = obj.int("fileIdx"),
|
fileIdx = obj.int("fileIdx"),
|
||||||
externalUrl = externalUrl,
|
externalUrl = externalUrl,
|
||||||
sources = obj.stringList("sources"),
|
sources = obj.stringList("sources"),
|
||||||
|
streamType = streamType,
|
||||||
addonName = addonName,
|
addonName = addonName,
|
||||||
addonId = addonId,
|
addonId = addonId,
|
||||||
clientResolve = clientResolve,
|
clientResolve = clientResolve,
|
||||||
|
|
|
||||||
|
|
@ -649,6 +649,7 @@ private fun PluginRuntimeResult.toStreamItem(
|
||||||
sourceName = scraper.name,
|
sourceName = scraper.name,
|
||||||
addonName = addonName,
|
addonName = addonName,
|
||||||
addonId = addonId,
|
addonId = addonId,
|
||||||
|
streamType = type,
|
||||||
behaviorHints = if (requestHeaders.isEmpty()) {
|
behaviorHints = if (requestHeaders.isEmpty()) {
|
||||||
StreamBehaviorHints()
|
StreamBehaviorHints()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue