mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Auto-reload live streams on transient errors
Adds a silent retry path for live HLS playback when the player raises a
transient I/O or parsing error: tryLiveAutoReload() reloads the source
and snaps to the live edge, with a 3s debounce and a 5-attempt cap so a
permanently broken source does not spin in a loop.
Triggered from:
- onPlayerError, for the error codes a transient outage typically
produces (IO_NETWORK_*, IO_BAD_HTTP_STATUS, IO_INVALID_HTTP_*,
IO_UNSPECIFIED, PARSING_CONTAINER_MALFORMED, PARSING_MANIFEST_MALFORMED)
- onPlaybackStateChanged, when STATE_ENDED hits on a live session
(covers cases where the player cannot raise an error before the
timeline empties)
On STATE_READY the attempts counter resets so future hiccups get a fresh
budget. The counters also reset on swapSourceInternal/clear so a previous
session's attempts cannot bleed into the next.
The dispatch is gated on the sticky _isLiveSession flag rather than the
dynamic isCurrentMediaItemLive: the latter flips false during the
transient empty timeline that a reload produces, which would otherwise
break the retry chain after the first attempt.
This commit is contained in:
@@ -209,6 +209,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
*/
|
||||
private var _isLiveSession: Boolean = false
|
||||
|
||||
/** Timestamp (ms) of last live auto-reload, used to back off duplicate reloads. */
|
||||
private var _lastLiveReloadAt_ms: Long = 0
|
||||
/** Consecutive live auto-reload attempts; resets on successful playback. */
|
||||
private var _liveReloadAttempts: Int = 0
|
||||
|
||||
private val _playerEventListener = object: Player.Listener {
|
||||
override fun onPlaybackSuppressionReasonChanged(playbackSuppressionReason: Int) {
|
||||
super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason)
|
||||
@@ -409,6 +414,40 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
player.seekToDefaultPosition()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* attempts at [LIVE_RELOAD_MAX_ATTEMPTS] before giving up so we don't spin on a
|
||||
* permanently-broken source.
|
||||
*
|
||||
* Returns true if a reload was actually issued.
|
||||
*/
|
||||
private fun tryLiveAutoReload(reason: String): Boolean {
|
||||
if (!_isLiveSession) return false
|
||||
val now = System.currentTimeMillis()
|
||||
val sinceLast = now - _lastLiveReloadAt_ms
|
||||
if (sinceLast < LIVE_RELOAD_MIN_INTERVAL_MS) {
|
||||
Logger.i(TAG, "tryLiveAutoReload skipped ($reason): last=${sinceLast}ms ago")
|
||||
return false
|
||||
}
|
||||
if (_liveReloadAttempts >= LIVE_RELOAD_MAX_ATTEMPTS) {
|
||||
Logger.w(TAG, "tryLiveAutoReload giving up ($reason): attempts=$_liveReloadAttempts")
|
||||
return false
|
||||
}
|
||||
_lastLiveReloadAt_ms = now
|
||||
_liveReloadAttempts += 1
|
||||
Logger.i(TAG, "tryLiveAutoReload ($reason): attempt=$_liveReloadAttempts")
|
||||
if (_mediaSource != null) {
|
||||
reloadMediaSource(play = true, resume = false)
|
||||
exoPlayer?.player?.seekToDefaultPosition()
|
||||
} else {
|
||||
exoPlayer?.player?.prepare()
|
||||
exoPlayer?.player?.playWhenReady = true
|
||||
exoPlayer?.player?.seekToDefaultPosition()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun changePlayer(newPlayer: PlayerManager?) {
|
||||
exoPlayer?.modifyState(exoPlayerStateName, {state -> state.listener = null});
|
||||
newPlayer?.modifyState(exoPlayerStateName, {state -> state.listener = _playerEventListener});
|
||||
@@ -574,6 +613,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
// swaps within an existing session must NOT reset live-state, so the audio/subtitle
|
||||
// overloads deliberately do not duplicate this block.
|
||||
_isLiveSession = false
|
||||
_liveReloadAttempts = 0
|
||||
_lastLiveReloadAt_ms = 0
|
||||
setLoading(false)
|
||||
val swapId = _swapIdVideo.incrementAndGet()
|
||||
_lastGeneratedDash = null;
|
||||
@@ -1088,6 +1129,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
_lastSubtitleMediaSource = null;
|
||||
_mediaSource = null;
|
||||
_isLiveSession = false
|
||||
_liveReloadAttempts = 0
|
||||
_lastLiveReloadAt_ms = 0
|
||||
}
|
||||
|
||||
fun stop(){
|
||||
@@ -1131,6 +1174,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
// For live streams, transient HLS/IO errors usually mean a segment expired or
|
||||
// the manifest tracker fell behind. Re-prepare and snap to the live edge so the
|
||||
// user does not have to back out of the video to recover.
|
||||
if (_isLiveSession && isLiveAutoRecoverableError(error)) {
|
||||
if (tryLiveAutoReload("onPlayerError code=${error.errorCode}")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
|
||||
@@ -1149,7 +1201,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||
//PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND,
|
||||
//PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
|
||||
//PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
|
||||
//PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
|
||||
@@ -1164,6 +1215,23 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a player error is the kind we should silently auto-reload on for live streams.
|
||||
* Excludes hard failures (DRM, decoder, malformed) where reloading won't help.
|
||||
*/
|
||||
private fun isLiveAutoRecoverableError(error: PlaybackException): Boolean {
|
||||
return when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
|
||||
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
|
||||
PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
|
||||
}
|
||||
@@ -1180,6 +1248,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
Logger.i(TAG, "_shouldPlaybackRestartOnConnectivity=false");
|
||||
_shouldPlaybackRestartOnConnectivity = false;
|
||||
}
|
||||
if (playbackState == ExoPlayer.STATE_READY) {
|
||||
// Successful prepare cycle; reset the attempts counter so future hiccups get a
|
||||
// fresh budget. The debounce timestamp does not need clearing -- a new error after
|
||||
// any non-trivial play interval will be well past LIVE_RELOAD_MIN_INTERVAL_MS.
|
||||
_liveReloadAttempts = 0
|
||||
}
|
||||
if (playbackState == ExoPlayer.STATE_ENDED && _isLiveSession) {
|
||||
// A live stream that 'ends' from the player's perspective is almost always a window
|
||||
// slip or stalled tracker. Try to silently rejoin at the live edge.
|
||||
tryLiveAutoReload("STATE_ENDED on live")
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun setLoading(isLoading: Boolean) { }
|
||||
@@ -1208,6 +1287,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
* generous enough to cover typical HLS/DASH live latencies (15-30s).
|
||||
*/
|
||||
const val LIVE_EDGE_FALLBACK_THRESHOLD_MS = 45_000L
|
||||
/** Min interval between live auto-reload attempts (debounce). */
|
||||
const val LIVE_RELOAD_MIN_INTERVAL_MS = 3_000L
|
||||
/** Max consecutive auto-reloads before giving up to avoid loops on broken sources. */
|
||||
const val LIVE_RELOAD_MAX_ATTEMPTS = 5
|
||||
|
||||
val SUPPORTED_SUBTITLES = hashSetOf("text/vtt", "application/x-subrip");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user