Skip to content

Improve live-stream functionality and UX#3275

Open
Titaniumtown wants to merge 7 commits intofuto-org:masterfrom
Titaniumtown:pr/improve-live-functionality
Open

Improve live-stream functionality and UX#3275
Titaniumtown wants to merge 7 commits intofuto-org:masterfrom
Titaniumtown:pr/improve-live-functionality

Conversation

@Titaniumtown
Copy link
Copy Markdown

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:

  1. If someone was behind in the stream, the timestamps displayed would place the minus in the wrong spot (ex. 00:-40).
  2. Fixes pre-existing issue handling a BehindLiveWindowException error thrown from a segment no longer existing (i.e the stream has moved on past that point)
  3. Add various helper functions and tools for dealing with the state of a live stream
  4. Auto-recover stream on network weirdness or stream lag.
  5. Fallback to stream recovery that allows you to click the play symbol to retrigger the stream. Before this, I had to close and re-open the stream completely.
  6. Adds a Red/Gray UI Pill that shows if you are behind or not on the stream. Clicking the element brings you to back to the live state of the broadcast.
  7. Show a more intuitive measurement of your offset from the current livestream broadcast.

For transparency's sake, I used Claude Opus 4.7. But what you are reading now has been written by a human :)

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants