From bcf04cfc09edbbb91bd6482042e4576cba107d6d Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 14:42:03 +0000 Subject: [PATCH 1/7] Fix: observe order by having an instance number --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 45 +++++--- .../ui/player/PlayerGeneratorViewModel.kt | 100 +++++++++++------- 2 files changed, 94 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 9ee85a941..dcf6003e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -143,7 +143,11 @@ class GeneratorPlayer : FullScreenPlayer() { const val STOP_ACTION = "stopcs3" private val generators = ConcurrentHashMap>() - fun newInstance(generator: VideoGenerator<*>, index : Int, syncData: HashMap? = null): Bundle { + fun newInstance( + generator: VideoGenerator<*>, + index: Int, + syncData: HashMap? = null + ): Bundle { Log.i(TAG, "newInstance = $syncData") val uuid = UUID.randomUUID().toString() generators[uuid] = generator @@ -178,12 +182,14 @@ class GeneratorPlayer : FullScreenPlayer() { private var isNextEpisode: Boolean = false // this is used to reset the watch time private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none - private val allMeta: List? get() = viewModel.state.generatorState?.allMeta?.filterIsInstance()?.map { episode -> - // Refresh all the episodes watch duration - getViewPos(episode.id)?.let { data -> - episode.copy(position = data.position, duration = data.duration) - } ?: episode - } + private val allMeta: List? + get() = viewModel.state.generatorState?.allMeta?.filterIsInstance() + ?.map { episode -> + // Refresh all the episodes watch duration + getViewPos(episode.id)?.let { data -> + episode.copy(position = data.position, duration = data.duration) + } ?: episode + } private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean { // If subtitle is changed and user initiated -> Save the language @@ -1541,7 +1547,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun startPlayer() { // We don't want double load when you skip loading - if(isPlayerActive.get()) { + if (isPlayerActive.get()) { return } @@ -2165,6 +2171,13 @@ class GeneratorPlayer : FullScreenPlayer() { unwrapBundle(arguments) super.onBindingCreated(binding, savedInstanceState) + + // Avoid showing no links found + if(viewModel.generator == null) { + exitPlayer() + return + } + context?.let { ctx -> val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) showName = settingsManager.getBoolean(ctx.getString(R.string.show_name_key), true) @@ -2200,7 +2213,7 @@ class GeneratorPlayer : FullScreenPlayer() { binding.overlayLoadingSkipButton.setOnClickListener { // Mark as "success" early viewModel.modifyState { - copy(loading = Resource.Success(true)) + copy(loading = Resource.Success(Unit)) } } @@ -2218,11 +2231,13 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentStamps) { stamps -> + observe(viewModel.currentStamps) { (stamps, instance) -> + if (instance != viewModel.state.instance) return@observe // Outdated observe player.addTimeStamps(stamps) } - observe(viewModel.currentSubtitles) { subtitles -> + observe(viewModel.currentSubtitles) { (subtitles, instance) -> + if (instance != viewModel.state.instance) return@observe // Outdated observe player.setActiveSubtitles(subtitles) // If the file is downloaded then do not select auto select the subtitles @@ -2233,7 +2248,9 @@ class GeneratorPlayer : FullScreenPlayer() { autoSelectSubtitles() } } - observe(viewModel.loadingLinks) { loading -> + observe(viewModel.loadingLinks) { (loading, instance) -> + if (instance != viewModel.state.instance) return@observe // Outdated observe + when (loading) { is Resource.Loading -> { releasePlayer() @@ -2254,7 +2271,9 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentLinks) { links -> + observe(viewModel.currentLinks) { (links, instance) -> + if (instance != viewModel.state.instance) return@observe // Outdated observe + val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true val wasGone = binding.overlayLoadingSkipButton.isGone diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 049ed06d6..6b3ea55dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -47,8 +47,9 @@ data class VideoState( val subtitles: PersistentSet = persistentSetOf(), val links: PersistentSet = persistentSetOf(), val stamps: PersistentList = persistentListOf(), - val loading: Resource = Resource.Loading(), + val loading: Resource = Resource.Loading(), val generatorState: GeneratorState? = null, + val instance: Int, ) { /** * This acts as a local cache for sorted links that are not copied over by the copy constructor. @@ -114,6 +115,11 @@ data class VideoState( fun set(items: Collection): VideoState = copy(stamps = items.toPersistentList()) } +data class VideoLive( + val value: T, + val instance: Int, +) + class PlayerGeneratorViewModel : ViewModel() { companion object { const val TAG = "PlayViewGen" @@ -132,20 +138,21 @@ class PlayerGeneratorViewModel : ViewModel() { * This value can be used without Synchronized or locking when reading, as all fields are immutable. * */ @Volatile - var state = VideoState() + var state = VideoState(instance = 0) private set - private val _currentLinks = MutableLiveData>>(setOf()) - val currentLinks: LiveData>> = _currentLinks + private val _currentLinks = + MutableLiveData>>>(null) + val currentLinks: LiveData>>> = _currentLinks - private val _currentSubtitles = MutableLiveData>(setOf()) - val currentSubtitles: LiveData> = _currentSubtitles + private val _currentSubtitles = MutableLiveData>>(null) + val currentSubtitles: LiveData>> = _currentSubtitles - private val _loadingLinks = MutableLiveData>() - val loadingLinks: LiveData> = _loadingLinks + private val _loadingLinks = MutableLiveData>>() + val loadingLinks: LiveData>> = _loadingLinks - private val _currentStamps = MutableLiveData>(emptyList()) - val currentStamps: LiveData> = _currentStamps + private val _currentStamps = MutableLiveData>>(null) + val currentStamps: LiveData>> = _currentStamps /** * Modifies the `state` variable safely, and with the correct observe behavior. @@ -158,6 +165,15 @@ class PlayerGeneratorViewModel : ViewModel() { val oldState = state state = op.invoke(oldState) + /** New instance, always push state */ + if (state.instance != oldState.instance) { + _currentSubtitles.postValue(VideoLive(state.subtitles, state.instance)) + _currentStamps.postValue(VideoLive(state.stamps, state.instance)) + _currentLinks.postValue(VideoLive(state.links, state.instance)) + _loadingLinks.postValue(VideoLive(state.loading, state.instance)) + return + } + /** * Only post the changed values, this makes sure we do not invoke the "observe" * @@ -165,15 +181,15 @@ class PlayerGeneratorViewModel : ViewModel() { * to avoid comparing the entire set or list as "Persistent" classes will hold the same reference if they are unchanged. * */ if (state.links !== oldState.links) - _currentLinks.postValue(state.links) + _currentLinks.postValue(VideoLive(state.links, state.instance)) if (state.stamps !== oldState.stamps) - _currentStamps.postValue(state.stamps) + _currentStamps.postValue(VideoLive(state.stamps, state.instance)) if (state.subtitles !== oldState.subtitles) - _currentSubtitles.postValue(state.subtitles) + _currentSubtitles.postValue(VideoLive(state.subtitles, state.instance)) /** Normal equality here as it is not a collection */ if (state.loading != oldState.loading) - _loadingLinks.postValue(state.loading) + _loadingLinks.postValue(VideoLive(state.loading, state.instance)) } private val _currentSubtitleYear = MutableLiveData(null) @@ -253,6 +269,7 @@ class PlayerGeneratorViewModel : ViewModel() { } fun attachGenerator(newGenerator: VideoGenerator<*>?, index: Int?) { + Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index") if (generator == null) { generator = newGenerator if (index != null) { @@ -321,45 +338,48 @@ class PlayerGeneratorViewModel : ViewModel() { } fun loadLinks(sourceTypes: Set = LOADTYPE_INAPP) { - Log.i(TAG, "loadLinks") + Log.i(TAG, "loadLinks with generator=$generator and index=$episodeIndex") currentJob?.cancel() val index = episodeIndex - currentJob = viewModelScope.launchSafe { - // Clear old data and reset the state - modifyState { - VideoState( - generatorState = generator?.let { gen -> - GeneratorState( - meta = gen.videos.getOrNull(index), - nextMeta = gen.videos.getOrNull(index + 1), - id = gen.getId(index), - response = (gen as? RepoLinkGenerator)?.page, - index = index, - allMeta = gen.videos - ) - } - ) - } + // Clear old data and reset the state + modifyState { + VideoState( + generatorState = generator?.let { gen -> + GeneratorState( + meta = gen.videos.getOrNull(index), + nextMeta = gen.videos.getOrNull(index + 1), + id = gen.getId(index), + response = (gen as? RepoLinkGenerator)?.page, + index = index, + allMeta = gen.videos + ) + }, + instance = instance + 1 + ) + } + currentJob = viewModelScope.launchSafe { // Load more data val loadingState = safeApiCall { generator?.generateLinks( sourceTypes = sourceTypes, clearCache = forceClearCache, callback = { link -> - modifyState { - add(link) - } + if (isActive) + modifyState { + add(link) + } }, isCasting = false, offset = index, subtitleCallback = { link -> - if (isValidSubtitle(link)) + if (isActive && isValidSubtitle(link)) modifyState { add(link) } }) + Unit } if (!isActive) { @@ -368,9 +388,13 @@ class PlayerGeneratorViewModel : ViewModel() { /** Only mark as success if we have not skipped loading */ modifyState { - when (loading) { - is Resource.Loading -> copy(loading = loadingState) - else -> this + if (!isActive) { + this + } else { + when (loading) { + is Resource.Loading -> copy(loading = loadingState) + else -> this + } } } } From 35171b4a5d7bf614374232bcb709f43810a4463f Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 15:02:37 +0000 Subject: [PATCH 2/7] Fix: Fragment recreation --- .../lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index dcf6003e1..64277156d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -2146,6 +2146,7 @@ class GeneratorPlayer : FullScreenPlayer() { fun releasePlayer() { player.release() currentSelectedSubtitles = null + currentSelectedLink = null isPlayerActive.set(false) binding?.overlayLoadingSkipButton?.isVisible = false binding?.playerLoadingOverlay?.isVisible = true @@ -2206,8 +2207,12 @@ class GeneratorPlayer : FullScreenPlayer() { preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF() - if (currentSelectedLink == null) { + val selectedLink = currentSelectedLink + if (selectedLink == null) { viewModel.loadLinks() + } else { + // Recreated view, so we need to recreate the + loadLink(selectedLink, true) } binding.overlayLoadingSkipButton.setOnClickListener { From 80acc5a6a27404e02bad8968c35b026c8568d801 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 15:23:58 +0000 Subject: [PATCH 3/7] Fix: Add onSaveInstanceState just in case --- .../com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 64277156d..aa732c2fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -2159,6 +2159,11 @@ class GeneratorPlayer : FullScreenPlayer() { activity?.popCurrentPage() } + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt("index", viewModel.episodeIndex) + super.onSaveInstanceState(outState) + } + override fun onBindingCreated(binding: FragmentPlayerBinding, savedInstanceState: Bundle?) { viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] From 1a77d2057d8a308b46fa818901acc75d09ddf517 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 15:35:14 +0000 Subject: [PATCH 4/7] Forgot to push episodeIndex --- .../cloudstream3/ui/player/PlayerGeneratorViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 6b3ea55dc..5a9d07122 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -129,7 +129,7 @@ class PlayerGeneratorViewModel : ViewModel() { var generator: VideoGenerator<*>? = null @Volatile - private var episodeIndex: Int = 0 + var episodeIndex: Int = 0 /** * The state of the video player, only modify it by modifyState to make sure observe is called, From 6fcb99489b5d996ea68a8ec41ad7d2774adff2c8 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 16:11:57 +0000 Subject: [PATCH 5/7] Fix: Always attach generator and index --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +++--- .../cloudstream3/ui/player/PlayerGeneratorViewModel.kt | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index aa732c2fd..37e25a452 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -2170,8 +2170,7 @@ class GeneratorPlayer : FullScreenPlayer() { val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid") val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index") - - viewModel.attachGenerator(generators[uuid], index) + val generator = generators[uuid] unwrapBundle(savedInstanceState) unwrapBundle(arguments) @@ -2179,10 +2178,11 @@ class GeneratorPlayer : FullScreenPlayer() { super.onBindingCreated(binding, savedInstanceState) // Avoid showing no links found - if(viewModel.generator == null) { + if (generator == null || index == null) { exitPlayer() return } + viewModel.attachGenerator(generator, index) context?.let { ctx -> val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 5a9d07122..2f8c8b8dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -268,14 +268,10 @@ class PlayerGeneratorViewModel : ViewModel() { loadLinks() } - fun attachGenerator(newGenerator: VideoGenerator<*>?, index: Int?) { + fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) { Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index") - if (generator == null) { - generator = newGenerator - if (index != null) { - episodeIndex = index - } - } + generator = newGenerator + episodeIndex = index } /** From 62ed338c1d11ca57ffdfd09dd7fe4e15a9429a99 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 16:27:54 +0000 Subject: [PATCH 6/7] Fix: Always set loading = true --- .../lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 2f8c8b8dd..e3c390d50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -341,6 +341,7 @@ class PlayerGeneratorViewModel : ViewModel() { // Clear old data and reset the state modifyState { VideoState( + loading = Resource.Loading(), generatorState = generator?.let { gen -> GeneratorState( meta = gen.videos.getOrNull(index), From 519bea8117613dbffba3f8795e56f67b9af24ce7 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 6 May 2026 16:37:39 +0000 Subject: [PATCH 7/7] Fix: Reduce getLinkPriority calls --- .../com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 37e25a452..2dfd5ef4d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -2298,7 +2298,7 @@ class GeneratorPlayer : FullScreenPlayer() { } safe { - if (viewModel.state.links.any { link -> + if (!isPlayerActive.get() && viewModel.state.links.any { link -> getLinkPriority(currentQualityProfile, link.first) >= QualityDataHelper.AUTO_SKIP_PRIORITY }