fix(player): a session superseded before it played is reclaimed, not "couldn't start"

Two session requests for one item in flight at once (a quick double seek; the
StrictMode replay of the start effect) let the server's last spawn be the older
request's. The newer request's answer then named a session already gone; its
playlist 404'd before a manifest parsed, and the player stopped with "Playback
couldn't start". The 404 reclaim now covers a session that never played too,
bounded by the same budget, and keeps the start's own autoplay.
This commit is contained in:
2026-09-27 20:22:43 +02:00
parent a9ac303d3e
commit 2abd4ffcb6
2 changed files with 46 additions and 14 deletions
@@ -248,12 +248,35 @@ describe("an HLS session the server no longer has", () => {
}
});
it("is a failure, not a restart, when the session never played", async () => {
// Otherwise a session that 404s from the start would restart itself in a loop.
it("is reclaimed at the asked position, and still starts, when it was lost before it played", async () => {
// Two session requests for one item in flight at once (a seek twice in quick succession; the
// dev StrictMode replay of the start effect): the server's LAST spawn wins, and it may be the
// older request's. The newer request's answer then names a session that is already gone, and
// its playlist 404s before a single manifest parsed — which ended playback with "couldn't
// start" (Arrow S03E14 on localdev, 2026-09-27). The video is still paused then, so the
// reclaim must keep the start's own "play once ready", not read it off the paused element.
const play = vi.spyOn(HTMLMediaElement.prototype, "play");
const session = mount();
await waitFor(() => expect(hlsInstances).toHaveLength(1));
act(() => hlsInstances[0]!.emit(Hls.Events.ERROR, lost404));
act(() =>
hlsInstances[0]!.emit(Hls.Events.ERROR, { ...lost404, details: "manifestLoadError" }),
);
await waitFor(() => expect(hlsInstances).toHaveLength(2));
expect(session).toHaveBeenCalledTimes(2);
expect(session.mock.calls[1]![1]).toBe(600);
act(() => hlsInstances[1]!.emit(Hls.Events.MANIFEST_PARSED));
expect(play).toHaveBeenCalled();
expect(screen.queryByText("plex.player.errGeneric")).toBeNull();
});
it("gives up and says so when a session that never played keeps 404ing", async () => {
// The reclaim budget is what bounds a server that 404s every session from the start.
const session = mount();
for (let i = 0; i < 3; i++) {
await waitFor(() => expect(hlsInstances).toHaveLength(i + 1));
act(() => hlsInstances[i]!.emit(Hls.Events.ERROR, lost404));
}
await screen.findByText("plex.player.errGeneric");
expect(session).toHaveBeenCalledTimes(1);
expect(session).toHaveBeenCalledTimes(3);
});
});