Fix video sizing and replace fixed landing timeout with playback-aware g

- Introduce `attachClip` to set volume, cover-fit, and re-fit on the first presented frame (`created`) so clips no longer render ~3.4× zoomed in due to v4 placeholder sizes.
- Use `setScale` instead of `setDisplaySize` in `fitCover`, which is stable across frame swaps that preserve scale rather than size.
- Drop the blanket 8s safety valve that cut ~15s landing clips in half; replace with two targeted guards: a 5s stalled check (no playback progress) and a duration-based cap (duration + 5s margin).
This commit is contained in:
Brian Fertig 2026-09-04 22:34:10 -06:00
parent dd7000638a
commit 69875c1ff0
1 changed files with 49 additions and 12 deletions

View File

@ -87,8 +87,7 @@ export class SurfaceScene extends Phaser.Scene {
// v4: add.video(x, y, key) — the key is the LAST argument (it loads the // v4: add.video(x, y, key) — the key is the LAST argument (it loads the
// cached clip and attaches the <video> element). // cached clip and attaches the <video> element).
const v = this.add.video(0, 0, this.landKey).setOrigin(0.5).setDepth(10); const v = this.add.video(0, 0, this.landKey).setOrigin(0.5).setDepth(10);
this.fitCover(v); this.attachClip(v);
v.setVolume(this.clipVolume());
this.landVideo = v; this.landVideo = v;
// Fire it straight away. If the browser's autoplay policy locks it // Fire it straight away. If the browser's autoplay policy locks it
@ -99,12 +98,29 @@ export class SurfaceScene extends Phaser.Scene {
v.play(); v.play();
v.on('complete', () => this.startSurface()); // played once → surface v.on('complete', () => this.startSurface()); // played once → surface
v.on('error', () => this.startSurface()); // broken clip → don't strand the player v.on('error', () => this.startSurface()); // broken clip → don't strand the player
// Safety valve: a stalled element (neither complete nor error) still
// gets the player to the deck. const el = v.video;
this.time.delayedCall(8000, () => { // The clip must play to its END before the surface stage (loop clip +
// deck), so no fixed "8s" valve — the current clips are ~15s and a
// constant would cut the landing in half. Two targeted guards instead:
// 1. STALLED — a clip that never starts moving (autoplay locked and
// never unlocked, decode failure, …) must not hold the deck: after
// a grace period with no playback progress, advance anyway.
// 2. CAP — 'ended' is expected by duration + margin; if the element
// goes silent before it, advance anyway.
const playing = () => !!el && (el.currentTime > 0.1 || !el.paused);
this.time.delayedCall(5000, () => {
if (this.landVideo === v && this.phase === 'landing' && !playing()) {
this.startSurface();
}
});
const durS = Number(el && el.duration);
if (Number.isFinite(durS) && durS > 0) {
this.time.delayedCall(durS * 1000 + 5000, () => {
if (this.landVideo === v && this.phase === 'landing') this.startSurface(); if (this.landVideo === v && this.phase === 'landing') this.startSurface();
}); });
} }
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Stage 2 — the looping surface clip + the deck // Stage 2 — the looping surface clip + the deck
@ -121,8 +137,7 @@ export class SurfaceScene extends Phaser.Scene {
if (this.hasVideo(this.surfaceKey)) { if (this.hasVideo(this.surfaceKey)) {
const s = this.add.video(0, 0, this.surfaceKey).setOrigin(0.5).setDepth(10); const s = this.add.video(0, 0, this.surfaceKey).setOrigin(0.5).setDepth(10);
this.fitCover(s); this.attachClip(s);
s.setVolume(this.clipVolume());
s.setLoop(true); s.setLoop(true);
s.play(); s.play();
this.surfaceVideo = s; this.surfaceVideo = s;
@ -286,15 +301,37 @@ export class SurfaceScene extends Phaser.Scene {
return !!(c && typeof c.has === 'function' && c.has(key)); return !!(c && typeof c.has === 'function' && c.has(key));
} }
/**
* Cover-fit a clip to the screen and keep it that way.
*
* v4 quirk: the video object's bookkeeping size (width/height, and even
* frame.realWidth) is a placeholder until the first presented frame
* lands sizing against it (setDisplaySize divides by that width) ends
* up ~3.4× too big, because the frame swap preserves the SCALE, not the
* size. That's how the clips rendered heavily zoomed in. So:
* - fit now with the best known dimensions (a placeholder-free guess),
* - re-fit on 'created', which fires on the first presented frame and
* carries the clip's true (w, h) after that, frame × scale is
* authoritative and stable for the clip's whole life.
*/
attachClip(v) {
v.setVolume(this.clipVolume());
this.fitCover(v);
v.on('created', (vv, w, h) => {
if (vv === v) this.fitCover(vv, w, h);
});
}
/** Cover-scale: the clip fills the screen, cropping whatever overflows. */ /** Cover-scale: the clip fills the screen, cropping whatever overflows. */
fitCover(v) { fitCover(v, iw = 0, ih = 0) {
const vw = (v.video && v.video.width) || v.width || 864; const el = v.video;
const vh = (v.video && v.video.height) || v.height || 480; const vw = iw || (el && (el.videoWidth || el.width)) || (v.frame && v.frame.realWidth) || 864;
const vh = ih || (el && (el.videoHeight || el.height)) || (v.frame && v.frame.realHeight) || 480;
const W = this.scale.width; const W = this.scale.width;
const H = this.scale.height; const H = this.scale.height;
const s = Math.max(W / vw, H / vh); const s = Math.max(W / vw, H / vh);
v.setPosition(W / 2, H / 2); v.setPosition(W / 2, H / 2);
v.setDisplaySize(vw * s, vh * s); v.setScale(s);
} }
clipVolume() { clipVolume() {