fix(player): clamp seeks inside media to stop end-of-stream pause loop (DR-095)

Seeking near the end of a transcoded video locked the player into a
stall/pause loop: unpausing or skipping bounced straight back to paused.

Both seek paths clamped the target to exactly `duration`. hls.js then
requested the segment whose start time lies *past* the end of the media
(a 6330.324s item asks for segment 1055, starting at 6336.33s). Jellyfin
never produces that segment, the fetch times out, and the gap-controller
stalls forever at the last buffered position — retrying ~1x/second and
firing an endless stream of AbortErrors as play() lands mid-nudge.

Clamp strictly inside the media instead, keeping one segment length
(6s) of margin, floored at 0 so short media still seeks to the start.
The seek-bar drag path needed this too: its range input `max` is the
duration itself, so dragging fully right produced the same dead target.

Also bumps the requirement-count fixture for the new DR-095 row.
This commit is contained in:
2026-07-30 12:52:03 +02:00
parent 984e594006
commit 98a6bca645
5 changed files with 87 additions and 8 deletions
+5 -1
View File
@@ -24,6 +24,7 @@
createTapGestureState,
registerTap,
resolveSeekTarget,
clampSeekTarget,
SEEK_FORWARD_SECONDS,
SEEK_BACKWARD_SECONDS,
type TapFeedback,
@@ -1147,7 +1148,10 @@
async function handleSeekBarChange(e: Event) {
const input = e.target as HTMLInputElement;
const targetTime = parseFloat(input.value);
// Clamp strictly inside the media: the range input's max IS the duration, so
// dragging fully right would otherwise request a segment past the media end,
// which the server never produces (see END_SEEK_MARGIN_SECONDS).
const targetTime = clampSeekTarget(parseFloat(input.value), duration);
// Set isSeeking immediately to prevent timeupdate from interfering
isSeeking = true;