[object Object]

← 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 →