mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Recover stuck live playback when play is pressed
Adds recoverFromStuck() and rewires the play-button click handler to call it before falling through to the normal play path. When the player has dropped into STATE_IDLE after a fatal error or STATE_ENDED on a slipped live window, plain play() is a no-op until we re-prepare. recoverFromStuck() reloads the cached media source and, on live, snaps to the live edge -- so pressing play recovers in place instead of forcing the user to back out of the video and reopen it. Non-live STATE_ENDED is left alone so the existing seek-to-0 replay path that VideoDetailView's onSourceEnded handler primes via setIsReplay(true) keeps working. The play handler also tightens the existing replay rewind: the previous 'contentPosition >= duration' check would seekTo(0) on live streams because Player.duration is C.TIME_UNSET (a large negative value); the new 'dur > 0 && pos >= dur' guard skips that for live and only rewinds finished VODs.
This commit is contained in:
@@ -225,24 +225,26 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_buttonNext.setOnClickListener { onNext.emit() };
|
||||
_buttonPrevious_fullscreen.setOnClickListener { onPrevious.emit() };
|
||||
_buttonNext_fullscreen.setOnClickListener { onNext.emit() };
|
||||
_control_play.setOnClickListener {
|
||||
exoPlayer?.player?.let {
|
||||
if (it.contentPosition >= it.duration) {
|
||||
it.seekTo(0)
|
||||
val playClickHandler = View.OnClickListener {
|
||||
// Order matters:
|
||||
// 1. If the player is stuck (STATE_IDLE after error, STATE_ENDED on a slipped live
|
||||
// window) plain play() is a no-op until we re-prepare. Recover first.
|
||||
// 2. Otherwise, if a VOD has played to its end, rewind to start (replay).
|
||||
// 3. Then start playback.
|
||||
val recovered = recoverFromStuck()
|
||||
if (!recovered) {
|
||||
exoPlayer?.player?.let {
|
||||
val dur = it.duration
|
||||
if (dur > 0 && it.contentPosition >= dur) {
|
||||
it.seekTo(0)
|
||||
}
|
||||
it.play()
|
||||
}
|
||||
exoPlayer?.player?.play();
|
||||
}
|
||||
updatePlayPause();
|
||||
};
|
||||
_control_play_fullscreen.setOnClickListener {
|
||||
exoPlayer?.player?.let {
|
||||
if (it.contentPosition >= it.duration) {
|
||||
it.seekTo(0)
|
||||
}
|
||||
exoPlayer?.player?.play();
|
||||
}
|
||||
updatePlayPause();
|
||||
};
|
||||
updatePlayPause()
|
||||
}
|
||||
_control_play.setOnClickListener(playClickHandler)
|
||||
_control_play_fullscreen.setOnClickListener(playClickHandler)
|
||||
_control_pause.setOnClickListener {
|
||||
exoPlayer?.player?.pause();
|
||||
updatePlayPause();
|
||||
|
||||
@@ -414,6 +414,46 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
player.seekToDefaultPosition()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovers playback when the player is stuck in a non-recoverable state.
|
||||
* Returns true if recovery was attempted (caller should not also call play()).
|
||||
*
|
||||
* STATE_IDLE happens after an unrecoverable error; STATE_ENDED happens on live when the
|
||||
* window slips past the player. For both, plain play() is a no-op until we re-prepare; for
|
||||
* live we additionally seek to the live edge so the user lands where YouTube's UI would.
|
||||
*
|
||||
* Non-live STATE_ENDED is *not* stuck -- it's the user pressing replay on a finished VOD --
|
||||
* so we deliberately fall through to the caller, which seeks to 0 and plays. Without this
|
||||
* guard the play button would re-prepare and seek to the saved end position, immediately
|
||||
* re-entering STATE_ENDED, and the replay icon set by [setIsReplay] would never replay.
|
||||
* We key off the sticky [_isLiveSession] rather than [Player.isCurrentMediaItemLive] because
|
||||
* the latter can flip false during a live-window slip even though the user *is* watching live.
|
||||
*/
|
||||
fun recoverFromStuck(): Boolean {
|
||||
val player = exoPlayer?.player ?: return false
|
||||
val state = player.playbackState
|
||||
if (state != Player.STATE_IDLE && state != Player.STATE_ENDED) return false
|
||||
if (state == Player.STATE_ENDED && !_isLiveSession) return false
|
||||
Logger.i(TAG, "recoverFromStuck state=$state isLive=${player.isCurrentMediaItemLive}")
|
||||
// Reload the current source if available; preserves position via reloadMediaSource(resume=true)
|
||||
// but for live we want to land at the edge.
|
||||
if (_mediaSource != null) {
|
||||
val wasLive = player.isCurrentMediaItemLive
|
||||
reloadMediaSource(play = true, resume = !wasLive)
|
||||
if (wasLive) {
|
||||
exoPlayer?.player?.seekToDefaultPosition()
|
||||
}
|
||||
} else {
|
||||
// No media source cached yet; just re-prepare what's loaded.
|
||||
player.prepare()
|
||||
player.playWhenReady = true
|
||||
if (player.isCurrentMediaItemLive) {
|
||||
player.seekToDefaultPosition()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort auto-reload for a live stream after a transient HLS/IO error.
|
||||
* Debounces consecutive reloads (min [LIVE_RELOAD_MIN_INTERVAL_MS] apart) and caps
|
||||
|
||||
Reference in New Issue
Block a user