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:
@@ -246,8 +246,9 @@ Internal architecture, components, and application logic.
|
||||
| DR-089 | Continue Watching suppresses resume entries superseded by Next Up: an in-progress episode whose series has a next-up entry strictly later in series order (season, then episode) is dropped from the Home and TV rows; movies, series without a next-up entry, and items with unknown/mixed episode ordering are always kept | UI | UR-059 | Done |
|
||||
| DR-090 | Relevance ranking in Rust (`domain/search_rank.rs`): results sort by match position (prefix → word-start → mid-word substring → no name match) then by media kind (containers before their contents), stably so the backend's own relevance breaks ties. Applied in `repository_search` to both the instant cache result and the merged cache+server union, so the list does not reshuffle when server results land | Backend | UR-060 | Done |
|
||||
| DR-091 | Search result groups split TV into separate Shows and Episodes groups and add a People group (default order: Shows → Episodes → Movies → Songs → Albums → Artists → People); a stored `tvShows` order from before the split expands in place to shows+episodes so an upgrading user keeps their arrangement | UI | UR-060 | Done |
|
||||
| DR-092 | Video tap gestures resolve in `tapGestures.ts` (pure, unit-tested) rather than inline in `VideoPlayer.svelte`: `registerTap` returns `pending` for a first tap — the component defers `togglePlayPause` behind a `DOUBLE_TAP_WINDOW_MS` (300 ms) timer that a second tap cancels — or `seek` (+30 s right / −10 s left) for a second tap inside the window; a consumed second tap resets the state so a third tap starts fresh, and a swipe cancels the pending tap. The compatibility `click` the browser synthesizes after a touch tap is filtered in `handleVideoClick` so it cannot bypass the deferral. `resolveSeekTarget` converts the delta to the absolute position the facade requires, clamped to `[0, duration]` and chained off a still-in-flight `pendingSeekTarget` so back-to-back skips accumulate instead of all resolving against a not-yet-updated position | UI | UR-061 | Done |
|
||||
| DR-092 | Video tap gestures resolve in `tapGestures.ts` (pure, unit-tested) rather than inline in `VideoPlayer.svelte`: `registerTap` returns `pending` for a first tap — the component defers `togglePlayPause` behind a `DOUBLE_TAP_WINDOW_MS` (300 ms) timer that a second tap cancels — or `seek` (+30 s right / −10 s left) for a second tap inside the window; a consumed second tap resets the state so a third tap starts fresh, and a swipe cancels the pending tap. The compatibility `click` the browser synthesizes after a touch tap is filtered in `handleVideoClick` so it cannot bypass the deferral. `resolveSeekTarget` converts the delta to the absolute position the facade requires, clamped per DR-095 and chained off a still-in-flight `pendingSeekTarget` so back-to-back skips accumulate instead of all resolving against a not-yet-updated position | UI | UR-061 | Done |
|
||||
| DR-094 | Frontend boundary tripwire (`scripts/check-frontend-boundary.sh`) detects Jellyfin item-type array literals **anywhere** in `src/` rather than only inline at an `includeItemTypes:` query site, so a category→type mapping cannot evade the check by being assigned to a named const (the evasion that let the `scoped-search` leak pass CI); requires two adjacent type literals so single-type presentation and `item.type ===` inspection stay legal, and caps the allowlist to force taxonomy into Rust instead of accumulating exceptions | Tooling | - | Done |
|
||||
| DR-095 | Seek targets clamp strictly *inside* the media (`clampSeekTarget`, `END_SEEK_MARGIN_SECONDS` = 6 s ≈ one HLS segment) instead of to the exact `duration`. Landing on the duration makes hls.js request the segment whose start time lies past the end of the media (e.g. a 6330.324 s item → segment 1055 starting at 6336.33 s), which Jellyfin never produces; the fetch times out and hls.js' gap-controller stalls at the last buffered position, presenting as "unpausing or skipping bounces straight back to paused". Applied on both seek paths — the relative-skip `resolveSeekTarget` and the seek-bar drag, whose range input `max` is the duration itself — and floored at 0 so media shorter than the margin still seeks to the start | UI | UR-061 | Done |
|
||||
| DR-093 | Traceability coverage gate derives its requirement denominators from `requirements.md` at run time rather than hardcoded literals: `countDefinedRequirements` counts an ID only where it leads a markdown table row (ignoring the "Traces To" column and prose) and deduplicates IDs listed both in the definition tables and in the §3 traceability matrix; `computeCoverage` reports the *intersection* of traced and defined IDs so an ID traced in code but absent from `requirements.md` is surfaced as `orphaned` instead of inflating the ratio past 100%. UT/IT test identifiers are excluded as a separate taxonomy. CI and `bun run traces:coverage` share this computation and fail on both a sub-threshold and an impossible >100% result | Tooling | - | Done |
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user