Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 60 additions & 18 deletions app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ fun PlayerScreen(
var currentPosition by remember { mutableLongStateOf(0L) }
var duration by remember { mutableLongStateOf(0L) }
var progress by remember { mutableFloatStateOf(0f) }
var currentPlaybackState by remember { mutableIntStateOf(Player.STATE_IDLE) }
var nextEpisodeTransitionInProgress by remember { mutableStateOf(false) }

// Skip overlay state - shows +10/-10 without showing full controls
var skipAmount by remember { mutableIntStateOf(0) }
Expand Down Expand Up @@ -437,10 +439,47 @@ fun PlayerScreen(
}
var nextEpisodePromptButton by remember { mutableIntStateOf(0) } // 0 = next, 1 = cancel
val nextEpisodePromptGate = remember { NextEpisodePromptGate() }
val playNextEpisode: (EpisodeIdentity, String?, String?, String?) -> Unit =
{ nextIdentity, nextAddonId, nextSourceName, nextBingeGroup ->
if (!nextEpisodeTransitionInProgress) {
nextEpisodeTransitionInProgress = true

val positionSnapshot = currentPosition
val durationSnapshot = duration
val playbackStateSnapshot = currentPlaybackState
val progressPercentSnapshot = if (durationSnapshot > 0L) {
((positionSnapshot.toDouble() / durationSnapshot.toDouble()) * 100.0)
.toInt()
.coerceIn(0, 100)
} else {
0
}

coroutineScope.launch {
runCatching {
viewModel.saveProgressAndWait(
position = positionSnapshot,
duration = durationSnapshot,
progressPercent = progressPercentSnapshot,
isPlaying = false,
playbackState = playbackStateSnapshot
)
}

onPlayNext(
nextIdentity,
nextAddonId,
nextSourceName,
nextBingeGroup
)
}
}
}

val playPendingNextEpisode: () -> Unit = playNext@{
showNextEpisodePrompt = false
val identity = pendingNextIdentity ?: return@playNext
onPlayNext(
playNextEpisode(
identity,
pendingNextAddonId,
pendingNextSourceName,
Expand Down Expand Up @@ -1027,6 +1066,7 @@ fun PlayerScreen(
// Add error listener to try next stream on codec errors
addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
currentPlaybackState = playbackState
val stateStr = when (playbackState) {
Player.STATE_IDLE -> "IDLE"
Player.STATE_BUFFERING -> "BUFFERING"
Expand Down Expand Up @@ -2450,22 +2490,24 @@ fun PlayerScreen(
controlsSeekJob?.cancel()
playerReleasedAtomic.set(true)
playerReleased = true
runCatching {
val safeDuration = exoPlayer.duration.takeIf { it > 0L && it != C.TIME_UNSET } ?: 0L
val safeProgressPercent = if (safeDuration > 0L) {
((exoPlayer.currentPosition.toDouble() / safeDuration.toDouble()) * 100.0)
.toInt()
.coerceIn(0, 100)
} else {
0
if (!nextEpisodeTransitionInProgress) {
runCatching {
val safeDuration = exoPlayer.duration.takeIf { it > 0L && it != C.TIME_UNSET } ?: 0L
val safeProgressPercent = if (safeDuration > 0L) {
((exoPlayer.currentPosition.toDouble() / safeDuration.toDouble()) * 100.0)
.toInt()
.coerceIn(0, 100)
} else {
0
}
viewModel.saveProgress(
exoPlayer.currentPosition,
safeDuration,
safeProgressPercent,
isPlaying = exoPlayer.isPlaying,
playbackState = exoPlayer.playbackState
)
}
viewModel.saveProgress(
exoPlayer.currentPosition,
safeDuration,
safeProgressPercent,
isPlaying = exoPlayer.isPlaying,
playbackState = exoPlayer.playbackState
)
}
runCatching { exoPlayer.release() }
// Restore the system stream volume if the player left it at zero.
Expand Down Expand Up @@ -2690,7 +2732,7 @@ fun PlayerScreen(
if (mediaType == MediaType.TV && nextEpisodeIdentity != null) {
val selected = uiState.selectedStream
val next = nextEpisodeIdentity ?: return@onKeyEvent true
onPlayNext(
playNextEpisode(
next,
selected?.addonId?.takeIf { it.isNotBlank() },
selected?.source?.takeIf { it.isNotBlank() },
Expand Down Expand Up @@ -3690,7 +3732,7 @@ fun PlayerScreen(
onClick = {
val next = nextEpisodeIdentity ?: return@PlayerIconButton
val selected = uiState.selectedStream
onPlayNext(
playNextEpisode(
next,
selected?.addonId?.takeIf { it.isNotBlank() },
selected?.source?.takeIf { it.isNotBlank() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3946,17 +3946,24 @@ class PlayerViewModel @Inject constructor(
}
}

fun saveProgress(position: Long, duration: Long, progressPercent: Int, isPlaying: Boolean, playbackState: Int) {
if (duration <= 0) return

// On pause/stop, always save (cancel any in-flight periodic save).
// During playback, skip if a previous save is still running (debounce).
fun saveProgress(
position: Long,
duration: Long,
progressPercent: Int,
isPlaying: Boolean,
playbackState: Int
): Job? {
if (duration <= 0) return null

// On pause/stop, replace an in-flight periodic save. During normal playback,
// debounce by returning the save that is already running.
if (!isPlaying || playbackState == Player.STATE_ENDED) {
progressSaveJob?.cancel()
} else if (progressSaveJob?.isActive == true) {
return
return progressSaveJob
}
progressSaveJob = viewModelScope.launch(Dispatchers.IO) {

val job = viewModelScope.launch(Dispatchers.IO) {
val currentTime = System.currentTimeMillis()
val progressFraction = (progressPercent / 100f).coerceIn(0f, 1f)
val selectedStream = _uiState.value.selectedStream
Expand Down Expand Up @@ -4202,9 +4209,37 @@ class PlayerViewModel @Inject constructor(
}

lastIsPlaying = isPlaying
}.also { job ->
job.invokeOnCompletion { progressSaveJob = null }
}

progressSaveJob = job
job.invokeOnCompletion {
if (progressSaveJob === job) {
progressSaveJob = null
}
}
return job
}

suspend fun saveProgressAndWait(
position: Long,
duration: Long,
progressPercent: Int,
isPlaying: Boolean,
playbackState: Int
) {
// Let an in-flight save finish before starting the final transition save.
// This avoids cancelling after hasMarkedWatched was set but before the
// corresponding remote/history writes completed.
progressSaveJob?.takeIf { it.isActive }?.join()

val finalJob = saveProgress(
position = position,
duration = duration,
progressPercent = progressPercent,
isPlaying = isPlaying,
playbackState = playbackState
)
finalJob?.join()
}

private var progressSaveJob: Job? = null
Expand Down
Loading