From de10e81259a29ed4af94767698491316c220dc8b Mon Sep 17 00:00:00 2001 From: byrongamatos Date: Sun, 12 Jul 2026 01:29:38 +0200 Subject: [PATCH] fix(venue-crowd): requeue mid-crossfade loops preempted by stingers; hard-stop on manifest swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex preflight round 4: (1) idleLayer() still points at the fading-in layer during a crossfade, so a stinger firing mid-fade overwrote the new loop with nothing requeued — the fading loop is now tracked and requeued like an in-flight load; (2) swapping venue packs while active now goes through stop() so _stopGen invalidates the old manifest's in-flight loads. Co-Authored-By: Claude Fable 5 --- static/v3/venue-crowd.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/static/v3/venue-crowd.js b/static/v3/venue-crowd.js index 994ee6e..bad1083 100644 --- a/static/v3/venue-crowd.js +++ b/static/v3/venue-crowd.js @@ -108,6 +108,7 @@ let _boundToRenderer = false; let _pendingLoop = null; // loop switch deferred by an active stinger let _loadingLoop = null; // loop currently waiting on canplaythrough + let _fadingLoop = null; // loop currently crossfading in (not yet active) let _stingerUntilEnded = false; let _lastStingerAt = -Infinity; let _prevStreak = 0; @@ -236,7 +237,9 @@ loadAndPlay(video, _manifest.loops[state], true, (ok) => { if (_loadingLoop === state) _loadingLoop = null; if (!ok || !_venueActive) return; + _fadingLoop = state; fadeMixTo(layer === 1 ? 1 : 0, fadeMs, () => { + if (_fadingLoop === state) _fadingLoop = null; const old = _videos[_activeLayer]; _activeLayer = layer; if (old && !old.paused) old.pause(); @@ -254,11 +257,15 @@ const layer = idleLayer(); const video = _videos[layer]; // The stinger reuses the idle layer's element, cancelling any loop - // load still in flight there — requeue that loop for when the + // load still in flight there — and idleLayer() is still the fading-in + // layer while a crossfade runs (_activeLayer flips on completion), so + // a mid-fade loop gets overwritten too. Requeue either for when the // stinger ends (the machine already advanced, nothing re-fires it). - if (_loadingLoop) { - _pendingLoop = _loadingLoop; + const interrupted = _loadingLoop || _fadingLoop; + if (interrupted) { + _pendingLoop = interrupted; _loadingLoop = null; + _fadingLoop = null; } // A loop switch deferred (or preempted) by this stinger must play // once the stinger is done OR failed — the machine already advanced, @@ -340,6 +347,7 @@ _stingerUntilEnded = false; _pendingLoop = null; _loadingLoop = null; + _fadingLoop = null; for (const v of _videos) { if (v && !v.paused) v.pause(); } @@ -373,8 +381,11 @@ const norm = normalizeManifest(m); _manifest = norm; if (_venueActive) { + // Full stop first even when replacing pack-for-pack: it bumps + // _stopGen so an in-flight load from the OLD manifest can't + // settle and fade a stale URL in after the new pack starts. + stop(); if (norm) start(); - else stop(); } }