fix(plex): subtitles — a retried full pass resumes, a stalled track is polled slowly
A full pass that ran out of time kept what it reached, but its retry an hour later started again at 0, so a file too big to read inside the extraction timeout never completed. The pass now starts where every embedded track's coverage from the head ends (less the window pre-roll, so a cue across the join is read whole). The seek needs -avoid_negative_ts on this read as on a window, measured on a real file: without it the cue running at the start shifts the whole output. A partial track whose full pass had failed never reached a settled state, so the player polled it every 2 s for the rest of the film. The status now says whether the track is still growing, and a stalled one is polled every 15 s — still inside the 60 s lookahead that reads the next window before an edge.
This commit is contained in:
@@ -678,6 +678,8 @@ export const plexApi = {
|
||||
state: "ready" | "partial" | "preparing" | "unavailable" | "failed";
|
||||
code: string | null;
|
||||
gen: number;
|
||||
/** On `partial`/`preparing`: whether the track still fills in on its own. */
|
||||
growing?: boolean;
|
||||
}> =>
|
||||
// `quiet`: the player reports a failed poll inline, so no global error dialog on top of it.
|
||||
req(
|
||||
|
||||
@@ -208,6 +208,22 @@ describe("usePlexTracks — when the status poll asks again, and when it stops",
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("slows down on a partial track that will not grow on its own, and keeps asking", async () => {
|
||||
// The full pass failed and is backed off for an hour: `partial` never turns `ready` by itself,
|
||||
// and the 2 s cadence ran for the rest of the film. It still carries the position — reading
|
||||
// ahead of an edge depends on it — so it slows rather than stops.
|
||||
const status = vi
|
||||
.spyOn(api, "plexSubtitleStatus")
|
||||
.mockResolvedValue({ state: "partial", code: null, gen: 4, growing: false });
|
||||
const { result } = mount(client());
|
||||
result.current.setSubOrd(0);
|
||||
await waitFor(() => expect(status).toHaveBeenCalledTimes(1));
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(status).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
expect(status).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("backs off after a failed request, and still asks again so a blip heals on its own", async () => {
|
||||
// A failed request leaves `data` empty; it used to read as "still preparing" and re-ask every
|
||||
// second for as long as the player stayed open.
|
||||
|
||||
@@ -484,6 +484,11 @@ const SUB_POLL_ERROR_MS = 10_000;
|
||||
// server keeps for a minute), so no need for the 1 s cadence — but the poll carries the position, and
|
||||
// moving into a stretch that IS cached should stop saying "couldn't be loaded" within a few seconds.
|
||||
const SUB_POLL_UNAVAILABLE_MS = 5_000;
|
||||
// …and on a `partial` track that is no longer `growing` (its full pass failed and is backed off for
|
||||
// an hour): it will not fill in on its own, so the 2 s cadence ran for the rest of the film. The poll
|
||||
// still carries the position — it is what reads the next window ahead of an edge, which the server
|
||||
// starts within LOOKAHEAD_S (60 s) of it — so it slows down rather than stops.
|
||||
const SUB_POLL_STALLED_MS = 15_000;
|
||||
const SUB_TRACK_RETRIES = 2;
|
||||
const SUB_TRACK_RETRY_MS = 2_000;
|
||||
|
||||
@@ -684,7 +689,8 @@ export function usePlexTracks(opts: {
|
||||
const st = q.state.data?.state;
|
||||
if (st === "ready" || st === "failed") return false;
|
||||
if (st === "unavailable") return SUB_POLL_UNAVAILABLE_MS;
|
||||
return st === "partial" ? 2000 : 1000;
|
||||
if (st === "partial") return q.state.data?.growing === false ? SUB_POLL_STALLED_MS : 2000;
|
||||
return 1000;
|
||||
},
|
||||
staleTime: 0,
|
||||
retry: false,
|
||||
|
||||
Reference in New Issue
Block a user