diff --git a/docs/requirements.md b/docs/requirements.md index 849ca630..0acd5fa5 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -340,6 +340,15 @@ Internal architecture, components, and application logic. | DR-179 | Jellyfin is told what was played: progress while it plays, and a stop when it ends. A device trace of 35 minutes' playback requested `/Sessions/Playing/Progress` **zero** times and sent 14 `Stopped` reports, every one of them at position 0. Three faults, one subject. *Progress never left the device*: the frontend service writes it to the local DB by design, and nothing on the Rust side reported it for webview-rendered media — so the server learned a position only when the player was closed, and a crash or a swipe-away cost the session. It is now reported from the controller's own position ticks, through the 30s throttler it already owned and shares with the native audio path, which covers all three rendering paths in one place instead of adding a second frequent IPC caller. *Zero-position stops were sent*: Jellyfin stores the reported position as the resume point, so a zero does not merely fail to inform, it instructs the server to forget — and no zero was ever real, each one coming from asking a player that was not rendering the media (see DR-178). They are withheld; one landed 40s after the frontend had correctly reported 15:22 for the same episode, overwriting it. *A finished episode reported nothing at all*: Jellyfin decides "watched" from the stop report and its percentage, and in background audio-only mode nobody sends one — the webview is suspended and its element was torn down at the handoff, while the backend advances to the next episode without a word about the one that ended, so an episode listened to end-to-end on the lockscreen never counted as watched. `on_playback_ended` now reports it stopped at its **runtime** (not the last tick, which can be seconds short or, on a handoff whose ticks stopped early, nowhere near the end) before any advance, since after one the queue's current item is the next episode. Scoped to the audio-only handoff, the case the frontend provably cannot cover, so foreground playback keeps its single existing report; music ending natively remains unreported and wants its own change. The reporting seam is a `PlaybackReportSink` the controller sends to, which also collapses three copies of the spawn-a-task-and-hope block into one and is what let all of this be written as failing tests rather than found on a device a second time | Player | UR-025, UR-005, UR-040 | Done (pending device verification) | | DR-180 | A background-audio handoff of a **downloaded** episode starts where the video left off. The handoff prefers a local file over the audio-only stream (DR-128), but the two begin in different places and were treated alike: a stream is built with `StartTimeTicks`, so the server makes the handoff point that stream's zero and the base is the handoff position with no seek — while a file has no such parameter and begins at the episode's own zero, so basing it at the handoff position claimed minutes of audio that were about to play from the beginning. Backgrounding a downloaded episode therefore restarted it while the lockscreen scrubber, dutifully adding the base, showed the position it should have been at. `background_audio_plan` splits the two: a file gets no base and a real seek, a stream keeps the base and no seek (seeking one would skip *past* the content by the handoff position again). The same distinction settles an inbound seek — `seek_absolute` re-opens a *streamed* handoff at the requested position because a chunked length-less transcode cannot honour a seek, which is not true of local media, and `resume_stream_at` refuses a non-remote source outright, so routing a lockscreen scrub of a downloaded episode through it failed the seek rather than performing it | Player | UR-040, UR-071 | Done (pending device verification) | | DR-181 | A resumed transcode plays. Every video stream URL carried the resume position as `StartTimeTicks`, which is correct for a progressive response and fatal for an HLS one: Jellyfin builds each segment URI by echoing the **master playlist's** query string into it, and its segment handler opens by rejecting any request carrying `StartTimeTicks > 0` (`ArgumentException` → `400`). One position on the playlist therefore 400s every `hls1/main/N.ts` behind it, so hls.js exhausted its retries and gave up — presenting as an episode that will not resume while the same episode from the beginning is fine, the `> 0` being exactly why the beginning survived. The parameter is also unnecessary there: a playlist spans the whole item and asking for segment N *is* the seek, which the server transcodes from. So it is removed from the URL builder entirely rather than conditionalised — the builder has one caller shape and no way to know whether the response will be segmented — and the position becomes what it always was for HLS, a seek issued once the player has loaded: the seek path reloads at zero and seeks the element, and the resume path lets the player seek itself. The progressive `/Audio/universal` builder used by the background-audio handoff is a different endpoint with no segments and keeps its `StartTimeTicks`, which is why an audio-only handoff resumes correctly and a video one did not | Playback | UR-004, UR-074 | Done | +| DR-182 | Native video shows a picture. The poster/title card is an opaque `bg-black` overlay drawn over the whole video area while `isMediaReady` is false, and **every** signal that clears it is emitted by the HTML5 `