fix: streamroute to crarry stream payload in memory to avoid ios serialization crash on routing

This commit is contained in:
tapframe 2026-04-16 12:14:41 +05:30
parent f46aa693e2
commit db4d79ff9d
3 changed files with 163 additions and 117 deletions

View file

@ -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()
}, },

View file

@ -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()
} }
} }

View file

@ -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()
}
}