Skip to content

Commit 03465a3

Browse files
authored
Merge pull request #150 from kalzEOS/fix/series-auto-next-state
[codex] Fix series auto-next playback state
2 parents 8aa41b9 + 19ecc33 commit 03465a3

3 files changed

Lines changed: 96 additions & 28 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import java.util.Properties
33
import org.gradle.api.provider.Property
44
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
55

6-
val appVersionCode = 143
7-
val appVersionName = "3.4.7"
6+
val appVersionCode = 144
7+
val appVersionName = "3.4.8"
88

99
plugins {
1010
alias(libs.plugins.android.application)

app/src/main/java/com/example/xtreamplayer/MainActivityUi.kt

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,16 +1125,28 @@ fun RootScreen(
11251125
}
11261126
}
11271127

1128+
fun syncPlaybackStateToQueueIndex(index: Int): Boolean {
1129+
val queue = activePlaybackQueue ?: return false
1130+
if (index !in queue.items.indices) return false
1131+
activePlaybackItems.getOrNull(index)?.let { activePlaybackItem = it }
1132+
activePlaybackTitle = queue.items[index].title
1133+
activePlaybackSubtitleState = null
1134+
resumePositionMs = null
1135+
return true
1136+
}
1137+
11281138
fun startPlayback(
11291139
item: ContentItem,
11301140
items: List<ContentItem>,
11311141
config: AuthConfig,
11321142
parentItem: ContentItem? = null,
11331143
playbackResumePositionMs: Long? = null
11341144
) {
1135-
val playableItems = items.filter(::isPlayableContent)
1145+
val playableItems = playableContentItemsForQueue(items, item)
1146+
val queue = buildPlaybackQueue(playableItems, item, config)
1147+
val startItem = playableItems.getOrNull(queue.startIndex) ?: item
11361148
activePlaybackItems = playableItems
1137-
activePlaybackItem = item
1149+
activePlaybackItem = startItem
11381150
activePlaybackSeriesParent =
11391151
if (item.contentType == ContentType.SERIES) {
11401152
parentItem
@@ -1149,9 +1161,8 @@ fun RootScreen(
11491161
BufferProfile.VOD
11501162
}
11511163
playbackEngine.setBufferProfile(profile)
1152-
val queue = buildPlaybackQueue(items, item, config)
11531164
activePlaybackQueue = queue
1154-
activePlaybackTitle = queue.items.getOrNull(queue.startIndex)?.title ?: item.title
1165+
activePlaybackTitle = queue.items.getOrNull(queue.startIndex)?.title ?: startItem.title
11551166
}
11561167

11571168
val handlePlayItem: (ContentItem, List<ContentItem>) -> Unit = { item, items ->
@@ -1160,12 +1171,7 @@ fun RootScreen(
11601171
resumeFocusId = resolveResumeFocusTarget(item)
11611172
val shouldResolveSeriesQueue =
11621173
item.contentType == ContentType.SERIES &&
1163-
!item.containerExtension.isNullOrBlank() &&
1164-
(items.size <= 1 ||
1165-
items.any {
1166-
it.contentType != ContentType.SERIES ||
1167-
it.containerExtension.isNullOrBlank()
1168-
})
1174+
!item.containerExtension.isNullOrBlank()
11691175
if (shouldResolveSeriesQueue) {
11701176
coroutineScope.launch {
11711177
val resolved =
@@ -1274,12 +1280,7 @@ fun RootScreen(
12741280
resumeFocusId = resolveResumeFocusTarget(item)
12751281
val shouldResolveSeriesQueue =
12761282
item.contentType == ContentType.SERIES &&
1277-
!item.containerExtension.isNullOrBlank() &&
1278-
(items.size <= 1 ||
1279-
items.any {
1280-
it.contentType != ContentType.SERIES ||
1281-
it.containerExtension.isNullOrBlank()
1282-
})
1283+
!item.containerExtension.isNullOrBlank()
12831284
if (shouldResolveSeriesQueue) {
12841285
coroutineScope.launch {
12851286
val resolved =
@@ -1387,9 +1388,16 @@ fun RootScreen(
13871388
completionThresholdPercent = CONTINUE_WATCHING_MAX_PROGRESS_PERCENT
13881389
)
13891390
val subtitleState = activePlaybackSubtitleState
1391+
val hasFollowingSeriesEpisode =
1392+
item.contentType == ContentType.SERIES &&
1393+
activePlaybackQueue?.items
1394+
?.drop(safePlayer.currentMediaItemIndex + 1)
1395+
?.any { it.type == ContentType.SERIES } == true
13901396
coroutineScope.launch {
13911397
if (!shouldStore) {
1392-
continueWatchingRepository.removeEntry(config, item)
1398+
if (!hasFollowingSeriesEpisode) {
1399+
continueWatchingRepository.removeEntry(config, item)
1400+
}
13931401
vodPlaybackStateRepository.removeEntry(config, queueMediaIdFor(item))
13941402
} else {
13951403
val parentItem =
@@ -1619,6 +1627,14 @@ fun RootScreen(
16191627
syncPlaybackStateFromPlayer(mediaItem)
16201628
}
16211629

1630+
override fun onPositionDiscontinuity(
1631+
oldPosition: Player.PositionInfo,
1632+
newPosition: Player.PositionInfo,
1633+
reason: Int
1634+
) {
1635+
syncPlaybackStateFromPlayer(newPosition.mediaItem)
1636+
}
1637+
16221638
override fun onPlaybackStateChanged(playbackState: Int) {
16231639
syncPlaybackStateFromPlayer()
16241640
if (playbackState == Player.STATE_READY) {
@@ -2294,8 +2310,15 @@ fun RootScreen(
22942310
resumePositionMs = null
22952311
},
22962312
onPlayNextEpisode = {
2297-
playbackEngine.player.seekToNextMediaItem()
2298-
playbackEngine.player.playWhenReady = true
2313+
val player = playbackEngine.player
2314+
savePlaybackProgress()
2315+
val nextIndex = player.currentMediaItemIndex + 1
2316+
if (syncPlaybackStateToQueueIndex(nextIndex)) {
2317+
player.seekTo(nextIndex, 0L)
2318+
} else {
2319+
player.seekToNextMediaItem()
2320+
}
2321+
player.playWhenReady = true
22992322
},
23002323
onMatchFrameRateChange = { enabled ->
23012324
playbackEngine.applySettings(settings.copy(matchFrameRateEnabled = enabled))
@@ -2718,6 +2741,17 @@ private fun isPlayableContent(item: ContentItem): Boolean {
27182741
return item.contentType != ContentType.SERIES || !item.containerExtension.isNullOrBlank()
27192742
}
27202743

2744+
private fun playableContentItemsForQueue(
2745+
items: List<ContentItem>,
2746+
current: ContentItem
2747+
): List<ContentItem> {
2748+
val playableItems = items.filter(::isPlayableContent).toMutableList()
2749+
if (playableItems.none { isSameContentIdentity(it, current) }) {
2750+
playableItems.add(current)
2751+
}
2752+
return playableItems
2753+
}
2754+
27212755
private fun isSameContentIdentity(first: ContentItem, second: ContentItem): Boolean {
27222756
if (first.contentType != second.contentType) return false
27232757
val firstStreamId = first.streamId?.takeUnless { it.isBlank() }
@@ -2739,10 +2773,7 @@ private fun buildPlaybackQueue(
27392773
current: ContentItem,
27402774
authConfig: AuthConfig
27412775
): PlaybackQueue {
2742-
val playableItems = items.filter(::isPlayableContent).toMutableList()
2743-
if (playableItems.none { isSameContentIdentity(it, current) }) {
2744-
playableItems.add(current)
2745-
}
2776+
val playableItems = playableContentItemsForQueue(items, current)
27462777
val startIndex =
27472778
playableItems.indexOfFirst { isSameContentIdentity(it, current) }.let { index ->
27482779
if (index >= 0) index else 0
@@ -8746,10 +8777,13 @@ fun ContinueWatchingScreen(
87468777
}
87478778
val columns = rememberReflowColumns(baseColumns, navLayoutExpanded)
87488779
val resolvedParents = remember { androidx.compose.runtime.mutableStateMapOf<String, ContentItem>() }
8780+
val parentResolutionAttempted =
8781+
remember { androidx.compose.runtime.mutableStateMapOf<String, Boolean>() }
87498782
val posterFontScale = remember(columns) { 4f / columns.toFloat() }
87508783
val resolvedParentsSnapshot = resolvedParents.toMap()
8784+
val parentResolutionAttemptedSnapshot = parentResolutionAttempted.toMap()
87518785
val displayEntries =
8752-
remember(continueWatchingItems, resolvedParentsSnapshot) {
8786+
remember(continueWatchingItems, resolvedParentsSnapshot, parentResolutionAttemptedSnapshot) {
87538787
fun displayGroupingKey(entry: ContinueWatchingEntry): String {
87548788
val canonicalSeriesId =
87558789
entry.parentItem?.streamId?.takeUnless { it.isBlank() }
@@ -8786,6 +8820,16 @@ fun ContinueWatchingScreen(
87868820
}
87878821
.maxByOrNull { it.first }
87888822
?.second
8823+
val waitingForParentResolution =
8824+
group.any { entry ->
8825+
entry.item.contentType == ContentType.SERIES &&
8826+
entry.parentItem == null &&
8827+
!resolvedParentsSnapshot.containsKey(entry.key) &&
8828+
parentResolutionAttemptedSnapshot[entry.key] != true
8829+
}
8830+
if (latestSeriesParent == null && waitingForParentResolution) {
8831+
return@mapNotNull null
8832+
}
87898833
val displayItem = latestSeriesParent ?: latest.item
87908834
val resumeLabel =
87918835
if (latest.item.contentType == ContentType.SERIES) {
@@ -8856,16 +8900,22 @@ fun ContinueWatchingScreen(
88568900
.filterNot { it in currentKeys }
88578901
.toList()
88588902
.forEach { resolvedParents.remove(it) }
8903+
parentResolutionAttempted.keys
8904+
.filterNot { it in currentKeys }
8905+
.toList()
8906+
.forEach { parentResolutionAttempted.remove(it) }
88598907
continueWatchingItems
88608908
.filter { entry ->
88618909
entry.item.contentType == ContentType.SERIES && entry.parentItem == null &&
8862-
!resolvedParents.containsKey(entry.key)
8910+
!resolvedParents.containsKey(entry.key) &&
8911+
parentResolutionAttempted[entry.key] != true
88638912
}
88648913
.forEach { entry ->
88658914
val resolved = resolveSeriesParent(entry.item)
88668915
if (resolved != null) {
88678916
resolvedParents[entry.key] = resolved
88688917
}
8918+
parentResolutionAttempted[entry.key] = true
88698919
}
88708920
}
88718921

app/src/main/java/com/example/xtreamplayer/PlayerScreen.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package com.example.xtreamplayer
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.DisposableEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableIntStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import androidx.media3.common.Player
410
import com.example.xtreamplayer.content.CategoryItem
511
import com.example.xtreamplayer.content.ContentItem
612
import com.example.xtreamplayer.content.ContentType
@@ -32,7 +38,19 @@ internal fun PlayerScreen(
3238
loadLiveCategoryThumbnail: suspend (CategoryItem) -> Result<String?>
3339
) {
3440
val queue = activePlaybackQueue ?: return
35-
val currentIndex = playbackEngine.player.currentMediaItemIndex
41+
val player = playbackEngine.player
42+
var currentIndex by remember(queue, player) { mutableIntStateOf(player.currentMediaItemIndex) }
43+
DisposableEffect(queue, player) {
44+
currentIndex = player.currentMediaItemIndex
45+
val listener =
46+
object : Player.Listener {
47+
override fun onEvents(player: Player, events: Player.Events) {
48+
currentIndex = player.currentMediaItemIndex
49+
}
50+
}
51+
player.addListener(listener)
52+
onDispose { player.removeListener(listener) }
53+
}
3654
val queueItems = queue.items
3755
val hasNextEpisode = currentIndex >= 0 && currentIndex < queueItems.size - 1
3856
val nextEpisodeTitle = queueItems.getOrNull(currentIndex + 1)?.title

0 commit comments

Comments
 (0)