fix(player): a stale session answer can no longer stop the playback that won

- The engine tracks the request out per item: A, then B, then back to A sent a second request
  for A beside the one still out.
- A hardware transcode superseded during its fallback answered with the NEWER session; the losing
  request's player then stopped it as stale. It now answers with its own (dead) session.
- A replaced hls.js instance's late manifest no longer marks the player ready or plays the
  outgoing media.
- A pause given after the answer stands: the manifest and the first canplay read the live intent.
- The debounced A/V-offset restart is dropped on an item change instead of restarting the
  previous item under the next one's title.
This commit is contained in:
2026-10-07 23:16:25 +02:00
parent d25a132495
commit 0a35f2a3b1
6 changed files with 131 additions and 15 deletions
+53
View File
@@ -515,3 +515,56 @@ it("tells React only when the snapshot really changed", async () => {
expect(seen.mock.calls.length).toBe(after);
await asks[0]!.resolve(direct("A"));
});
describe("an item the viewer comes back to while its request is still out", () => {
it("waits for that request instead of sending a second one beside it (A → B → A)", async () => {
// The in-flight request was tracked in one slot: B's request took it, and the way back to A
// sent A2 next to the A1 still out — two spawns for one item, and the loser's answer could
// name the winner's session for the stop.
const { engine, asks } = setup();
engine.start("A", 0);
engine.leaveItem();
engine.start("B", 0);
engine.leaveItem();
engine.start("A", 0);
expect(asks.map((a) => a.id)).toEqual(["A", "B"]);
await asks[0]!.resolve(hls("A", 0, "s1"));
expect(asks.map((a) => a.id)).toEqual(["A", "B", "A"]);
});
});
describe("a session that was replaced before its manifest parsed", () => {
it("neither marks the player ready nor plays the outgoing media", async () => {
const { engine, asks, hlses, media } = setup();
engine.start("A", 0);
await asks[0]!.resolve(hls("A"));
engine.seek("A", 900); // nothing seekable yet: a restart
const plays = media.plays;
hlses[0]!.handlers.onManifestParsed(); // the replaced instance parses late
expect(engine.getSnapshot().ready).toBe(false);
expect(media.plays).toBe(plays);
});
});
describe("a pause given after the answer, before the media can play", () => {
it("stands: neither the manifest nor the first canplay plays it again", async () => {
const { engine, asks, hlses, media } = setup();
engine.start("A", 0);
await asks[0]!.resolve(hls("A"));
engine.requestPlay(false);
hlses[0]!.handlers.onManifestParsed();
expect(media.paused).toBe(true);
media.fire("canplay");
expect(media.paused).toBe(true);
});
it("stands after the manifest played it too", async () => {
const { engine, asks, hlses, media } = setup();
engine.start("A", 0);
await asks[0]!.resolve(hls("A"));
hlses[0]!.handlers.onManifestParsed();
engine.requestPlay(false);
media.fire("canplay");
expect(media.paused).toBe(true);
});
});