← back to Butlr
live page: harden audio — auto-listen on appear, resume ctx per frame, red MUTED banner when audio arrives locked
4997af2e21eb55f5c1c4947e841c09e623dcb24f · 2026-05-26 10:41:22 -0700 · SteveStudio2
Files touched
Diff
commit 4997af2e21eb55f5c1c4947e841c09e623dcb24f
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 26 10:41:22 2026 -0700
live page: harden audio — auto-listen on appear, resume ctx per frame, red MUTED banner when audio arrives locked
---
routes/live.js | 49 +++++++++++++++++++++++++++++++++++++------------
1 file changed, 37 insertions(+), 12 deletions(-)
diff --git a/routes/live.js b/routes/live.js
index 2cfdfc3..23eca01 100644
--- a/routes/live.js
+++ b/routes/live.js
@@ -176,7 +176,7 @@ function renderLive(calls) {
<span class="count"><span id="liveCount">${live.length}</span> active</span>
<span class="sub" style="margin-left:auto">auto-refresh 3s · click 🔊 Listen to hear live (no page reload)</span>
</header>
- <div id="audio-unlock-banner" onclick="unlockAudio()" style="background:#22c55e;color:#022;text-align:center;padding:10px;font-weight:700;cursor:pointer;font-size:14px;">🔊 CLICK HERE ONCE TO ENABLE SOUND — chime on every new call + live audio plays automatically as calls connect</div>
+ <div id="audio-unlock-banner" onclick="unlockAudio()" style="position:sticky;top:0;z-index:50;background:#22c55e;color:#022;text-align:center;padding:10px;font-weight:700;cursor:pointer;font-size:14px;">🔊 CLICK HERE ONCE TO ENABLE SOUND — chime on every new call + live audio plays automatically as calls connect</div>
<div style="display:none">
</div>
<main id="grid">${cardsHtml}</main>
@@ -228,6 +228,23 @@ function renderLive(calls) {
document.addEventListener('click', unlockAudio, { once: false });
document.addEventListener('keydown', unlockAudio, { once: false });
+ // Re-surface the unlock banner (loudly) when live audio is arriving but the
+ // browser is still muted — the only thing standing between Steve and sound
+ // is one click. Throttled so it doesn't thrash on every frame.
+ let _bannerFlashTs = 0;
+ function flashUnlockBanner() {
+ const now = Date.now();
+ if (now - _bannerFlashTs < 1000) return;
+ _bannerFlashTs = now;
+ const banner = document.getElementById('audio-unlock-banner');
+ if (!banner) return;
+ banner.style.display = 'block';
+ banner.textContent = '🔇 LIVE AUDIO IS PLAYING BUT YOUR BROWSER IS MUTED — CLICK ANYWHERE TO HEAR IT';
+ banner.style.background = '#ef4444';
+ banner.style.color = '#fff';
+ banner.style.animation = 'pulse 1s infinite';
+ }
+
// Two-tone ascending chime (no audio asset needed) — fired whenever a NEW
// call appears in the grid so Steve hears "a call is being made" even before
// the called party picks up (the live stream is silent until they speak).
@@ -288,6 +305,12 @@ function renderLive(calls) {
stateEl.className = 'ws-state err';
};
ws.onmessage = (ev) => {
+ // Audio is arriving. If the context is suspended (autoplay policy) or the
+ // user never clicked to unlock, the buffers play SILENTLY — that's the
+ // "I heard nothing" bug. Force a resume every frame, and if still locked,
+ // surface the unlock banner loudly so one click turns sound on.
+ if (audioCtx.state === 'suspended') { try { audioCtx.resume(); } catch (e) {} }
+ if (!audioUnlocked || audioCtx.state === 'suspended') flashUnlockBanner();
const pcm16 = new Int16Array(ev.data);
const f32 = new Float32Array(pcm16.length);
for (let i = 0; i < pcm16.length; i++) f32[i] = pcm16[i] / 32768;
@@ -300,8 +323,9 @@ function renderLive(calls) {
src.start(startAt);
nextPlayTime = startAt + buf.duration;
frameCount++;
- if (frameCount % 20 === 0) {
- stateEl.textContent = '● live · ' + frameCount + ' frames';
+ if (frameCount === 1 || frameCount % 20 === 0) {
+ stateEl.textContent = '● LIVE · ' + frameCount + ' frames';
+ stateEl.className = 'ws-state active';
}
};
@@ -383,15 +407,16 @@ function renderLive(calls) {
const html = (${cardHtml.toString()})(c);
if (existing) existing.outerHTML = html;
else grid.insertAdjacentHTML('beforeend', html);
- // AUTO-PLAY ON CONNECT — if the call just transitioned to connected/on_hold
- // (the called party picked up), start listening automatically.
- if (c.status === 'connected' || c.status === 'on_hold') {
- if (!sessions.has(c.id)) {
- const btn = grid.querySelector('.card[data-id="' + c.id + '"] .listen-btn');
- const stateEl = grid.querySelector('.card[data-id="' + c.id + '"] .ws-state');
- const sr = parseInt(btn && btn.dataset.sr, 10) || 8000;
- if (btn && stateEl) startListen(c.id, sr, btn, stateEl);
- }
+ // AUTO-LISTEN — open the WS the instant the call appears (any live
+ // status), not just on connect. The connected window can be short
+ // (voicemail + AMD hangup), so opening early guarantees we catch the
+ // audio the moment frames start. No frames flow until the stream
+ // begins, so connecting during dialing is harmless.
+ if (!sessions.has(c.id)) {
+ const btn = grid.querySelector('.card[data-id="' + c.id + '"] .listen-btn');
+ const stateEl = grid.querySelector('.card[data-id="' + c.id + '"] .ws-state');
+ const sr = parseInt(btn && btn.dataset.sr, 10) || 8000;
+ if (btn && stateEl) startListen(c.id, sr, btn, stateEl);
}
});
if (calls.length === 0 && sessions.size === 0) {
← 6e3d4f7 butlr: recording disclosure to 'being recorded for Steve'
·
back to Butlr
·
butlr: sequential Ventura-Blvd hours sweep dialer (one call 11722b0 →