Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

bios: retry stream:play without crossOrigin if CORS blocks load

The KPBJ stream (stream.kpbj.fm — vanilla Icecast 2.4.4) doesn't send
Access-Control-Allow-Origin headers, so setting audio.crossOrigin =
"anonymous" caused Chrome to reject the load with NotSupportedError and
the "no supported source was found" CORS failure seen in chat.

Keep trying with CORS first (r8dio via radio.co supports it, and we want
the analyser hooked up for frequency bars). If that attempt rejects, tear
down the tainted <audio> element and retry without crossOrigin so the
stream still plays — the visualizer gracefully falls back to the synthetic
wave+noise animation already wired up in chat.mjs and lib/radio.mjs.

Also cache-bust the URL with ?t=<now> on every play, matching the
approach used by kpbj.fm's own navbar player to avoid stale buffered
data on reconnect.

+63 -29
+63 -29
system/public/aesthetic.computer/bios.mjs
··· 16540 16540 // Start playing a streaming audio URL 16541 16541 if (type === "stream:play") { 16542 16542 const { id, url, volume } = content; 16543 - 16543 + 16544 16544 // Stop any existing stream with this id 16545 16545 if (streamAudio[id]) { 16546 16546 streamAudio[id].audio.pause(); 16547 16547 streamAudio[id].audio.src = ""; 16548 16548 delete streamAudio[id]; 16549 16549 } 16550 - 16551 - const audio = new Audio(); 16552 - audio.crossOrigin = "anonymous"; 16553 - audio.src = url; 16550 + 16554 16551 const baseVolume = clampVolume(volume ?? 1); 16555 - audio.volume = baseVolume * masterVolume; 16556 - 16557 - // Connect to AudioContext for analysis if available 16558 - if (audioContext) { 16559 - try { 16560 - const source = audioContext.createMediaElementSource(audio); 16561 - const analyser = audioContext.createAnalyser(); 16562 - analyser.fftSize = 256; 16563 - source.connect(analyser); 16564 - analyser.connect(audioContext.destination); 16565 - streamAudio[id] = { audio, analyser, source, baseVolume }; 16566 - } catch (e) { 16567 - // If already connected, just store the audio 16568 - streamAudio[id] = { audio, baseVolume }; 16552 + 16553 + // Cache-bust the stream URL to force a fresh connection and avoid stale 16554 + // buffered data. Matches the approach used by kpbj.fm's own navbar player. 16555 + const bustedUrl = url + (url.includes("?") ? "&" : "?") + "t=" + Date.now(); 16556 + 16557 + // Try with CORS first so we can hook up the analyser (for r8dio/radio.co 16558 + // which sends Access-Control-Allow-Origin). If the server doesn't support 16559 + // CORS (e.g. vanilla Icecast like stream.kpbj.fm), the browser blocks the 16560 + // load — retry once without crossOrigin so the stream still plays (sans 16561 + // frequency analyser; the visualizer falls back to a synthetic waveform). 16562 + const tryPlay = (withCors) => { 16563 + const audio = new Audio(); 16564 + if (withCors) audio.crossOrigin = "anonymous"; 16565 + audio.src = bustedUrl; 16566 + audio.volume = baseVolume * masterVolume; 16567 + 16568 + const entry = { audio, baseVolume }; 16569 + 16570 + if (withCors && audioContext) { 16571 + try { 16572 + const source = audioContext.createMediaElementSource(audio); 16573 + const analyser = audioContext.createAnalyser(); 16574 + analyser.fftSize = 256; 16575 + source.connect(analyser); 16576 + analyser.connect(audioContext.destination); 16577 + entry.analyser = analyser; 16578 + entry.source = source; 16579 + } catch (e) { 16580 + // createMediaElementSource throws if the element is already hooked 16581 + // up elsewhere; fall through and keep the plain audio entry. 16582 + } 16569 16583 } 16570 - } else { 16571 - streamAudio[id] = { audio, baseVolume }; 16572 - } 16573 - 16574 - audio.play().then(() => { 16575 - send({ type: "stream:playing", content: { id } }); 16576 - }).catch(err => { 16577 - console.warn("🎵 Stream play failed:", err); 16578 - send({ type: "stream:error", content: { id, error: err.message } }); 16584 + 16585 + streamAudio[id] = entry; 16586 + 16587 + return audio.play().then(() => { 16588 + send({ type: "stream:playing", content: { id } }); 16589 + }); 16590 + }; 16591 + 16592 + tryPlay(true).catch((err) => { 16593 + // CORS rejection surfaces as MediaError code 4 / NotSupportedError. 16594 + // Tear down the tainted audio element and retry without crossOrigin. 16595 + console.warn( 16596 + "🎵 Stream play with CORS failed, retrying without:", 16597 + err?.message || err, 16598 + ); 16599 + if (streamAudio[id]) { 16600 + try { 16601 + streamAudio[id].audio.pause(); 16602 + streamAudio[id].audio.src = ""; 16603 + } catch (_) {} 16604 + delete streamAudio[id]; 16605 + } 16606 + tryPlay(false).catch((err2) => { 16607 + console.warn("🎵 Stream play failed:", err2); 16608 + send({ 16609 + type: "stream:error", 16610 + content: { id, error: err2.message }, 16611 + }); 16612 + }); 16579 16613 }); 16580 - 16614 + 16581 16615 return; 16582 16616 } 16583 16617