fix(sim): add ended safety net so the base loop never freezes

Root cause: the steady-state loop (#vid-loop) restarts only via a narrow
timeupdate window (last 0.08s -> seek to LOOP_TAIL_S), with native loop off.
Headless Chromium reliably lands a final timeupdate in that window, but under
real-browser decode/GPU load a timeupdate can skip past it; the clip then fires
'ended' and freezes on its last frame ('videos aren't looping'). There was no
recovery path. Add an 'ended' listener that seeks back to the tail and resumes,
guaranteeing the loop regardless of the timeupdate race. No change to normal
playback. Proven by a Playwright regression test that forces the ended state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-29 21:52:50 -07:00
parent a2d8179507
commit ec660c4880
2 changed files with 40 additions and 0 deletions
+29
View File
@@ -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);
});