import { describe, it, expect } from "vitest"; import { fatalNetworkErrorAction } from "./hlsRecovery"; /** * A fatal hls.js network error mid-film must be retried, not reported as the * end of the stream — reporting "ended" hands control to autoplay and skips to * the next item while the user is still watching this one. * * The position the player displays is *already absolute*: the RAF loop sets * `currentTime = seekOffset + element.currentTime`. Anything that adds the * offset a second time doubles the apparent position, and after a quality * switch or a transcoded seek the offset is the whole resume position — so past * roughly the halfway mark the doubled value clears the near-end threshold and * every transient error is misread as the end. * * TRACES: UR-004, UR-074 | DR-177 | UT-174 */ describe("fatalNetworkErrorAction", () => { it("retries a mid-film failure after a quality switch instead of ending playback", () => { // 90-minute film, quality switched at the 50-minute mark: the reloaded // stream's timeline starts at 0, so seekOffset carries the 50 minutes and // the displayed position — already absolute — is 3000s of 5400s, 56% // through and nowhere near the end. const action = fatalNetworkErrorAction({ positionSeconds: 3000, knownDurationSeconds: 5400, attempts: 1, }); expect(action).toBe("retry"); }); it("treats a failure in the last tenth of the stream as the end", () => { // Jellyfin's transcoded HLS does not always emit #EXT-X-ENDLIST, so a // genuine end-of-stream arrives as a fatal network error. const action = fatalNetworkErrorAction({ positionSeconds: 5300, knownDurationSeconds: 5400, attempts: 1, }); expect(action).toBe("ended"); }); it("stops retrying once the recovery budget is spent", () => { const action = fatalNetworkErrorAction({ positionSeconds: 60, knownDurationSeconds: 5400, attempts: 4, }); expect(action).toBe("giveUp"); }); it("retries when the runtime is not known yet", () => { const action = fatalNetworkErrorAction({ positionSeconds: 120, knownDurationSeconds: 0, attempts: 1, }); expect(action).toBe("retry"); }); });