mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
fix: streamroute to crarry stream payload in memory to avoid ios serialization crash on routing
This commit is contained in:
parent
f46aa693e2
commit
db4d79ff9d
3 changed files with 163 additions and 117 deletions
|
|
@ -40,6 +40,7 @@ import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
|
@ -55,6 +56,8 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
|
@ -139,9 +142,9 @@ import com.nuvio.app.features.collection.CollectionSyncService
|
||||||
import com.nuvio.app.features.home.HomeCatalogSettingsSyncService
|
import com.nuvio.app.features.home.HomeCatalogSettingsSyncService
|
||||||
import com.nuvio.app.features.collection.FolderDetailScreen
|
import com.nuvio.app.features.collection.FolderDetailScreen
|
||||||
import com.nuvio.app.features.collection.FolderDetailRepository
|
import com.nuvio.app.features.collection.FolderDetailRepository
|
||||||
import com.nuvio.app.features.streams.StreamContext
|
|
||||||
import com.nuvio.app.features.streams.StreamContextStore
|
|
||||||
import com.nuvio.app.features.streams.StreamAutoPlayPolicy
|
import com.nuvio.app.features.streams.StreamAutoPlayPolicy
|
||||||
|
import com.nuvio.app.features.streams.StreamLaunch
|
||||||
|
import com.nuvio.app.features.streams.StreamLaunchStore
|
||||||
import com.nuvio.app.features.streams.StreamLinkCacheRepository
|
import com.nuvio.app.features.streams.StreamLinkCacheRepository
|
||||||
import com.nuvio.app.features.streams.StreamsRepository
|
import com.nuvio.app.features.streams.StreamsRepository
|
||||||
import com.nuvio.app.features.streams.StreamsScreen
|
import com.nuvio.app.features.streams.StreamsScreen
|
||||||
|
|
@ -223,23 +226,7 @@ data class FolderDetailRoute(val collectionId: String, val folderId: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StreamRoute(
|
data class StreamRoute(
|
||||||
val type: String,
|
val launchId: Long,
|
||||||
val videoId: String,
|
|
||||||
val parentMetaId: String? = null,
|
|
||||||
val parentMetaType: String? = null,
|
|
||||||
val title: String,
|
|
||||||
val logo: String? = null,
|
|
||||||
val poster: String? = null,
|
|
||||||
val background: String? = null,
|
|
||||||
val seasonNumber: Int? = null,
|
|
||||||
val episodeNumber: Int? = null,
|
|
||||||
val episodeTitle: String? = null,
|
|
||||||
val episodeThumbnail: String? = null,
|
|
||||||
val streamContextId: Long? = null,
|
|
||||||
val resumePositionMs: Long? = null,
|
|
||||||
val resumeProgressFraction: Float? = null,
|
|
||||||
val manualSelection: Boolean = false,
|
|
||||||
val startFromBeginning: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -676,11 +663,8 @@ private fun MainAppContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val streamContextId = pauseDescription
|
val streamLaunchId = StreamLaunchStore.put(
|
||||||
?.takeIf { it.isNotBlank() }
|
StreamLaunch(
|
||||||
?.let { StreamContextStore.put(StreamContext(pauseDescription = it)) }
|
|
||||||
navController.navigate(
|
|
||||||
StreamRoute(
|
|
||||||
type = type,
|
type = type,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
parentMetaId = parentMetaId,
|
parentMetaId = parentMetaId,
|
||||||
|
|
@ -693,13 +677,16 @@ private fun MainAppContent(
|
||||||
episodeNumber = episodeNumber,
|
episodeNumber = episodeNumber,
|
||||||
episodeTitle = episodeTitle,
|
episodeTitle = episodeTitle,
|
||||||
episodeThumbnail = episodeThumbnail,
|
episodeThumbnail = episodeThumbnail,
|
||||||
streamContextId = streamContextId,
|
pauseDescription = pauseDescription,
|
||||||
resumePositionMs = if (startFromBeginning) 0L else resumePositionMs,
|
resumePositionMs = if (startFromBeginning) 0L else resumePositionMs,
|
||||||
resumeProgressFraction = targetResumeProgressFraction,
|
resumeProgressFraction = targetResumeProgressFraction,
|
||||||
manualSelection = manualSelection,
|
manualSelection = manualSelection,
|
||||||
startFromBeginning = startFromBeginning,
|
startFromBeginning = startFromBeginning,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
navController.navigate(
|
||||||
|
StreamRoute(launchId = streamLaunchId),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val onPlay: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit =
|
val onPlay: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit =
|
||||||
|
|
@ -1078,61 +1065,79 @@ private fun MainAppContent(
|
||||||
}
|
}
|
||||||
composable<StreamRoute> { backStackEntry ->
|
composable<StreamRoute> { backStackEntry ->
|
||||||
val route = backStackEntry.toRoute<StreamRoute>()
|
val route = backStackEntry.toRoute<StreamRoute>()
|
||||||
val pauseDescription = remember(route.streamContextId) {
|
val launch = remember(route.launchId) {
|
||||||
route.streamContextId?.let { contextId ->
|
StreamLaunchStore.get(route.launchId)
|
||||||
StreamContextStore.get(contextId)?.pauseDescription
|
}
|
||||||
|
if (launch == null) {
|
||||||
|
LaunchedEffect(route.launchId) {
|
||||||
|
StreamsRepository.clear()
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
return@composable
|
||||||
|
}
|
||||||
|
val pauseDescription = launch.pauseDescription
|
||||||
|
val lifecycleOwner = backStackEntry
|
||||||
|
DisposableEffect(lifecycleOwner, route.launchId) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_DESTROY) {
|
||||||
|
StreamLaunchStore.remove(route.launchId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val shouldResolveEpisodeVideoId =
|
val shouldResolveEpisodeVideoId =
|
||||||
route.parentMetaId != null &&
|
launch.parentMetaId != null &&
|
||||||
route.seasonNumber != null &&
|
launch.seasonNumber != null &&
|
||||||
route.episodeNumber != null
|
launch.episodeNumber != null
|
||||||
var effectiveVideoId by rememberSaveable(
|
var effectiveVideoId by rememberSaveable(
|
||||||
route.videoId,
|
launch.videoId,
|
||||||
route.parentMetaId,
|
launch.parentMetaId,
|
||||||
route.seasonNumber,
|
launch.seasonNumber,
|
||||||
route.episodeNumber,
|
launch.episodeNumber,
|
||||||
) {
|
) {
|
||||||
mutableStateOf(route.videoId)
|
mutableStateOf(launch.videoId)
|
||||||
}
|
}
|
||||||
var hasResolvedVideoId by rememberSaveable(
|
var hasResolvedVideoId by rememberSaveable(
|
||||||
route.videoId,
|
launch.videoId,
|
||||||
route.parentMetaId,
|
launch.parentMetaId,
|
||||||
route.seasonNumber,
|
launch.seasonNumber,
|
||||||
route.episodeNumber,
|
launch.episodeNumber,
|
||||||
) {
|
) {
|
||||||
mutableStateOf(!shouldResolveEpisodeVideoId)
|
mutableStateOf(!shouldResolveEpisodeVideoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
route.videoId,
|
launch.videoId,
|
||||||
route.parentMetaId,
|
launch.parentMetaId,
|
||||||
route.parentMetaType,
|
launch.parentMetaType,
|
||||||
route.type,
|
launch.type,
|
||||||
route.seasonNumber,
|
launch.seasonNumber,
|
||||||
route.episodeNumber,
|
launch.episodeNumber,
|
||||||
) {
|
) {
|
||||||
effectiveVideoId = route.videoId
|
effectiveVideoId = launch.videoId
|
||||||
if (!shouldResolveEpisodeVideoId) {
|
if (!shouldResolveEpisodeVideoId) {
|
||||||
hasResolvedVideoId = true
|
hasResolvedVideoId = true
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
hasResolvedVideoId = false
|
hasResolvedVideoId = false
|
||||||
val metaType = route.parentMetaType ?: route.type
|
val metaType = launch.parentMetaType ?: launch.type
|
||||||
val metaId = route.parentMetaId ?: return@LaunchedEffect
|
val metaId = launch.parentMetaId ?: return@LaunchedEffect
|
||||||
val resolvedVideoId = runCatching {
|
val resolvedVideoId = runCatching {
|
||||||
MetaDetailsRepository.fetch(metaType, metaId)
|
MetaDetailsRepository.fetch(metaType, metaId)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
?.videos
|
?.videos
|
||||||
?.firstOrNull { video ->
|
?.firstOrNull { video ->
|
||||||
video.season == route.seasonNumber &&
|
video.season == launch.seasonNumber &&
|
||||||
video.episode == route.episodeNumber
|
video.episode == launch.episodeNumber
|
||||||
}
|
}
|
||||||
?.id
|
?.id
|
||||||
?.takeIf { it.isNotBlank() }
|
?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
effectiveVideoId = resolvedVideoId ?: route.videoId
|
effectiveVideoId = resolvedVideoId ?: launch.videoId
|
||||||
hasResolvedVideoId = true
|
hasResolvedVideoId = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1142,15 +1147,15 @@ private fun MainAppContent(
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Reuse Last Link: auto-play from cache if enabled (only on first entry)
|
// Reuse Last Link: auto-play from cache if enabled (only on first entry)
|
||||||
var reuseHandled by rememberSaveable(route.videoId, effectiveVideoId) { mutableStateOf(false) }
|
var reuseHandled by rememberSaveable(launch.videoId, effectiveVideoId) { mutableStateOf(false) }
|
||||||
var reuseNavigated by remember { mutableStateOf(false) }
|
var reuseNavigated by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(effectiveVideoId, hasResolvedVideoId, playerSettings.streamReuseLastLinkEnabled, route.manualSelection) {
|
LaunchedEffect(effectiveVideoId, hasResolvedVideoId, playerSettings.streamReuseLastLinkEnabled, launch.manualSelection) {
|
||||||
if (!hasResolvedVideoId) return@LaunchedEffect
|
if (!hasResolvedVideoId) return@LaunchedEffect
|
||||||
if (reuseHandled) return@LaunchedEffect
|
if (reuseHandled) return@LaunchedEffect
|
||||||
reuseHandled = true
|
reuseHandled = true
|
||||||
if (route.manualSelection) return@LaunchedEffect
|
if (launch.manualSelection) return@LaunchedEffect
|
||||||
if (!playerSettings.streamReuseLastLinkEnabled) return@LaunchedEffect
|
if (!playerSettings.streamReuseLastLinkEnabled) return@LaunchedEffect
|
||||||
val cacheKey = StreamLinkCacheRepository.contentKey(route.type, effectiveVideoId)
|
val cacheKey = StreamLinkCacheRepository.contentKey(launch.type, effectiveVideoId)
|
||||||
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
|
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
|
||||||
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
|
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
|
|
@ -1158,32 +1163,31 @@ private fun MainAppContent(
|
||||||
StreamsRepository.clear()
|
StreamsRepository.clear()
|
||||||
val launchId = PlayerLaunchStore.put(
|
val launchId = PlayerLaunchStore.put(
|
||||||
PlayerLaunch(
|
PlayerLaunch(
|
||||||
title = route.title,
|
title = launch.title,
|
||||||
sourceUrl = cached.url,
|
sourceUrl = cached.url,
|
||||||
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
||||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(cached.responseHeaders),
|
sourceResponseHeaders = sanitizePlaybackResponseHeaders(cached.responseHeaders),
|
||||||
logo = route.logo,
|
logo = launch.logo,
|
||||||
poster = route.poster,
|
poster = launch.poster,
|
||||||
background = route.background,
|
background = launch.background,
|
||||||
seasonNumber = route.seasonNumber,
|
seasonNumber = launch.seasonNumber,
|
||||||
episodeNumber = route.episodeNumber,
|
episodeNumber = launch.episodeNumber,
|
||||||
episodeTitle = route.episodeTitle,
|
episodeTitle = launch.episodeTitle,
|
||||||
episodeThumbnail = route.episodeThumbnail,
|
episodeThumbnail = launch.episodeThumbnail,
|
||||||
streamTitle = cached.streamName,
|
streamTitle = cached.streamName,
|
||||||
streamSubtitle = null,
|
streamSubtitle = null,
|
||||||
bingeGroup = cached.bingeGroup,
|
bingeGroup = cached.bingeGroup,
|
||||||
pauseDescription = pauseDescription,
|
pauseDescription = pauseDescription,
|
||||||
providerName = cached.addonName,
|
providerName = cached.addonName,
|
||||||
providerAddonId = cached.addonId,
|
providerAddonId = cached.addonId,
|
||||||
contentType = route.type,
|
contentType = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = route.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
parentMetaType = route.parentMetaType ?: route.type,
|
parentMetaType = launch.parentMetaType ?: launch.type,
|
||||||
initialPositionMs = route.resumePositionMs ?: 0L,
|
initialPositionMs = launch.resumePositionMs ?: 0L,
|
||||||
initialProgressFraction = route.resumeProgressFraction,
|
initialProgressFraction = launch.resumeProgressFraction,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
route.streamContextId?.let(StreamContextStore::remove)
|
|
||||||
navController.navigate(PlayerRoute(launchId = launchId)) {
|
navController.navigate(PlayerRoute(launchId = launchId)) {
|
||||||
popUpTo<StreamRoute> { inclusive = true }
|
popUpTo<StreamRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
|
|
@ -1191,17 +1195,17 @@ private fun MainAppContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
val streamsUiState by StreamsRepository.uiState.collectAsStateWithLifecycle()
|
val streamsUiState by StreamsRepository.uiState.collectAsStateWithLifecycle()
|
||||||
var autoPlayHandled by rememberSaveable(route.videoId, effectiveVideoId) { mutableStateOf(false) }
|
var autoPlayHandled by rememberSaveable(launch.videoId, effectiveVideoId) { mutableStateOf(false) }
|
||||||
LaunchedEffect(streamsUiState.autoPlayStream, reuseHandled, route.manualSelection) {
|
LaunchedEffect(streamsUiState.autoPlayStream, reuseHandled, launch.manualSelection) {
|
||||||
if (!reuseHandled) return@LaunchedEffect
|
if (!reuseHandled) return@LaunchedEffect
|
||||||
if (route.manualSelection) return@LaunchedEffect
|
if (launch.manualSelection) return@LaunchedEffect
|
||||||
if (reuseNavigated) return@LaunchedEffect
|
if (reuseNavigated) return@LaunchedEffect
|
||||||
if (autoPlayHandled) return@LaunchedEffect
|
if (autoPlayHandled) return@LaunchedEffect
|
||||||
val stream = streamsUiState.autoPlayStream ?: return@LaunchedEffect
|
val stream = streamsUiState.autoPlayStream ?: return@LaunchedEffect
|
||||||
val sourceUrl = stream.directPlaybackUrl ?: return@LaunchedEffect
|
val sourceUrl = stream.directPlaybackUrl ?: return@LaunchedEffect
|
||||||
autoPlayHandled = true
|
autoPlayHandled = true
|
||||||
if (playerSettings.streamReuseLastLinkEnabled) {
|
if (playerSettings.streamReuseLastLinkEnabled) {
|
||||||
val cacheKey = StreamLinkCacheRepository.contentKey(route.type, effectiveVideoId)
|
val cacheKey = StreamLinkCacheRepository.contentKey(launch.type, effectiveVideoId)
|
||||||
StreamLinkCacheRepository.save(
|
StreamLinkCacheRepository.save(
|
||||||
contentKey = cacheKey,
|
contentKey = cacheKey,
|
||||||
url = sourceUrl,
|
url = sourceUrl,
|
||||||
|
|
@ -1217,33 +1221,32 @@ private fun MainAppContent(
|
||||||
}
|
}
|
||||||
val launchId = PlayerLaunchStore.put(
|
val launchId = PlayerLaunchStore.put(
|
||||||
PlayerLaunch(
|
PlayerLaunch(
|
||||||
title = route.title,
|
title = launch.title,
|
||||||
sourceUrl = sourceUrl,
|
sourceUrl = sourceUrl,
|
||||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||||
logo = route.logo,
|
logo = launch.logo,
|
||||||
poster = route.poster,
|
poster = launch.poster,
|
||||||
background = route.background,
|
background = launch.background,
|
||||||
seasonNumber = route.seasonNumber,
|
seasonNumber = launch.seasonNumber,
|
||||||
episodeNumber = route.episodeNumber,
|
episodeNumber = launch.episodeNumber,
|
||||||
episodeTitle = route.episodeTitle,
|
episodeTitle = launch.episodeTitle,
|
||||||
episodeThumbnail = route.episodeThumbnail,
|
episodeThumbnail = launch.episodeThumbnail,
|
||||||
streamTitle = stream.streamLabel,
|
streamTitle = stream.streamLabel,
|
||||||
streamSubtitle = stream.streamSubtitle,
|
streamSubtitle = stream.streamSubtitle,
|
||||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||||
pauseDescription = pauseDescription,
|
pauseDescription = pauseDescription,
|
||||||
providerName = stream.addonName,
|
providerName = stream.addonName,
|
||||||
providerAddonId = stream.addonId,
|
providerAddonId = stream.addonId,
|
||||||
contentType = route.type,
|
contentType = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = route.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
parentMetaType = route.parentMetaType ?: route.type,
|
parentMetaType = launch.parentMetaType ?: launch.type,
|
||||||
initialPositionMs = route.resumePositionMs ?: 0L,
|
initialPositionMs = launch.resumePositionMs ?: 0L,
|
||||||
initialProgressFraction = route.resumeProgressFraction,
|
initialProgressFraction = launch.resumeProgressFraction,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
StreamsRepository.consumeAutoPlay()
|
StreamsRepository.consumeAutoPlay()
|
||||||
route.streamContextId?.let(StreamContextStore::remove)
|
|
||||||
navController.navigate(PlayerRoute(launchId = launchId)) {
|
navController.navigate(PlayerRoute(launchId = launchId)) {
|
||||||
popUpTo<StreamRoute> { inclusive = true }
|
popUpTo<StreamRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
|
|
@ -1260,28 +1263,28 @@ private fun MainAppContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamsScreen(
|
StreamsScreen(
|
||||||
type = route.type,
|
type = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = route.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
parentMetaType = route.parentMetaType ?: route.type,
|
parentMetaType = launch.parentMetaType ?: launch.type,
|
||||||
title = route.title,
|
title = launch.title,
|
||||||
logo = route.logo,
|
logo = launch.logo,
|
||||||
poster = route.poster,
|
poster = launch.poster,
|
||||||
background = route.background,
|
background = launch.background,
|
||||||
seasonNumber = route.seasonNumber,
|
seasonNumber = launch.seasonNumber,
|
||||||
episodeNumber = route.episodeNumber,
|
episodeNumber = launch.episodeNumber,
|
||||||
episodeTitle = route.episodeTitle,
|
episodeTitle = launch.episodeTitle,
|
||||||
episodeThumbnail = route.episodeThumbnail,
|
episodeThumbnail = launch.episodeThumbnail,
|
||||||
resumePositionMs = route.resumePositionMs,
|
resumePositionMs = launch.resumePositionMs,
|
||||||
resumeProgressFraction = route.resumeProgressFraction,
|
resumeProgressFraction = launch.resumeProgressFraction,
|
||||||
manualSelection = route.manualSelection,
|
manualSelection = launch.manualSelection,
|
||||||
startFromBeginning = route.startFromBeginning,
|
startFromBeginning = launch.startFromBeginning,
|
||||||
onStreamSelected = { stream, resolvedResumePositionMs, resolvedResumeProgressFraction ->
|
onStreamSelected = { stream, resolvedResumePositionMs, resolvedResumeProgressFraction ->
|
||||||
val sourceUrl = stream.directPlaybackUrl
|
val sourceUrl = stream.directPlaybackUrl
|
||||||
if (sourceUrl != null) {
|
if (sourceUrl != null) {
|
||||||
// Persist for Reuse Last Link
|
// Persist for Reuse Last Link
|
||||||
if (playerSettings.streamReuseLastLinkEnabled) {
|
if (playerSettings.streamReuseLastLinkEnabled) {
|
||||||
val cacheKey = StreamLinkCacheRepository.contentKey(route.type, effectiveVideoId)
|
val cacheKey = StreamLinkCacheRepository.contentKey(launch.type, effectiveVideoId)
|
||||||
StreamLinkCacheRepository.save(
|
StreamLinkCacheRepository.save(
|
||||||
contentKey = cacheKey,
|
contentKey = cacheKey,
|
||||||
url = sourceUrl,
|
url = sourceUrl,
|
||||||
|
|
@ -1297,39 +1300,37 @@ private fun MainAppContent(
|
||||||
}
|
}
|
||||||
val launchId = PlayerLaunchStore.put(
|
val launchId = PlayerLaunchStore.put(
|
||||||
PlayerLaunch(
|
PlayerLaunch(
|
||||||
title = route.title,
|
title = launch.title,
|
||||||
sourceUrl = sourceUrl,
|
sourceUrl = sourceUrl,
|
||||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||||
logo = route.logo,
|
logo = launch.logo,
|
||||||
poster = route.poster,
|
poster = launch.poster,
|
||||||
background = route.background,
|
background = launch.background,
|
||||||
seasonNumber = route.seasonNumber,
|
seasonNumber = launch.seasonNumber,
|
||||||
episodeNumber = route.episodeNumber,
|
episodeNumber = launch.episodeNumber,
|
||||||
episodeTitle = route.episodeTitle,
|
episodeTitle = launch.episodeTitle,
|
||||||
episodeThumbnail = route.episodeThumbnail,
|
episodeThumbnail = launch.episodeThumbnail,
|
||||||
streamTitle = stream.streamLabel,
|
streamTitle = stream.streamLabel,
|
||||||
streamSubtitle = stream.streamSubtitle,
|
streamSubtitle = stream.streamSubtitle,
|
||||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||||
pauseDescription = pauseDescription,
|
pauseDescription = pauseDescription,
|
||||||
providerName = stream.addonName,
|
providerName = stream.addonName,
|
||||||
providerAddonId = stream.addonId,
|
providerAddonId = stream.addonId,
|
||||||
contentType = route.type,
|
contentType = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
parentMetaId = route.parentMetaId ?: effectiveVideoId,
|
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||||
parentMetaType = route.parentMetaType ?: route.type,
|
parentMetaType = launch.parentMetaType ?: launch.type,
|
||||||
initialPositionMs = resolvedResumePositionMs ?: 0L,
|
initialPositionMs = resolvedResumePositionMs ?: 0L,
|
||||||
initialProgressFraction = resolvedResumeProgressFraction,
|
initialProgressFraction = resolvedResumeProgressFraction,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
route.streamContextId?.let(StreamContextStore::remove)
|
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
PlayerRoute(launchId = launchId)
|
PlayerRoute(launchId = launchId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBack = {
|
onBack = {
|
||||||
route.streamContextId?.let(StreamContextStore::remove)
|
|
||||||
StreamsRepository.clear()
|
StreamsRepository.clear()
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.nuvio.app.features.profiles.ProfileRepository
|
||||||
import com.nuvio.app.features.search.SearchRepository
|
import com.nuvio.app.features.search.SearchRepository
|
||||||
import com.nuvio.app.features.settings.ThemeSettingsRepository
|
import com.nuvio.app.features.settings.ThemeSettingsRepository
|
||||||
import com.nuvio.app.features.streams.StreamContextStore
|
import com.nuvio.app.features.streams.StreamContextStore
|
||||||
|
import com.nuvio.app.features.streams.StreamLaunchStore
|
||||||
import com.nuvio.app.features.streams.StreamsRepository
|
import com.nuvio.app.features.streams.StreamsRepository
|
||||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||||
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
||||||
|
|
@ -53,6 +54,7 @@ internal object LocalAccountDataCleaner {
|
||||||
SearchRepository.reset()
|
SearchRepository.reset()
|
||||||
SubtitleRepository.clear()
|
SubtitleRepository.clear()
|
||||||
PlayerLaunchStore.clear()
|
PlayerLaunchStore.clear()
|
||||||
|
StreamLaunchStore.clear()
|
||||||
StreamContextStore.clear()
|
StreamContextStore.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.nuvio.app.features.streams
|
||||||
|
|
||||||
|
data class StreamLaunch(
|
||||||
|
val type: String,
|
||||||
|
val videoId: String,
|
||||||
|
val parentMetaId: String? = null,
|
||||||
|
val parentMetaType: String? = null,
|
||||||
|
val title: String,
|
||||||
|
val logo: String? = null,
|
||||||
|
val poster: String? = null,
|
||||||
|
val background: String? = null,
|
||||||
|
val seasonNumber: Int? = null,
|
||||||
|
val episodeNumber: Int? = null,
|
||||||
|
val episodeTitle: String? = null,
|
||||||
|
val episodeThumbnail: String? = null,
|
||||||
|
val pauseDescription: String? = null,
|
||||||
|
val resumePositionMs: Long? = null,
|
||||||
|
val resumeProgressFraction: Float? = null,
|
||||||
|
val manualSelection: Boolean = false,
|
||||||
|
val startFromBeginning: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
object StreamLaunchStore {
|
||||||
|
private var nextLaunchId = 1L
|
||||||
|
private val launches = mutableMapOf<Long, StreamLaunch>()
|
||||||
|
|
||||||
|
fun put(launch: StreamLaunch): Long {
|
||||||
|
val launchId = nextLaunchId++
|
||||||
|
launches[launchId] = launch
|
||||||
|
return launchId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(launchId: Long): StreamLaunch? = launches[launchId]
|
||||||
|
|
||||||
|
fun remove(launchId: Long) {
|
||||||
|
launches.remove(launchId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
nextLaunchId = 1L
|
||||||
|
launches.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue