fix(venue-crowd): requeue mid-crossfade loops preempted by stingers; hard-stop on manifest swap

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 <noreply@anthropic.com>
This commit is contained in:
byrongamatos
2026-07-12 01:29:38 +02:00
co-authored by Claude Fable 5
parent 1172bc30cb
commit de10e81259
+15 -4
View File
@@ -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();
}
}