Compare commits

...

7 commits

Author SHA1 Message Date
firelight
519bea8117
Fix: Reduce getLinkPriority calls 2026-05-06 16:37:39 +00:00
firelight
62ed338c1d
Fix: Always set loading = true 2026-05-06 16:27:54 +00:00
firelight
6fcb99489b
Fix: Always attach generator and index 2026-05-06 16:11:57 +00:00
firelight
1a77d2057d
Forgot to push episodeIndex 2026-05-06 15:35:14 +00:00
firelight
80acc5a6a2
Fix: Add onSaveInstanceState just in case 2026-05-06 15:23:58 +00:00
firelight
35171b4a5d
Fix: Fragment recreation 2026-05-06 15:02:37 +00:00
firelight
bcf04cfc09
Fix: observe order by having an instance number 2026-05-06 14:42:03 +00:00
2 changed files with 113 additions and 63 deletions

View file

@ -143,7 +143,11 @@ class GeneratorPlayer : FullScreenPlayer() {
const val STOP_ACTION = "stopcs3" const val STOP_ACTION = "stopcs3"
private val generators = ConcurrentHashMap<String, VideoGenerator<*>>() private val generators = ConcurrentHashMap<String, VideoGenerator<*>>()
fun newInstance(generator: VideoGenerator<*>, index : Int, syncData: HashMap<String, String>? = null): Bundle { fun newInstance(
generator: VideoGenerator<*>,
index: Int,
syncData: HashMap<String, String>? = null
): Bundle {
Log.i(TAG, "newInstance = $syncData") Log.i(TAG, "newInstance = $syncData")
val uuid = UUID.randomUUID().toString() val uuid = UUID.randomUUID().toString()
generators[uuid] = generator generators[uuid] = generator
@ -178,7 +182,9 @@ class GeneratorPlayer : FullScreenPlayer() {
private var isNextEpisode: Boolean = false // this is used to reset the watch time 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 var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none
private val allMeta: List<ResultEpisode>? get() = viewModel.state.generatorState?.allMeta?.filterIsInstance<ResultEpisode>()?.map { episode -> private val allMeta: List<ResultEpisode>?
get() = viewModel.state.generatorState?.allMeta?.filterIsInstance<ResultEpisode>()
?.map { episode ->
// Refresh all the episodes watch duration // Refresh all the episodes watch duration
getViewPos(episode.id)?.let { data -> getViewPos(episode.id)?.let { data ->
episode.copy(position = data.position, duration = data.duration) episode.copy(position = data.position, duration = data.duration)
@ -1541,7 +1547,7 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun startPlayer() { private fun startPlayer() {
// We don't want double load when you skip loading // We don't want double load when you skip loading
if(isPlayerActive.get()) { if (isPlayerActive.get()) {
return return
} }
@ -2140,6 +2146,7 @@ class GeneratorPlayer : FullScreenPlayer() {
fun releasePlayer() { fun releasePlayer() {
player.release() player.release()
currentSelectedSubtitles = null currentSelectedSubtitles = null
currentSelectedLink = null
isPlayerActive.set(false) isPlayerActive.set(false)
binding?.overlayLoadingSkipButton?.isVisible = false binding?.overlayLoadingSkipButton?.isVisible = false
binding?.playerLoadingOverlay?.isVisible = true binding?.playerLoadingOverlay?.isVisible = true
@ -2152,19 +2159,31 @@ class GeneratorPlayer : FullScreenPlayer() {
activity?.popCurrentPage() activity?.popCurrentPage()
} }
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt("index", viewModel.episodeIndex)
super.onSaveInstanceState(outState)
}
override fun onBindingCreated(binding: FragmentPlayerBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentPlayerBinding, savedInstanceState: Bundle?) {
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
sync = ViewModelProvider(this)[SyncViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java]
val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid") val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid")
val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index") val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index")
val generator = generators[uuid]
viewModel.attachGenerator(generators[uuid], index)
unwrapBundle(savedInstanceState) unwrapBundle(savedInstanceState)
unwrapBundle(arguments) unwrapBundle(arguments)
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
// Avoid showing no links found
if (generator == null || index == null) {
exitPlayer()
return
}
viewModel.attachGenerator(generator, index)
context?.let { ctx -> context?.let { ctx ->
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
showName = settingsManager.getBoolean(ctx.getString(R.string.show_name_key), true) showName = settingsManager.getBoolean(ctx.getString(R.string.show_name_key), true)
@ -2193,14 +2212,18 @@ class GeneratorPlayer : FullScreenPlayer() {
preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF() preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF()
if (currentSelectedLink == null) { val selectedLink = currentSelectedLink
if (selectedLink == null) {
viewModel.loadLinks() viewModel.loadLinks()
} else {
// Recreated view, so we need to recreate the
loadLink(selectedLink, true)
} }
binding.overlayLoadingSkipButton.setOnClickListener { binding.overlayLoadingSkipButton.setOnClickListener {
// Mark as "success" early // Mark as "success" early
viewModel.modifyState { viewModel.modifyState {
copy(loading = Resource.Success(true)) copy(loading = Resource.Success(Unit))
} }
} }
@ -2218,11 +2241,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) player.addTimeStamps(stamps)
} }
observe(viewModel.currentSubtitles) { subtitles -> observe(viewModel.currentSubtitles) { (subtitles, instance) ->
if (instance != viewModel.state.instance) return@observe // Outdated observe
player.setActiveSubtitles(subtitles) player.setActiveSubtitles(subtitles)
// If the file is downloaded then do not select auto select the subtitles // If the file is downloaded then do not select auto select the subtitles
@ -2233,7 +2258,9 @@ class GeneratorPlayer : FullScreenPlayer() {
autoSelectSubtitles() autoSelectSubtitles()
} }
} }
observe(viewModel.loadingLinks) { loading -> observe(viewModel.loadingLinks) { (loading, instance) ->
if (instance != viewModel.state.instance) return@observe // Outdated observe
when (loading) { when (loading) {
is Resource.Loading -> { is Resource.Loading -> {
releasePlayer() releasePlayer()
@ -2254,7 +2281,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 turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true
val wasGone = binding.overlayLoadingSkipButton.isGone val wasGone = binding.overlayLoadingSkipButton.isGone
@ -2269,7 +2298,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
safe { safe {
if (viewModel.state.links.any { link -> if (!isPlayerActive.get() && viewModel.state.links.any { link ->
getLinkPriority(currentQualityProfile, link.first) >= getLinkPriority(currentQualityProfile, link.first) >=
QualityDataHelper.AUTO_SKIP_PRIORITY QualityDataHelper.AUTO_SKIP_PRIORITY
} }

View file

@ -47,8 +47,9 @@ data class VideoState(
val subtitles: PersistentSet<SubtitleData> = persistentSetOf(), val subtitles: PersistentSet<SubtitleData> = persistentSetOf(),
val links: PersistentSet<VideoLink> = persistentSetOf(), val links: PersistentSet<VideoLink> = persistentSetOf(),
val stamps: PersistentList<VideoSkipStamp> = persistentListOf(), val stamps: PersistentList<VideoSkipStamp> = persistentListOf(),
val loading: Resource<Boolean?> = Resource.Loading(), val loading: Resource<Unit> = Resource.Loading(),
val generatorState: GeneratorState? = null, 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. * 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<VideoSkipStamp>): VideoState = copy(stamps = items.toPersistentList()) fun set(items: Collection<VideoSkipStamp>): VideoState = copy(stamps = items.toPersistentList())
} }
data class VideoLive<T>(
val value: T,
val instance: Int,
)
class PlayerGeneratorViewModel : ViewModel() { class PlayerGeneratorViewModel : ViewModel() {
companion object { companion object {
const val TAG = "PlayViewGen" const val TAG = "PlayViewGen"
@ -123,7 +129,7 @@ class PlayerGeneratorViewModel : ViewModel() {
var generator: VideoGenerator<*>? = null var generator: VideoGenerator<*>? = null
@Volatile @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, * The state of the video player, only modify it by modifyState to make sure observe is called,
@ -132,20 +138,21 @@ class PlayerGeneratorViewModel : ViewModel() {
* This value can be used without Synchronized or locking when reading, as all fields are immutable. * This value can be used without Synchronized or locking when reading, as all fields are immutable.
* */ * */
@Volatile @Volatile
var state = VideoState() var state = VideoState(instance = 0)
private set private set
private val _currentLinks = MutableLiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>>(setOf()) private val _currentLinks =
val currentLinks: LiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>> = _currentLinks MutableLiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>>(null)
val currentLinks: LiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>> = _currentLinks
private val _currentSubtitles = MutableLiveData<Set<SubtitleData>>(setOf()) private val _currentSubtitles = MutableLiveData<VideoLive<Set<SubtitleData>>>(null)
val currentSubtitles: LiveData<Set<SubtitleData>> = _currentSubtitles val currentSubtitles: LiveData<VideoLive<Set<SubtitleData>>> = _currentSubtitles
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>() private val _loadingLinks = MutableLiveData<VideoLive<Resource<Unit>>>()
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks val loadingLinks: LiveData<VideoLive<Resource<Unit>>> = _loadingLinks
private val _currentStamps = MutableLiveData<List<VideoSkipStamp>>(emptyList()) private val _currentStamps = MutableLiveData<VideoLive<List<VideoSkipStamp>>>(null)
val currentStamps: LiveData<List<VideoSkipStamp>> = _currentStamps val currentStamps: LiveData<VideoLive<List<VideoSkipStamp>>> = _currentStamps
/** /**
* Modifies the `state` variable safely, and with the correct observe behavior. * Modifies the `state` variable safely, and with the correct observe behavior.
@ -158,6 +165,15 @@ class PlayerGeneratorViewModel : ViewModel() {
val oldState = state val oldState = state
state = op.invoke(oldState) 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" * 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. * 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) if (state.links !== oldState.links)
_currentLinks.postValue(state.links) _currentLinks.postValue(VideoLive(state.links, state.instance))
if (state.stamps !== oldState.stamps) if (state.stamps !== oldState.stamps)
_currentStamps.postValue(state.stamps) _currentStamps.postValue(VideoLive(state.stamps, state.instance))
if (state.subtitles !== oldState.subtitles) 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 */ /** Normal equality here as it is not a collection */
if (state.loading != oldState.loading) if (state.loading != oldState.loading)
_loadingLinks.postValue(state.loading) _loadingLinks.postValue(VideoLive(state.loading, state.instance))
} }
private val _currentSubtitleYear = MutableLiveData<Int?>(null) private val _currentSubtitleYear = MutableLiveData<Int?>(null)
@ -252,14 +268,11 @@ class PlayerGeneratorViewModel : ViewModel() {
loadLinks() loadLinks()
} }
fun attachGenerator(newGenerator: VideoGenerator<*>?, index: Int?) { fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) {
if (generator == null) { Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index")
generator = newGenerator generator = newGenerator
if (index != null) {
episodeIndex = index episodeIndex = index
} }
}
}
/** /**
* If duplicate nothing will happen * If duplicate nothing will happen
@ -321,14 +334,14 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
fun loadLinks(sourceTypes: Set<ExtractorLinkType> = LOADTYPE_INAPP) { fun loadLinks(sourceTypes: Set<ExtractorLinkType> = LOADTYPE_INAPP) {
Log.i(TAG, "loadLinks") Log.i(TAG, "loadLinks with generator=$generator and index=$episodeIndex")
currentJob?.cancel() currentJob?.cancel()
val index = episodeIndex val index = episodeIndex
currentJob = viewModelScope.launchSafe {
// Clear old data and reset the state // Clear old data and reset the state
modifyState { modifyState {
VideoState( VideoState(
loading = Resource.Loading(),
generatorState = generator?.let { gen -> generatorState = generator?.let { gen ->
GeneratorState( GeneratorState(
meta = gen.videos.getOrNull(index), meta = gen.videos.getOrNull(index),
@ -338,16 +351,19 @@ class PlayerGeneratorViewModel : ViewModel() {
index = index, index = index,
allMeta = gen.videos allMeta = gen.videos
) )
} },
instance = instance + 1
) )
} }
currentJob = viewModelScope.launchSafe {
// Load more data // Load more data
val loadingState = safeApiCall { val loadingState = safeApiCall {
generator?.generateLinks( generator?.generateLinks(
sourceTypes = sourceTypes, sourceTypes = sourceTypes,
clearCache = forceClearCache, clearCache = forceClearCache,
callback = { link -> callback = { link ->
if (isActive)
modifyState { modifyState {
add(link) add(link)
} }
@ -355,11 +371,12 @@ class PlayerGeneratorViewModel : ViewModel() {
isCasting = false, isCasting = false,
offset = index, offset = index,
subtitleCallback = { link -> subtitleCallback = { link ->
if (isValidSubtitle(link)) if (isActive && isValidSubtitle(link))
modifyState { modifyState {
add(link) add(link)
} }
}) })
Unit
} }
if (!isActive) { if (!isActive) {
@ -368,6 +385,9 @@ class PlayerGeneratorViewModel : ViewModel() {
/** Only mark as success if we have not skipped loading */ /** Only mark as success if we have not skipped loading */
modifyState { modifyState {
if (!isActive) {
this
} else {
when (loading) { when (loading) {
is Resource.Loading -> copy(loading = loadingState) is Resource.Loading -> copy(loading = loadingState)
else -> this else -> this
@ -375,4 +395,5 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
} }
} }
}
} }