[object Object]

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