From 9178959dbdc7c72be7727c10279d262a48dc69de Mon Sep 17 00:00:00 2001 From: byrongamatos Date: Sun, 12 Jul 2026 01:33:12 +0200 Subject: [PATCH] fix(venue-crowd): generation-gate stinger handlers; recrop on video size change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex preflight round 5: (1) an ended/timeout handler orphaned by stop() could fire into a later stinger's lifecycle on the reused element — handlers now detach unconditionally and carry a generation token; (2) the renderer only re-applied cover-crop on camera aspect changes, so a src swap with a different intrinsic size kept stale repeat/offset — it now recrops when videoWidth/Height change. Co-Authored-By: Claude Fable 5 --- plugins/highway_3d/screen.js | 10 ++++++++++ static/v3/venue-crowd.js | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index b4ee0f3..a070750 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -3489,6 +3489,16 @@ // videoWidth === 0 until metadata lands — showing the // plane before that paints a black flash over the plate. const ready = !!el && el.videoWidth > 0; + // venue-crowd.js swaps src on the same element (loop ↔ + // stinger); a new intrinsic size needs a fresh + // cover-crop, which _bgFitBackdropPlane only reapplies + // on camera aspect changes. + if (ready && (layer.lastVidW !== el.videoWidth || + layer.lastVidH !== el.videoHeight)) { + layer.lastVidW = el.videoWidth; + layer.lastVidH = el.videoHeight; + layer.applyCoverCrop(); + } const opacity = i === 0 ? 1 - _venueCrowdMix : _venueCrowdMix; layer.mat.opacity = opacity; layer.mesh.visible = ready && opacity > 0.01; diff --git a/static/v3/venue-crowd.js b/static/v3/venue-crowd.js index bad1083..2e92422 100644 --- a/static/v3/venue-crowd.js +++ b/static/v3/venue-crowd.js @@ -110,6 +110,7 @@ let _loadingLoop = null; // loop currently waiting on canplaythrough let _fadingLoop = null; // loop currently crossfading in (not yet active) let _stingerUntilEnded = false; + let _stingerGen = 0; // identity for ended/timeout handlers let _lastStingerAt = -Infinity; let _prevStreak = 0; let _lastAccuracyPct = null; // from perf events; stats:recorded carries none @@ -276,10 +277,15 @@ _pendingLoop = null; showLoop(pending, FADE_MS); }; + const myGen = ++_stingerGen; const back = () => { - if (!_stingerUntilEnded) return; - _stingerUntilEnded = false; + // Always detach: a handler left behind by a stop()/manifest swap + // must not fire into a LATER stinger's lifecycle on this reused + // element (the gen check below guards that; the boolean alone + // would pass once a new stinger is active). video.removeEventListener('ended', back); + if (_stingerGen !== myGen || !_stingerUntilEnded) return; + _stingerUntilEnded = false; // Fade back to the loop layer (which kept playing underneath). fadeMixTo(_activeLayer === 1 ? 1 : 0, STINGER_FADE_MS); flushPending(); @@ -344,6 +350,7 @@ function stop() { cancelFade(); _stopGen++; + _stingerGen++; _stingerUntilEnded = false; _pendingLoop = null; _loadingLoop = null;