← back to Dead Agentabrams
Music room: YouTube era-matched video-library backdrop + archive.org failover + resume (TK-11178)
b7cdfc80049f07ffa7b4c9669acd86a2297b24da · 2026-09-03 09:56:28 -0700 · Steve (via Claude)
- 18-video embed-verified library (1969-1995), nearest-year era matching
- Every show: muted era-matched official GD concert video as #stage backdrop
- archive.org down -> unmuted YouTube concert becomes the audio (replaces the
old dead 'open archive.org player' fallback that also failed during an outage)
- localStorage resume; venue button toggles the video backdrop; React-to-Music
disabled on the YouTube stream (Web Audio can't read a cross-origin iframe)
- Timed the two hot-path archive.org fetches so an outage fails fast to fallback
- Mutual audio exclusion enforced (no double audio); verified end-to-end via
Playwright against the live archive.org outage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XLgczMhQr91Ad5jL38JM4
Files touched
Diff
commit b7cdfc80049f07ffa7b4c9669acd86a2297b24da
Author: Steve (via Claude) <steve@designerwallcoverings.com>
Date: Thu Sep 3 09:56:28 2026 -0700
Music room: YouTube era-matched video-library backdrop + archive.org failover + resume (TK-11178)
- 18-video embed-verified library (1969-1995), nearest-year era matching
- Every show: muted era-matched official GD concert video as #stage backdrop
- archive.org down -> unmuted YouTube concert becomes the audio (replaces the
old dead 'open archive.org player' fallback that also failed during an outage)
- localStorage resume; venue button toggles the video backdrop; React-to-Music
disabled on the YouTube stream (Web Audio can't read a cross-origin iframe)
- Timed the two hot-path archive.org fetches so an outage fails fast to fallback
- Mutual audio exclusion enforced (no double audio); verified end-to-end via
Playwright against the live archive.org outage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XLgczMhQr91Ad5jL38JM4
---
room/index.html | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 181 insertions(+), 6 deletions(-)
diff --git a/room/index.html b/room/index.html
index 3971347..35c8ccc 100644
--- a/room/index.html
+++ b/room/index.html
@@ -24,6 +24,14 @@
#stage{position:fixed;inset:0;display:block}
canvas{position:absolute;inset:0;width:100%;height:100%}
#orb{z-index:4;pointer-events:none}
+ /* ---- Concert-video backdrop (TK-11178): official GD footage behind the generative scene ---- */
+ #venueVideo{position:absolute;inset:0;overflow:hidden;z-index:0;pointer-events:none;background:#000;opacity:0;transition:opacity 1.4s ease}
+ #venueVideo iframe,#venueVideo>div{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
+ width:100vw;height:56.25vw;min-height:100vh;min-width:177.78vh;border:0;pointer-events:none}
+ #stage.video-bg #venueVideo{opacity:1}
+ #stage.video-bg #sky{opacity:.20}
+ #stage.video-bg #scene{opacity:.66}
+ .btn:disabled,button:disabled{opacity:.42;cursor:not-allowed}
/* construction-paper grain — the cutout/South-Park texture over the whole scene */
.grain{position:fixed;inset:0;pointer-events:none;z-index:5;opacity:.10;mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 220 220' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
@@ -123,6 +131,8 @@
.btn[aria-pressed="true"]:hover{background:linear-gradient(180deg,rgba(242,182,74,.28),rgba(242,182,74,.12))}
.toggle{display:flex;flex-direction:column;gap:8px;align-items:stretch}
.toggle .btn{justify-content:flex-start} /* equal-width pills, left-aligned labels */
+ .toggle.pillrow{flex-direction:row;flex-wrap:wrap;align-items:center}
+ .toggle.pillrow .btn{justify-content:center}
/* ---- Camera pills ---- */
.cams{position:fixed;left:26px;bottom:20px;z-index:7;display:flex;gap:8px;flex-direction:column}
@@ -263,6 +273,7 @@
<h1 class="sr">Scarlet Riverboat Masquerade — an animated generative gallery</h1>
<!-- role removed from <main>: role="application" suppressed normal screen-reader navigation over the native controls + prose -->
+ <div id="venueVideo" aria-hidden="true"><div id="ytHost"></div></div>
<canvas id="sky" aria-hidden="true"></canvas>
<canvas id="scene" aria-hidden="true"></canvas>
<canvas id="orb" aria-hidden="true"></canvas>
@@ -326,7 +337,7 @@
<button class="btn" id="playBtn" aria-pressed="true">❚❚ Pause</button>
<button class="btn" id="restartBtn">↻ Restart</button>
</div>
- <div class="toggle">
+ <div class="toggle pillrow">
<button class="btn" id="reduceBtn" aria-pressed="false">◐ Reduced Motion</button>
<button class="btn" id="trailBtn" aria-pressed="true">✧ Motion Trails</button>
<button class="btn" id="reactBtn" aria-pressed="false">♫ React to Music</button>
@@ -1494,7 +1505,8 @@ chromeBtn.addEventListener('click', toggleChrome);
const venueBtn=document.getElementById('venueBtn');
venueBtn.addEventListener('click',()=>{
venueOn=!venueOn; venueBtn.setAttribute('aria-pressed',String(venueOn));
- announce(venueOn?'Venue backdrop on.':'Venue backdrop off.');
+ if(window.__vbVenue) window.__vbVenue(venueOn); // also toggle the concert-video backdrop (TK-11178)
+ announce(venueOn?'Concert-video backdrop on.':'Concert-video backdrop off.');
});
// keyboard shortcuts
@@ -1568,6 +1580,146 @@ const CATALOG = [
];
const DEFAULT_SHOW = 7; // Cornell '77
+/* ============ Concert-video backdrop + archive.org failover + resume (TK-11178) ============
+ archive.org (the Live Music Archive) is the ONLY host for the real SBD tapes — when it is
+ unreachable the room would go silent. This layer adds, WITHOUT re-hosting anything (YouTube
+ is an embed, archive.org is a stream, resume lives in the visitor's own browser):
+ • an era-matched OFFICIAL Grateful Dead concert video as the #stage backdrop during every
+ show (muted, behind the generative scene), and
+ • a YouTube fallback that BECOMES the audio when archive.org is down.
+ Video IDs verified embeddable via yt-dlp (playable_in_embed=true, age_limit=0, not-live). */
+const MR_VIDEOS=[
+ {id:'2lK8DS449j0',year:1969,title:'1/24/69 · San Francisco'},
+ {id:'Pg-_ok-FqOE',year:1970,title:'5/1/70 · Alfred College'},
+ {id:'qu5Ij4vFgLo',year:1971,title:'8/6/71 · Hollywood Palladium'},
+ {id:'K7k9rXwaxcs',year:1972,title:'7/18/72 · Roosevelt Stadium'},
+ {id:'fzZ8ruYGgLM',year:1973,title:'2/15/73 · Madison, WI'},
+ {id:'auYN5XQSXxI',year:1974,title:'2/24/74 · Winterland'},
+ {id:'nfVLeDXLvK4',year:1976,title:'NYE 1976 · Cow Palace'},
+ {id:'zs5Sd6nd3u0',year:1977,title:'5/11/77'},
+ {id:'A-CqDFfJ0Rc',year:1978,title:'Closing of Winterland · 12/31/78'},
+ {id:'aEmJlxYAxps',year:1980,title:'Dead Ahead · Oct 1980'},
+ {id:'qa6BmpOZNXc',year:1981,title:'10/16/81 · Amsterdam'},
+ {id:'MJ3iAA2QL60',year:1985,title:'4/7/85 · Philadelphia'},
+ {id:'lzuYS_I8dpo',year:1987,title:'Shoreline · 10/2/87'},
+ {id:'A8rVM8GlD4c',year:1989,title:'Truckin’ Up to Buffalo · 7/4/89'},
+ {id:'iR4_D6P6Hrc',year:1990,title:'Rich Stadium · 7/16/90'},
+ {id:'G9ESSXKBq48',year:1991,title:'10/31/91 · Oakland Coliseum'},
+ {id:'YDgqP_4Q_fM',year:1993,title:'Buckeye Lake · 6/11/93'},
+ {id:'sSPyP9qwkpA',year:1995,title:'Soldier Field · 1995 (final era)'}
+];
+function _yearOf(s){
+ const m=String((s&&s.date)||'').match(/\b(19\d{2})\b/); if(m) return +m[1];
+ const m2=String((s&&s.id)||'').match(/gd(\d{2,4})/i);
+ if(m2){ let y=+m2[1]; if(y<100) y=(y<=40?2000:1900)+y; return y; }
+ return null;
+}
+function pickVideoFor(s){
+ const y=_yearOf(s);
+ if(y==null) return MR_VIDEOS[Math.floor(MR_VIDEOS.length/2)];
+ let best=MR_VIDEOS[0], bd=1e9;
+ for(const v of MR_VIDEOS){ const d=Math.abs(v.year-y); if(d<bd){ bd=d; best=v; } }
+ return best;
+}
+
+// --- YouTube IFrame-API backed background player (loaded lazily; this host sets no CSP) ---
+const VB={ mode:'off', ready:false, player:null, curId:null, wantId:null, wantPlay:false, wantMuted:true, venueOn:true };
+const _stageEl=document.getElementById('stage');
+function _vbShow(){ if(VB.venueOn && VB.wantId) _stageEl.classList.add('video-bg'); }
+function _vbHide(){ _stageEl.classList.remove('video-bg'); }
+function _loadYTApi(){
+ if(window.YT && window.YT.Player){ VB.ready=true; _buildPlayer(); return; }
+ if(!document.getElementById('yt-iframe-api')){
+ const t=document.createElement('script'); t.id='yt-iframe-api';
+ t.src='https://www.youtube.com/iframe_api'; document.head.appendChild(t);
+ }
+}
+const _prevYTReady=window.onYouTubeIframeAPIReady;
+window.onYouTubeIframeAPIReady=function(){ if(typeof _prevYTReady==='function'){ try{_prevYTReady();}catch(e){} } VB.ready=true; _buildPlayer(); };
+function _buildPlayer(){
+ if(VB.player || !(window.YT && window.YT.Player)) return;
+ if(!document.getElementById('ytHost')) return;
+ VB.player=new YT.Player('ytHost',{
+ host:'https://www.youtube-nocookie.com',
+ playerVars:{playsinline:1,controls:0,disablekb:1,rel:0,modestbranding:1,iv_load_policy:3,fs:0,cc_load_policy:0},
+ events:{ onReady:()=>{ if(VB.wantId) _applyVideo(); }, onStateChange:_onPlayerState, onError:_onPlayerError }
+ });
+}
+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 _applyVideo(){
+ const p=VB.player; if(!p || !p.loadVideoById) return;
+ if(VB.wantMuted){ try{p.mute();}catch(e){} } else { try{p.unMute(); p.setVolume(100);}catch(e){} }
+ if(VB.wantId!==VB.curId){
+ VB.curId=VB.wantId;
+ if(VB.wantPlay) p.loadVideoById(VB.wantId); else p.cueVideoById(VB.wantId);
+ } else if(VB.wantPlay){ try{p.playVideo();}catch(e){} }
+}
+function vbSetVideo(show,opts){
+ opts=opts||{};
+ const v=pickVideoFor(show); if(!v) return;
+ 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();
+ _vbShow();
+}
+// browsers block UNMUTED autoplay without a gesture — arm a one-time tap/key handler to add sound
+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; };
+ 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.
+function enterArchiveMode(show){
+ VB.mode='archive';
+ if(typeof reactBtn!=='undefined' && reactBtn){ reactBtn.disabled=false; reactBtn.removeAttribute('title'); }
+ 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){
+ 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);
+ 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)||{};
+ $('mrNow').textContent='▶ '+(v.title?('Grateful Dead — '+v.title):'Grateful Dead (YouTube)');
+ $('mrSub').textContent='archive.org is unreachable — playing the nearest official Grateful Dead concert on YouTube.';
+ const msg=$('mrMsg');
+ if(msg){ msg.hidden=false;
+ msg.innerHTML='The Live Music Archive is unreachable right now, so the room is streaming the nearest official '+
+ 'Grateful Dead concert from YouTube — your exact tape returns when archive.org is back.'+
+ (hasAct?'':' Tap or press any key for sound.')+
+ ' <a href="https://www.youtube.com/watch?v='+VB.wantId+'" target="_blank" rel="noopener noreferrer">Watch on YouTube ↗</a>'; }
+ 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.');
+}
+function vbTogglePlay(){
+ const p=VB.player; if(!p || !window.YT) return;
+ const st=p.getPlayerState?p.getPlayerState():-1;
+ if(st===YT.PlayerState.PLAYING){ try{p.pauseVideo();}catch(e){} setPlay(false); }
+ else { try{ if(VB.mode==='youtube') p.unMute(); p.playVideo(); }catch(e){} setPlay(true); }
+}
+// --- resume where you left off (visitor's own browser only; wrapped so private mode can't throw) ---
+const _RKEY='deadroom.resume.v1'; let _rLast=0;
+function saveResume(){ try{
+ const now=Date.now(); if(now-_rLast<1000) return; _rLast=now;
+ localStorage.setItem(_RKEY, JSON.stringify({showIdx, trackIdx, t:Math.floor(audio.currentTime||0), mode:VB.mode}));
+}catch(e){} }
+function readResume(){ try{ return JSON.parse(localStorage.getItem(_RKEY)||'null'); }catch(e){ return null; } }
+/* ============ end TK-11178 backdrop/failover/resume subsystem ============ */
+
const $ = id => document.getElementById(id);
const audio = $('mrAudio'), room = $('musicroom'), sel = $('mrShow'), live = $('live');
let showIdx=-1, tracks=[], trackIdx=-1, seeking=false, loadToken=0;
@@ -1596,7 +1748,7 @@ async function loadShowRef(s, opts){
$('mrTracks').innerHTML=''; $('mrMsg').hidden=true; $('mrMsg').innerHTML='';
setFoot(s);
try{
- const r=await fetch(`https://archive.org/metadata/${s.id}`,{mode:'cors'});
+ const r=await fetchT(`https://archive.org/metadata/${s.id}`, 7000); // timeout so an archive.org outage fails fast → YouTube fallback (TK-11178)
if(!r.ok) throw new Error('metadata '+r.status);
const j=await r.json();
if(myToken!==loadToken) return; // a newer show was selected while we awaited — drop this result
@@ -1615,10 +1767,11 @@ async function loadShowRef(s, opts){
highlight();
$('mrDur').textContent='0:00'; $('mrCur').textContent='0:00'; $('mrSeek').value='0';
announce(`Loaded ${s.title}. ${tracks.length} tracks from the Live Music Archive.`);
+ enterArchiveMode(s); // muted era-matched concert video behind the live SBD audio
if(opts.play) playTrack(0);
}catch(e){
if(myToken!==loadToken) return; // superseded — let the newer selection own the UI
- fallbackEmbed(s);
+ enterYouTubeFallback(s); // archive.org unreachable → the official YouTube concert becomes the audio
}
}
async function loadShow(i, opts){ if(sel) sel.value=String(i); return loadShowRef(Object.assign({venueIdx:i}, CATALOG[i]), opts); }
@@ -1802,13 +1955,21 @@ 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){ fallbackEmbed(CATALOG[showIdx]); return; }
+ if(!tracks.length){ enterYouTubeFallback(CATALOG[showIdx]); return; }
// 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.'; }
});
+// --- keep the muted concert backdrop in step with the SBD audio (archive mode only) + resume + venue bridge ---
+audio.addEventListener('play', ()=>{ if(VB.mode==='archive' && VB.player && VB.player.playVideo){ try{ VB.player.mute(); VB.player.playVideo(); }catch(e){} } });
+audio.addEventListener('pause',()=>{ if(VB.mode==='archive' && VB.player && VB.player.pauseVideo){ try{ VB.player.pauseVideo(); }catch(e){} } });
+audio.addEventListener('timeupdate', saveResume);
+audio.addEventListener('pause', saveResume);
+window.__vbVenue=function(on){ VB.venueOn=!!on; if(VB.venueOn){ _vbShow(); } else { _vbHide(); } };
+
$('mrPlay').addEventListener('click',()=>{
+ if(VB.mode==='youtube'){ vbTogglePlay(); return; } // in fallback the YouTube video IS the audio
if(!tracks.length){ loadShow(showIdx<0?DEFAULT_SHOW:showIdx,{play:true}); return; }
if(trackIdx<0){ playTrack(0); return; }
if(audio.paused) audio.play().catch(()=>{}); else audio.pause();
@@ -1935,7 +2096,7 @@ function _matches(title,want){ const n=_norm(title), w=_norm(want); if(!n||!w) r
// fan out to the curated shows in parallel, pool every mp3 track (title + direct url)
async function _poolFromShows(){
const results=await Promise.allSettled(CATALOG.map(s=>
- fetch(`https://archive.org/metadata/${s.id}`,{mode:'cors'}).then(r=>r.ok?r.json():null).then(j=>({s,j}))));
+ fetchT(`https://archive.org/metadata/${s.id}`, 6000).then(r=>r.ok?r.json():null).then(j=>({s,j})))); // timed so boot can't hang on an archive.org outage (TK-11178)
const pool=[];
for(const res of results){
if(res.status!=='fulfilled' || !res.value || !res.value.j) continue;
@@ -1957,6 +2118,19 @@ function _tryAutoplay(){
});
}
async function bootMusic(){
+ // Returning visitor → resume their last show (cued; playback stays user-initiated per the listen-only ethos).
+ // First-ever visit (or blocked storage) → fall through to the curated autostart set below.
+ const _R=readResume();
+ if(_R && Number.isInteger(_R.showIdx) && _R.showIdx>=0 && _R.showIdx<CATALOG.length){
+ $('mrNow').textContent='Resuming your last show…';
+ await loadShow(_R.showIdx); // archive down → this cues the YouTube fallback for that era
+ if(tracks.length && _R.trackIdx>0 && _R.trackIdx<tracks.length){
+ 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();
+ return;
+ }
$('mrNow').textContent='Cueing the set…';
try{
const pool=await _poolFromShows();
@@ -1967,6 +2141,7 @@ async function bootMusic(){
$('mrNow').textContent=`♪ ${tracks[0].title}`;
$('mrSub').textContent='Autostart — '+list.map(t=>t.title).join(' · ');
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();
return;
← f3c92ee docs(TK-11100): Option A patch artifact for the scarlet-rive
·
back to Dead Agentabrams
·
Put all dock pills (Pause/Restart + the 5 mode toggles) on o 339f860 →