orbit/dev/video-replay.mjs

168 lines
6.3 KiB
JavaScript

/**
* Dev-only regression test: feed-window video play/pause/replay.
*
* Symptom under test (Build/Research/Map/Quests windows): open the
* window, close it, open it again — the feed stays PAUSED.
*
* Root cause lives in the vendored Phaser 4 Video object: play() only
* issues the native <video>.play() while its internal _playCalled flag
* is false; pause() leaves the flag true, so a later play() call is a
* silent no-op. resume() is the code path that re-issues native
* playback for a previously-played, now-paused clip.
*
* This page exercises exactly those call sequences on real Phaser
* Video objects (assets/videos/build.mp4) and reports:
* A play → pause → play (old open() path — EXPECTED to stay paused)
* B play → pause → resume (new open() path — EXPECTED to play)
* C resume (first open) (new open() path on a fresh clip)
*
* node dev/server.mjs 8123 # any static server that also answers /sleep?ms=N
* firefox --headless --screenshot VIDEO_REPLAY.png \
* --window-size=960,520 "http://127.0.0.1:8123/dev/video-replay.html"
*/
import Phaser from '../js/vendor/phaser.js';
const errors = [];
const origErr = console.error.bind(console);
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
window.addEventListener('error', (e) => errors.push(String(e.message)));
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
const report = document.createElement('pre');
report.id = 'report';
report.style.cssText =
'position:fixed;left:10px;top:10px;z-index:9999;margin:0;padding:10px 14px;font:13px/1.55 monospace;color:#eaf6ff;background:rgba(6,20,16,0.94);border:1px solid #1b3a5a;white-space:pre-wrap;';
document.body.appendChild(report);
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
setReport('booting…');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
class VideoTestScene extends Phaser.Scene {
constructor() {
super({ key: 'VideoTestScene' }); // v4: scenes carry their key explicitly
}
preload() {
this.load.video('feedA', 'assets/videos/build.mp4');
this.load.video('feedB', 'assets/videos/build.mp4');
this.load.video('feedC', 'assets/videos/build.mp4');
}
create() {
this.__ready = true; // create() runs after the loader completes
}
}
const game = new Phaser.Game({
type: Phaser.AUTO,
width: 300,
height: 200,
parent: 'game',
backgroundColor: '#04060d',
scene: VideoTestScene,
});
window.game = game;
const results = [];
const line = (ok, tag, detail) => results.push(`${ok ? 'PASS' : 'FAIL'} ${tag}${detail}`);
function snapshot(v, label) {
const el = v && v.video;
const s = el
? `paused=${el.paused} t=${(el.currentTime || 0).toFixed(2)}s ended=${el.ended}`
: 'no <video> element';
return `${label}: ${s}`;
}
async function run() {
// the scene manager resolves by name only after boot — poll (same as
// the other dev harnesses, e.g. research-shot.mjs)
let scene = null;
for (let i = 0; i < 200; i++) {
scene = game.scene.getScene('VideoTestScene');
if (scene) break;
await sleep(100);
}
if (!scene) {
const dump = JSON.stringify({
booted: game.isBooted,
renderer: game.renderer && game.renderer.type,
scenes: (game.scene && game.scene.scenes || []).map((sc) => (sc.sys && (sc.sys.settings && sc.sys.settings.key || sc.sys.key)) || sc.key),
}, null, 0);
throw new Error('scene never registered — ' + dump);
}
// wait for the scene's create() (loader done)
for (let i = 0; i < 200 && !scene.__ready; i++) await sleep(100);
if (!scene.__ready) throw new Error('scene never reached create()');
await sleep(300);
const mk = (key) => {
const v = scene.add.video(0, 0, key);
v.setLoop(true);
if (v.video) v.video.muted = true;
return v;
};
// ── A: the OLD open() path — play() after pause() ─────────────────
const a = mk('feedA');
a.play(); // open #1
await sleep(1200);
const a1 = a.video;
line(!a1.paused, 'A-open#1 play()', snapshot(a, 'after play()'));
a.pause(); // close
await sleep(400);
line(a1.paused, 'A-close pause()', snapshot(a, 'after pause()'));
a.play(); // open #2 — the old code path
await sleep(1200);
line(
a1.paused,
'A-open#2 play() (old path — reproduces the bug: stays paused)',
snapshot(a, 'after 2nd play()')
);
// ── B: the NEW open() path — resume() after pause() ────────────────
const b = mk('feedB');
b.resume(); // open #1 (resume() delegates to play() on a fresh clip)
await sleep(1200);
const b1 = b.video;
line(!b1.paused, 'B-open#1 resume()', snapshot(b, 'after first resume()'));
b.pause(); // close
await sleep(400);
const tBefore = b1.currentTime;
b.resume(); // open #2 — the new code path
await sleep(1200);
line(
!b1.paused && b1.currentTime > tBefore,
'B-open#2 resume() (new path — fixed)',
snapshot(b, `after resume() (was t=${tBefore.toFixed(2)}s)`)
);
// ── C: first-ever open uses resume() (never played before) ────────
const c = mk('feedC');
c.resume(); // open — never played
await sleep(1200);
const c1 = c.video;
line(!c1.paused && c1.currentTime > 0, 'C-open#1 resume() (fresh clip)', snapshot(c, 'after resume()'));
// ── verdict ───────────────────────────────────────────────────────
const allPass = results.every((r) => r.startsWith('PASS'));
const lines = [
`Phaser ${Phaser.VERSION}`,
allPass ? 'VIDEO REPLAY TEST — ALL PASS' : 'VIDEO REPLAY TEST — FAILURES',
'─'.repeat(64),
...results,
'',
'A-open#2 staying paused is EXPECTED: it is the Phaser 4 play()-after-',
'pause() no-op that the window fix (resume()) works around.',
errors.length ? `errors:\n${errors.slice(0, 5).join('\n')}` : 'no console errors',
];
setReport(lines);
window.__done = true; // dev/shot-firefox.mjs polls this
a.pause(); // stop the repro clip; B/C keep looping (ambience, muted)
setReport(lines);
}
run().catch((e) => {
window.__done = true;
setReport(['VIDEO REPLAY TEST — ERROR', String((e && e.message) || e), ...errors.slice(0, 5)]);
});