Merge fix/video-loop-ended-safety-net: base loop never freezes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// Regression: the steady-state loop (#vid-loop) restarts from the morph-tail
|
||||
// offset via a narrow timeupdate window. If that window is ever missed the clip
|
||||
// fires `ended` and — without a safety net — freezes on its last frame ("videos
|
||||
// aren't looping"). This asserts the `ended` safety net recovers playback.
|
||||
test("loop recovers when a clip reaches `ended` (no freeze on last frame)", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#loading")?.classList.contains("done"),
|
||||
null,
|
||||
{ timeout: 45_000 },
|
||||
);
|
||||
|
||||
const r = await page.evaluate(async () => {
|
||||
const lv = document.getElementById("vid-loop") as HTMLVideoElement;
|
||||
// Reproduce the stuck state: parked on the last frame, as after a missed wrap.
|
||||
lv.pause();
|
||||
lv.currentTime = Math.max(0, lv.duration - 0.001);
|
||||
// The browser emits `ended` here in real life; emit it deterministically.
|
||||
lv.dispatchEvent(new Event("ended"));
|
||||
await new Promise((res) => setTimeout(res, 800));
|
||||
return { dur: +lv.duration.toFixed(2), ct: +lv.currentTime.toFixed(2), paused: lv.paused };
|
||||
});
|
||||
|
||||
// Recovered: playing again, and seeked back near the loop tail (not stuck at end).
|
||||
expect(r.paused).toBe(false);
|
||||
expect(r.ct).toBeLessThan(5);
|
||||
});
|
||||
@@ -274,6 +274,17 @@ loopVid.addEventListener("timeupdate", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Safety net: the timeupdate wrap above is a narrow window (the last 0.08 s). Under
|
||||
// real-browser decode/GPU load a `timeupdate` can skip past it, the clip fires
|
||||
// `ended`, and — with native loop off — freezes on its last frame ("not looping").
|
||||
// `ended` GUARANTEES recovery: seek back to the tail and resume. Headless rarely
|
||||
// hits this; loaded real machines do.
|
||||
loopVid.addEventListener("ended", () => {
|
||||
if (loopVid.dataset.loopTail !== "1") return;
|
||||
try { loopVid.currentTime = LOOP_TAIL_S; } catch (e) { /* not seekable */ }
|
||||
loopVid.play().catch(() => {});
|
||||
});
|
||||
|
||||
// Compose the video look. The Right DREAM is now a painterly WebGL restyle of the
|
||||
// live frames (see the render loop below) — so here we only stash its intensity for
|
||||
// that loop, and build the Dark/Light mood grade as a CSS filter. The grade rides
|
||||
|
||||
Reference in New Issue
Block a user