Show offset behind live edge as -MM:SS in time display

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 commit is contained in:
Simon Gardling
2026-05-01 00:37:58 -04:00
parent 985bd433a2
commit 845c6b0031
2 changed files with 23 additions and 1 deletions
@@ -478,7 +478,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
updateAutoplayButton() updateAutoplayButton()
val progressUpdateListener = { position: Long, bufferedPosition: Long -> val progressUpdateListener = { position: Long, bufferedPosition: Long ->
val currentTime = position.formatDuration() // For live streams that have been seeked behind, replace the running position with
// a -MM:SS "behind live" indicator (the videojs/HLS convention). At the live edge
// we keep showing the running position; this matches YouTube's web behaviour where
// the LIVE pill alone (red "caught up" / gray "behind") + a clear offset readout
// tell the whole story.
val behindMs = if (isLive) behindLiveMs else null
val currentTime = if (behindMs != null && behindMs > 0) {
"-" + behindMs.formatDuration()
} else {
position.formatDuration()
}
val currentDuration = duration.formatDuration() val currentDuration = duration.formatDuration()
_control_time.text = currentTime; _control_time.text = currentTime;
_control_time_fullscreen.text = currentTime; _control_time_fullscreen.text = currentTime;
@@ -176,6 +176,18 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
} }
/**
* How far the player is behind the live edge from a user perspective, in ms. Subtracts the
* manifest's natural live offset (or the [LIVE_EDGE_FALLBACK_THRESHOLD_MS] when unknown) so
* the value reflects the user-perceptible delay rather than the inherent HLS/DASH latency.
* Returns null when not live or the offset is unknown; returns 0 when at the live edge.
*/
val behindLiveMs: Long? get() {
val offset = liveOffsetMs ?: return null
val baseline = targetLiveOffsetMs ?: LIVE_EDGE_FALLBACK_THRESHOLD_MS
return (offset - baseline).coerceAtLeast(0)
}
var isAudioMode: Boolean = false var isAudioMode: Boolean = false
private set; private set;