Improve live-stream functionality and UX#3275
Open
Titaniumtown wants to merge 7 commits intofuto-org:masterfrom
Open
Improve live-stream functionality and UX#3275Titaniumtown wants to merge 7 commits intofuto-org:masterfrom
Titaniumtown wants to merge 7 commits intofuto-org:masterfrom
Conversation
formatDuration() previously emitted strings like '00:-49' for negative values because the modulo operations propagated the sign. This shows up on live streams that briefly report a negative position during reload, and as the basis for a planned 'behind live edge' indicator. Recurse on the absolute value when negative so we get a clean -MM:SS (or -HH:MM:SS) format.
The 'error is BehindLiveWindowException' check in onPlayerError was always false (Kotlin compiler had been warning about it). The actual exception is wrapped as the *cause* of an ExoPlaybackException, with errorCode 1002 (ERROR_CODE_BEHIND_LIVE_WINDOW). This made the existing recovery branch dead code, so when the live window slipped past the player it would silently drop into STATE_IDLE and the user had to back out of the video and reopen it. Test the cause and the error code so the branch actually fires, and snap to the live edge with seekToDefaultPosition() after both the BehindLiveWindow and PlaylistStuckException reloads so the user lands where the player would naturally play.
Foundational hooks for follow-up live-recovery and UI work; no behaviour
change on its own.
- isLive: whether the current media item is live
- liveOffsetMs: offset behind wall-clock live edge
- targetLiveOffsetMs: manifest's intended offset (null if not declared)
- isAtLiveEdge: target-aware boundary, with a 45s fallback for sources
(e.g. YouTube HLS) that do not declare targetOffsetMs
- seekToLiveEdge(): wraps Player.seekToDefaultPosition() for live items
- onLiveChanged event, fired from onTimelineChanged/onMediaItemTransition
- _isLiveSession sticky flag: stays true through the transient empty
timeline the player goes through during a reload, so consumers can
distinguish 'a live source is loaded' from the dynamic isLive bit
LIVE_EDGE_TOLERANCE_MS = 5s and LIVE_EDGE_FALLBACK_THRESHOLD_MS = 45s
are tuned to match what YouTube's HLS player reports (currentLiveOffset
sits at ~25-30s natively even at the edge, so a tighter threshold would
report 'behind' forever).
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.
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.
LIVE pill rendered next to the time display (regular + fullscreen layouts): - red filled with a white dot when at the live edge - gray bordered with a muted dot when behind - tap to jump to the live edge While live, the duration text + divider, loop button, and chapter view are hidden -- they're meaningless on a live stream and were just visual noise. Position text stays visible since for HLS DVR streams it shows offset within the available seek window. Pill state updates in the existing PlayerControlView progress tick; applyLiveUI() switches the surrounding UI on the onLiveChanged event. At construction the current isLive value is applied once so that attaching to an already-live media item starts in the right state. The chapter view's left-constraint now chains through live_pill_container, which collapses to width 0 when the pill is GONE on VOD -- so the chapter view sits in the same place it always did when live is off.
When a live stream has been seeked behind, replace the running position with a -MM:SS 'behind live' indicator (the videojs/HLS convention; matches the offset readout commonly seen in third-party YouTube live tooling). At the live edge we keep showing the running position. The offset uses [behindLiveMs], which subtracts the manifest's natural live offset (or LIVE_EDGE_FALLBACK_THRESHOLD_MS when the manifest doesn't declare one) so the value reflects the user-perceptible delay rather than the inherent ~25-30s HLS latency. The 'show as behind' boundary always agrees with the 'pill turns gray' boundary (both keyed on the same baseline), so the readout and the LIVE pill cannot disagree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR is a collection of various live-stream related fixes that I have been iterating on over the past day.
List of my changes:
00:-40).BehindLiveWindowExceptionerror thrown from a segment no longer existing (i.e the stream has moved on past that point)For transparency's sake, I used Claude Opus 4.7. But what you are reading now has been written by a human :)