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"
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")
val uuid = UUID.randomUUID().toString()
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 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
getViewPos(episode.id)?.let { data ->
episode.copy(position = data.position, duration = data.duration)
@ -2140,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
@ -2152,19 +2159,31 @@ 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]
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)
super.onBindingCreated(binding, savedInstanceState)
// Avoid showing no links found
if (generator == null || index == null) {
exitPlayer()
return
}
viewModel.attachGenerator(generator, index)
context?.let { ctx ->
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
showName = settingsManager.getBoolean(ctx.getString(R.string.show_name_key), true)
@ -2193,14 +2212,18 @@ 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 {
// Mark as "success" early
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)
}
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 +2258,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 +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 wasGone = binding.overlayLoadingSkipButton.isGone
@ -2269,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
}

View file

@ -47,8 +47,9 @@ data class VideoState(
val subtitles: PersistentSet<SubtitleData> = persistentSetOf(),
val links: PersistentSet<VideoLink> = persistentSetOf(),
val stamps: PersistentList<VideoSkipStamp> = persistentListOf(),
val loading: Resource<Boolean?> = Resource.Loading(),
val loading: Resource<Unit> = 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<VideoSkipStamp>): VideoState = copy(stamps = items.toPersistentList())
}
data class VideoLive<T>(
val value: T,
val instance: Int,
)
class PlayerGeneratorViewModel : ViewModel() {
companion object {
const val TAG = "PlayViewGen"
@ -123,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,
@ -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<Set<Pair<ExtractorLink?, ExtractorUri?>>>(setOf())
val currentLinks: LiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>> = _currentLinks
private val _currentLinks =
MutableLiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>>(null)
val currentLinks: LiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>> = _currentLinks
private val _currentSubtitles = MutableLiveData<Set<SubtitleData>>(setOf())
val currentSubtitles: LiveData<Set<SubtitleData>> = _currentSubtitles
private val _currentSubtitles = MutableLiveData<VideoLive<Set<SubtitleData>>>(null)
val currentSubtitles: LiveData<VideoLive<Set<SubtitleData>>> = _currentSubtitles
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>()
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks
private val _loadingLinks = MutableLiveData<VideoLive<Resource<Unit>>>()
val loadingLinks: LiveData<VideoLive<Resource<Unit>>> = _loadingLinks
private val _currentStamps = MutableLiveData<List<VideoSkipStamp>>(emptyList())
val currentStamps: LiveData<List<VideoSkipStamp>> = _currentStamps
private val _currentStamps = MutableLiveData<VideoLive<List<VideoSkipStamp>>>(null)
val currentStamps: LiveData<VideoLive<List<VideoSkipStamp>>> = _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<Int?>(null)
@ -252,14 +268,11 @@ class PlayerGeneratorViewModel : ViewModel() {
loadLinks()
}
fun attachGenerator(newGenerator: VideoGenerator<*>?, index: Int?) {
if (generator == null) {
fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) {
Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index")
generator = newGenerator
if (index != null) {
episodeIndex = index
}
}
}
/**
* If duplicate nothing will happen
@ -321,14 +334,14 @@ class PlayerGeneratorViewModel : ViewModel() {
}
fun loadLinks(sourceTypes: Set<ExtractorLinkType> = 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(
loading = Resource.Loading(),
generatorState = generator?.let { gen ->
GeneratorState(
meta = gen.videos.getOrNull(index),
@ -338,16 +351,19 @@ class PlayerGeneratorViewModel : ViewModel() {
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 ->
if (isActive)
modifyState {
add(link)
}
@ -355,11 +371,12 @@ class PlayerGeneratorViewModel : ViewModel() {
isCasting = false,
offset = index,
subtitleCallback = { link ->
if (isValidSubtitle(link))
if (isActive && isValidSubtitle(link))
modifyState {
add(link)
}
})
Unit
}
if (!isActive) {
@ -368,6 +385,9 @@ class PlayerGeneratorViewModel : ViewModel() {
/** Only mark as success if we have not skipped loading */
modifyState {
if (!isActive) {
this
} else {
when (loading) {
is Resource.Loading -> copy(loading = loadingState)
else -> this
@ -376,3 +396,4 @@ class PlayerGeneratorViewModel : ViewModel() {
}
}
}
}