/** * Pure helpers for keeping the player's position variable honest. * * TRACES: UR-004, UR-041 | DR-265 | UT-245 * * `VideoPlayer.svelte` tracks the absolute playback position in its own * `currentTime` variable rather than reading `videoElement.currentTime` at the * point of use — transcoded HLS resets the element to 0 on every segment * rebuild, so only the component's running total is meaningful. Everything * downstream reads that variable: the seek bar, the progress reports, the * position mirrored into Rust, and the background-audio handoff. * * Which makes "who is allowed to write it" a correctness question, not a * rendering detail — hence a pure module with tests rather than a condition * buried in an event handler. */ export interface TimeUpdateGate { /** * Deliberately does NOT gate the update, and is accepted only to say so. * * `timeupdate` was written as a fallback "for when RAF isn't running" and so * excluded itself whenever `isPlaying` was true. But RAF is driven by the * document being rendered, and an Android activity behind a picture-in-picture * window is paused: the loop stops while the element plays on, and the one * remaining position source had switched itself off. Both writing the same * derived value costs nothing — the element is the authority either way. */ isPlaying?: boolean; isSeeking: boolean; isDraggingSeekBar: boolean; readyState: number; } /** * Whether a `timeupdate` event may write the component's position. * * Kept free of Svelte/DOM so the rule is unit-testable without mounting the * player. */ export function shouldApplyTimeUpdate(opts: TimeUpdateGate): boolean { // An in-flight seek or a drag owns the position until it settles, and an // element with no current data reads 0, which would rewind it. return !opts.isSeeking && !opts.isDraggingSeekBar && opts.readyState >= 2; }