← back to Dead Agentabrams
Music room: fix contrarian findings — showIdx assignment (resume + mid-show failover were dead), expired-gesture read, muted-announce collision, bounded error-hop (TK-11178)
9ae8b460b594c4115f155b156e55e24bf1a94591 · 2026-09-03 10:14:44 -0700 · Steve (via Claude)
- showIdx now set in loadShowRef (+curShow) so resume + mid-show archive->YouTube
failover actually fire (were unreachable: showIdx stayed -1 forever)
- capture userActivation BEFORE the metadata await (transient activation expires)
- guard _tryAutoplay with VB.mode!=='youtube' so the fallback isn't stomped by
the old 3-song wishlist announce + a duplicate unlock listener
- _onPlayerError bounded (only genuine-unavailability codes 100/101/150, <=3 hops,
each video once) + refreshes on-screen title/link; harden the gesture-unlock race
- persist resume in fallback mode too
- era-match unit-tested in node (1973->1973, 1991->1991, etc.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XLgczMhQr91Ad5jL38JM4
Files touched
Diff
commit 9ae8b460b594c4115f155b156e55e24bf1a94591
Author: Steve (via Claude) <steve@designerwallcoverings.com>
Date: Thu Sep 3 10:14:44 2026 -0700
Music room: fix contrarian findings — showIdx assignment (resume + mid-show failover were dead), expired-gesture read, muted-announce collision, bounded error-hop (TK-11178)
- showIdx now set in loadShowRef (+curShow) so resume + mid-show archive->YouTube
failover actually fire (were unreachable: showIdx stayed -1 forever)
- capture userActivation BEFORE the metadata await (transient activation expires)
- guard _tryAutoplay with VB.mode!=='youtube' so the fallback isn't stomped by
the old 3-song wishlist announce + a duplicate unlock listener
- _onPlayerError bounded (only genuine-unavailability codes 100/101/150, <=3 hops,
each video once) + refreshes on-screen title/link; harden the gesture-unlock race
- persist resume in fallback mode too
- era-match unit-tested in node (1973->1973, 1991->1991, etc.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XLgczMhQr91Ad5jL38JM4
---
room/index.html | 82 ++++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 67 insertions(+), 15 deletions(-)
diff --git a/room/index.html b/room/index.html
index f105e85..2749d0e 100644
--- a/room/index.html
+++ b/room/index.html
@@ -237,6 +237,34 @@
.mr-foot a{color:var(--gold);text-decoration:none;border-bottom:1px solid rgba(242,182,74,.4)}
.mr-msg{margin-top:9px;font-size:11px;line-height:1.5;color:var(--ink-dim)}
+ /* ---- Videos panel — official GD concert footage on YouTube, standalone.
+ Independent of archive.org/Music Room so it still works when that's down. ---- */
+ .videoroom{
+ position:fixed;right:26px;top:158px;z-index:9;width:min(84vw,300px);
+ background:var(--panel);border:1px solid var(--panel-edge);border-radius:14px;
+ backdrop-filter:blur(9px);box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden;font-size:12px;
+ }
+ .videoroom > header{display:flex;align-items:center;gap:9px;padding:11px 14px;cursor:pointer;
+ font-family:var(--serif);font-style:italic;color:var(--gold);font-size:14px;user-select:none}
+ .videoroom .chev{margin-left:auto;transition:transform .3s;font-size:11px}
+ .videoroom[data-open="false"] .chev{transform:rotate(-90deg)}
+ .videoroom[data-open="false"] .vrbody{display:none}
+ .vrbody{padding:0 14px 14px}
+ .vr-embed{position:relative;width:100%;aspect-ratio:16/9;border-radius:9px;overflow:hidden;background:#000;margin-bottom:9px}
+ .vr-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
+ .vr-embed:empty::after{content:'Pick a show below';display:flex;align-items:center;justify-content:center;
+ height:100%;color:var(--ink-dim);font-style:italic;font-size:11.5px}
+ .vr-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px}
+ .vr-row{display:flex;gap:9px;align-items:baseline;width:100%;text-align:left;background:none;border:0;
+ color:var(--ink-dim);font-family:var(--sans);font-size:11.5px;padding:6px 6px;cursor:pointer;border-radius:6px}
+ .vr-row:hover{color:var(--ink);background:rgba(70,20,26,.6)}
+ .vr-row.on{color:var(--gold);background:rgba(242,182,74,.12)}
+ .vr-row .ry{color:var(--gold);font-variant-numeric:tabular-nums;min-width:34px;flex:none}
+ .vr-row .rt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ @media (max-width:820px){
+ .videoroom{top:auto;bottom:148px;left:12px;right:12px;width:auto}
+ }
+
/* master Controls toggle — panels start collapsed, user opens them */
.chrome-toggle{position:fixed;top:22px;right:26px;z-index:12}
#stage.hide-chrome .dock,
@@ -1654,10 +1682,23 @@ function _onPlayerState(e){
// loop the backdrop so the visuals (and, in fallback, the audio) persist for the whole show
if(window.YT && e.data===YT.PlayerState.ENDED){ try{ VB.player.seekTo(0); VB.player.playVideo(); }catch(_){} }
}
-function _onPlayerError(){
- // a specific video went private/blocked → hop to the next-nearest era video once
- const cur=VB.curId, alt=MR_VIDEOS.find(v=>v.id!==cur);
- if(alt){ VB.wantId=alt.id; VB.curId=null; _applyVideo(); }
+function _onPlayerError(e){
+ // Only hop for GENUINE unavailability — 100 (removed/private), 101/150 (embedding disabled).
+ // Codes 2 (bad param) and 5 (HTML5 playback, e.g. headless browsers) are transient → don't cascade the library.
+ const code=e&&e.data;
+ if(code!==100 && code!==101 && code!==150) return;
+ VB._tried=VB._tried||new Set(); VB._tried.add(VB.curId);
+ VB._hops=(VB._hops||0)+1;
+ const alt=(VB._hops<=3) ? MR_VIDEOS.find(v=>!VB._tried.has(v.id)) : null; // bounded: at most 3 hops, each video once
+ if(!alt){
+ if(VB.mode==='youtube'){ const m=$('mrMsg'); if(m){ m.hidden=false; m.textContent='This concert video is unavailable right now — try another night, or check back when archive.org is up.'; } }
+ return;
+ }
+ VB.wantId=alt.id; VB.curId=null; _applyVideo();
+ if(VB.mode==='youtube'){ // keep the on-screen title + Watch link in step with what's actually playing
+ $('mrNow').textContent='▶ Grateful Dead — '+alt.title;
+ const m=$('mrMsg'); if(m && !m.hidden){ m.innerHTML=m.innerHTML.replace(/watch\?v=[\w-]+/, 'watch?v='+alt.id); }
+ }
}
function _applyVideo(){
const p=VB.player; if(!p || !p.loadVideoById) return;
@@ -1670,6 +1711,7 @@ function _applyVideo(){
function vbSetVideo(show,opts){
opts=opts||{};
const v=pickVideoFor(show); if(!v) return;
+ VB._tried=new Set(); VB._hops=0; // new intended show → reset the bounded error-hop guard
VB.wantId=v.id; VB.wantPlay=opts.play!==false ? !!opts.play : false; VB.wantMuted=opts.muted!==false;
_loadYTApi();
if(VB.player && VB.player.loadVideoById) _applyVideo();
@@ -1679,8 +1721,13 @@ function vbSetVideo(show,opts){
let _ytUnlockArmed=false;
function _primeYTUnlock(){
if(_ytUnlockArmed) return; _ytUnlockArmed=true;
- const go=()=>{ if(VB.mode==='youtube' && VB.player){ try{ VB.player.unMute(); VB.player.setVolume(100); VB.player.playVideo(); }catch(e){} }
- document.removeEventListener('pointerdown',go,true); document.removeEventListener('keydown',go,true); _ytUnlockArmed=false; };
+ const done=()=>{ document.removeEventListener('pointerdown',go,true); document.removeEventListener('keydown',go,true); _ytUnlockArmed=false; };
+ const go=()=>{
+ if(VB.mode!=='youtube'){ done(); return; } // left the fallback — stop listening
+ if(!VB.player || !VB.player.unMute){ return; } // player not built yet — KEEP listening for the next gesture (no wasted tap)
+ try{ VB.player.unMute(); VB.player.setVolume(100); VB.player.playVideo(); }catch(e){}
+ done();
+ };
document.addEventListener('pointerdown',go,true); document.addEventListener('keydown',go,true);
}
// archive mode: the real SBD tape plays from <audio>; the concert video is a MUTED visual backdrop.
@@ -1690,11 +1737,12 @@ function enterArchiveMode(show){
vbSetVideo(show,{play:true,muted:true});
}
// youtube fallback: archive.org is down → the video BECOMES the audio (unmuted). Mutual exclusion: kill <audio>.
-function enterYouTubeFallback(show){
+function enterYouTubeFallback(show, hadGesture){
VB.mode='youtube';
try{ audio.pause(); }catch(e){}
try{ audio.removeAttribute('src'); audio.load(); }catch(e){} // no archive audio can leak under the video
- const hasAct=!!(navigator.userActivation ? navigator.userActivation.isActive : false);
+ // Prefer the gesture captured BEFORE the metadata await (transient activation expires in ~5s); else read it live.
+ const hasAct=(hadGesture!=null) ? !!hadGesture : !!(navigator.userActivation ? navigator.userActivation.isActive : false);
vbSetVideo(show,{play:true,muted:!hasAct}); // gesture → sound now; cold load → muted+visible
if(!hasAct) _primeYTUnlock();
const v=MR_VIDEOS.find(x=>x.id===VB.wantId)||{};
@@ -1709,6 +1757,7 @@ function enterYouTubeFallback(show){
setPlay(true);
if(typeof reactBtn!=='undefined' && reactBtn){ reactBtn.disabled=true; reactBtn.title='Live audio analysis isn’t available on the YouTube stream.'; }
announce('archive.org is unreachable — playing the nearest official Grateful Dead concert from YouTube.');
+ saveResume(); // persist the selected show even in fallback (audio timeupdate won't fire here)
}
function vbTogglePlay(){
const p=VB.player; if(!p || !window.YT) return;
@@ -1727,7 +1776,7 @@ function readResume(){ try{ return JSON.parse(localStorage.getItem(_RKEY)||'null
const $ = id => document.getElementById(id);
const audio = $('mrAudio'), room = $('musicroom'), sel = $('mrShow'), live = $('live');
-let showIdx=-1, tracks=[], trackIdx=-1, seeking=false, loadToken=0;
+let showIdx=-1, tracks=[], trackIdx=-1, seeking=false, loadToken=0, curShow=null;
CATALOG.forEach((s,i)=>{ const o=document.createElement('option'); o.value=String(i); o.textContent=`${s.date} — ${s.title}`; sel.appendChild(o); });
@@ -1747,6 +1796,9 @@ async function loadShowRef(s, opts){
const myToken=++loadToken; // guard against a stale fetch overwriting a newer selection
try{ audio.pause(); }catch(e){} // stop the current night before loading the next
tracks=[]; trackIdx=-1;
+ showIdx = (s && s.venueIdx!=null) ? s.venueIdx : -1; // TK-11178: real venue index — resume + mid-show failover read this
+ curShow = s; // the actual show object (covers search-result shows too)
+ const _gestureNow = !!(navigator.userActivation ? navigator.userActivation.isActive : false); // capture BEFORE the await (transient activation expires ~5s)
if(window.SRM){ window.SRM.showIdx=(s.venueIdx!=null?s.venueIdx:null); window.SRM.venueLabel=s.venue||[s.title,s.date].filter(Boolean).join(' · '); }
$('mrNow').textContent=`${s.title} — loading set list…`;
$('mrSub').textContent=[s.date,s.sub].filter(Boolean).join(' · ');
@@ -1776,7 +1828,7 @@ async function loadShowRef(s, opts){
if(opts.play) playTrack(0);
}catch(e){
if(myToken!==loadToken) return; // superseded — let the newer selection own the UI
- enterYouTubeFallback(s); // archive.org unreachable → the official YouTube concert becomes the audio
+ enterYouTubeFallback(s, _gestureNow); // archive.org unreachable → the official YouTube concert becomes the audio (honor the just-made gesture)
}
}
async function loadShow(i, opts){ if(sel) sel.value=String(i); return loadShowRef(Object.assign({venueIdx:i}, CATALOG[i]), opts); }
@@ -1959,8 +2011,8 @@ audio.addEventListener('timeupdate',()=>{
audio.addEventListener('error',()=>{
const err=audio.error;
if(err && err.code===err.MEDIA_ERR_ABORTED) return; // fired by our own src swaps — not a real failure
- if(showIdx<0) return;
- if(!tracks.length){ enterYouTubeFallback(CATALOG[showIdx]); return; }
+ if(!curShow) return; // nothing loaded yet
+ if(!tracks.length){ enterYouTubeFallback(curShow); return; } // archive stream died mid-show → YouTube fallback
// a specific track wouldn't stream (404/geo/transient) — move on rather than stall silently
if(trackIdx>=0 && trackIdx<tracks.length-1){ announce('That track was unavailable; skipping ahead.'); playTrack(trackIdx+1); }
else { setPlay(false); const m=$('mrMsg'); m.hidden=false; m.textContent='That track could not be streamed from the archive right now — try another night.'; }
@@ -2133,7 +2185,7 @@ async function bootMusic(){
trackIdx=_R.trackIdx; audio.src=tracks[_R.trackIdx].url; highlight(); $('mrNow').textContent='♪ '+tracks[_R.trackIdx].title;
}
if(_R.t>0){ const _seek=()=>{ try{ if(audio.duration) audio.currentTime=Math.min(_R.t, audio.duration-1); }catch(e){} audio.removeEventListener('loadedmetadata',_seek); }; audio.addEventListener('loadedmetadata',_seek); }
- _tryAutoplay();
+ if(VB.mode!=='youtube') _tryAutoplay(); // YT fallback owns its own unlock + announcement
return;
}
$('mrNow').textContent='Cueing the set…';
@@ -2148,12 +2200,12 @@ async function bootMusic(){
setFoot(list[0].show);
enterArchiveMode(list[0].show); // muted era-matched concert video behind the autostart set
announce('Autostarting: '+list.map(t=>t.title).join(', ')+'.');
- _tryAutoplay();
+ if(VB.mode!=='youtube') _tryAutoplay();
return;
}
}catch(e){}
await loadShow(DEFAULT_SHOW); // fallback: archive slow/down → cue the default night, still try to start
- _tryAutoplay();
+ if(VB.mode!=='youtube') _tryAutoplay(); // if archive is down, loadShow already switched to the YouTube fallback (owns its own start)
}
bootMusic();
← 339f860 Put all dock pills (Pause/Restart + the 5 mode toggles) on o
·
back to Dead Agentabrams
·
Add a standalone Videos section, independent of archive.org/ 6b68f71 →