fix(plex): a seek during a quiet reclaim is no longer undone
A quiet request leaves `live` alone, so a seek inside the old session's range went native and the answer then jumped back to where it asked from. `seek` now also goes by `pending`; the server-away retry had the same hole. Comments that still named 404 for a lost session say 410, and the hls.js retry comment now tells a finished playlist from a growing one.
This commit is contained in:
@@ -374,7 +374,7 @@ describe("a failed session request", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a session the server no longer has (404)", () => {
|
||||
describe("something that never arrives (a fatal 404)", () => {
|
||||
it("is reclaimed where the viewer is", async () => {
|
||||
const { engine, asks, hlses, state } = setup();
|
||||
engine.start("A", 600);
|
||||
@@ -482,6 +482,17 @@ describe("a session the server says is gone (410)", () => {
|
||||
expect(engine.getSnapshot().error).toBe("generic");
|
||||
});
|
||||
|
||||
it("keeps a seek made while it is out, inside what the old session still reports", async () => {
|
||||
const { engine, asks, hlses, media } = await playing();
|
||||
media.seekEnd = 800;
|
||||
hlses[0]!.gone();
|
||||
media.currentTime = 42;
|
||||
engine.seek("A", 1250);
|
||||
expect(media.currentTime).toBe(42); // the outgoing media stays where it is
|
||||
await asks[1]!.resolve(hls("A", 1299));
|
||||
expect(asks.map((a) => a.startAt)).toEqual([600, 1299, 1250]);
|
||||
});
|
||||
|
||||
it("leaves a segment that is merely not there yet to hls.js's retries", async () => {
|
||||
const { engine, asks, hlses } = await playing();
|
||||
hlses[0]!.retrying(404);
|
||||
@@ -493,6 +504,20 @@ describe("a session the server says is gone (410)", () => {
|
||||
describe("a server that is restarting (a deploy)", () => {
|
||||
// The gateway answers 502 while the app restarts, and nothing at all may answer (status 0): the
|
||||
// stream is not broken, the server is away. The buffer keeps playing meanwhile.
|
||||
it("keeps a seek made while the retry is out", async () => {
|
||||
const { engine, asks, hlses, state, fire, media } = setup();
|
||||
engine.start("A", 600);
|
||||
await asks[0]!.resolve(hls("A", 600));
|
||||
hlses[0]!.handlers.onManifestParsed();
|
||||
media.seekEnd = 800;
|
||||
state.position = 1299;
|
||||
hlses[0]!.lost(502);
|
||||
expect(fire()).toBe(true);
|
||||
engine.seek("A", 1250);
|
||||
await asks[1]!.resolve(hls("A", 1299));
|
||||
expect(asks.map((a) => a.startAt)).toEqual([600, 1299, 1250]);
|
||||
});
|
||||
|
||||
it.each([502, 503, 504, 0, null])(
|
||||
"is waited out on %s: no error, and the stream comes back where the viewer is",
|
||||
async (status) => {
|
||||
|
||||
@@ -285,7 +285,7 @@ export class HlsEngine {
|
||||
|
||||
/** (Re)start `id` at `startAt` seconds. Another item's request goes out at once; one for the item
|
||||
* already being asked for waits for that answer (see the header). `quiet`: the attached stream is
|
||||
* still playing from its buffer (the server went away under it), so it stays the player's until
|
||||
* still playing from its buffer (the server went away or lost its session), so it stays the player's until
|
||||
* the answer replaces it — no "Loading…" over a running picture, its clock still true. */
|
||||
start(id: string, startAt: number, play = true, quiet = false): void {
|
||||
if (!this.deps.media()) return;
|
||||
@@ -467,12 +467,14 @@ export class HlsEngine {
|
||||
// Except a session the server no longer has (410): gone, not broken. Either the server reaped it
|
||||
// or restarted, or it was superseded before it ever played (two requests for one item: the
|
||||
// server's LAST spawn wins), and the reclaim is the asking again that answer wants. It does not
|
||||
// wait for hls.js to give up: with one video variant it has nothing to switch to, so it retries
|
||||
// even a 4xx, six times over about half a minute (measured: seven requests, then the reclaim), and
|
||||
// in a throttled page once a minute (four minutes). Quiet, since the buffer is still playing.
|
||||
// wait for hls.js to give up. On a finished playlist, with one video variant and nothing to
|
||||
// switch to, it retries even a 4xx, six times over about half a minute (measured: seven requests,
|
||||
// then the reclaim), and in a throttled page once a minute (four minutes). On a growing one it
|
||||
// takes the segment for a gap and moves on, so the error is never fatal. Either way the first
|
||||
// answer is reported. Quiet, since the buffer is still playing.
|
||||
//
|
||||
// And except a fatal 404: a segment still not there after those retries, so a transcode has fallen
|
||||
// behind, and asking again is all that is left.
|
||||
// And except a fatal 404: hls.js gave up on something the server still does not have, and asking
|
||||
// again is all that is left.
|
||||
//
|
||||
// Both are bounded by the budget, not by whether the session got as far as playing: that belongs to
|
||||
// ONE session, and every restart builds a fresh one, so it could never bound a loop. A transcode
|
||||
@@ -565,9 +567,10 @@ export class HlsEngine {
|
||||
const rel = T - this.sessionStart.current;
|
||||
// Native only when the attached media IS the current session. While one is still being asked
|
||||
// for, `sessionStart` and `seekable` describe the OLD one: a native seek there left the pending
|
||||
// restart current, and its answer then jumped to the earlier target.
|
||||
// restart current, and its answer then jumped to the earlier target. A quiet request leaves
|
||||
// `live` alone, so `pending` is what says one is out.
|
||||
const end = media.seekable.length ? media.seekable.end(0) : 0;
|
||||
if (this.live && rel >= 0 && rel <= end + 0.5) {
|
||||
if (this.live && !this.pending && rel >= 0 && rel <= end + 0.5) {
|
||||
media.currentTime = rel; // keeps playing on its own
|
||||
if (play) media.play().catch(() => {});
|
||||
} else {
|
||||
|
||||
@@ -134,13 +134,13 @@ const lost404 = {
|
||||
fatal: true,
|
||||
type: Hls.ErrorTypes.NETWORK_ERROR,
|
||||
details: "fragLoadError",
|
||||
response: { code: 404, text: "No active playback session" },
|
||||
response: { code: 404, text: "Not available yet" },
|
||||
};
|
||||
|
||||
describe("an HLS session the server no longer has", () => {
|
||||
describe("an HLS segment that never arrives (a fatal 404)", () => {
|
||||
it("is restarted where the viewer is, instead of ending playback", async () => {
|
||||
// The server reaps a session nobody fetches from (a long pause; a deploy). hls.js never retries
|
||||
// a 4xx, so the next segment's 404 used to end playback with "couldn't play".
|
||||
// Once hls.js has given up on it, the 404 used to end playback with "couldn't play". A session
|
||||
// the server no longer has answers 410 instead: `hlsEngine.test.ts` holds that case.
|
||||
const session = mount();
|
||||
await waitFor(() => expect(hlsInstances).toHaveLength(1));
|
||||
act(() => hlsInstances[0]!.emit(Hls.Events.MANIFEST_PARSED));
|
||||
@@ -155,10 +155,9 @@ describe("an HLS session the server no longer has", () => {
|
||||
|
||||
it("gives up and says so when the reclaimed session keeps 404ing", async () => {
|
||||
// `parsed` cannot bound this: it lives on ONE hls instance and every restart builds a fresh one,
|
||||
// so a session that parses its manifest and then 404s re-arms the guard every cycle. The server
|
||||
// also answers 404 for "that segment is not there YET" (a transcode falling behind realtime),
|
||||
// which is indistinguishable on the wire — so the player respawned ffmpeg forever behind a
|
||||
// spinner that never became an error.
|
||||
// so a session that parses its manifest and then 404s re-arms the guard every cycle. A transcode
|
||||
// falling behind realtime answers 404 for "that segment is not there YET" every time, so the
|
||||
// player respawned ffmpeg forever behind a spinner that never became an error.
|
||||
const session = mount();
|
||||
const v = () => document.querySelector("video") as Media;
|
||||
// The first session plus the two it is allowed to reclaim; the third 404 has nothing left.
|
||||
|
||||
Reference in New Issue
Block a user