piclez commited on
Commit
8e7fabd
·
1 Parent(s): 2ce13a1

fix: prime audio playback on mobile to unblock HAL voice reply

Browse files
Files changed (1) hide show
  1. static/index.html +44 -4
static/index.html CHANGED
@@ -106,6 +106,29 @@
106
  let recorder = null;
107
  let chunks = [];
108
  let busy = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  function setState(state) {
111
  eye.classList.remove('listening', 'thinking', 'speaking');
@@ -175,11 +198,28 @@
175
  updateLog(userText, halText);
176
 
177
  const audioBlob = await res.blob();
178
- const audio = new Audio(URL.createObjectURL(audioBlob));
 
 
 
 
 
179
  audio.onended = () => { setState('idle'); busy = false; };
180
  audio.onerror = () => { setState('idle'); busy = false; };
181
  setState('speaking');
182
- audio.play();
 
 
 
 
 
 
 
 
 
 
 
 
183
  } catch (err) {
184
  console.error('request error', err);
185
  setState('idle');
@@ -187,10 +227,10 @@
187
  }
188
  }
189
 
190
- eye.addEventListener('mousedown', e => { e.preventDefault(); startRecording(); });
191
  eye.addEventListener('mouseup', e => { e.preventDefault(); stopAndSend(); });
192
  eye.addEventListener('mouseleave', () => { if (recorder && recorder.state === 'recording') stopAndSend(); });
193
- eye.addEventListener('touchstart', e => { e.preventDefault(); startRecording(); }, { passive: false });
194
  eye.addEventListener('touchend', e => { e.preventDefault(); stopAndSend(); }, { passive: false });
195
  eye.addEventListener('touchcancel', e => { e.preventDefault(); stopAndSend(); }, { passive: false });
196
  eye.addEventListener('contextmenu', e => e.preventDefault());
 
106
  let recorder = null;
107
  let chunks = [];
108
  let busy = false;
109
+ let audioCtx = null;
110
+ let primedAudio = null;
111
+
112
+ // iOS/Safari require audio playback to be initiated from a user gesture.
113
+ // Prime an <audio> element and an AudioContext on first touch so later
114
+ // programmatic play() calls are allowed.
115
+ function primePlayback() {
116
+ try {
117
+ if (!audioCtx) {
118
+ const Ctx = window.AudioContext || window.webkitAudioContext;
119
+ if (Ctx) audioCtx = new Ctx();
120
+ }
121
+ if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
122
+ if (!primedAudio) {
123
+ primedAudio = new Audio();
124
+ primedAudio.playsInline = true;
125
+ primedAudio.setAttribute('playsinline', '');
126
+ primedAudio.muted = true;
127
+ primedAudio.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=';
128
+ primedAudio.play().catch(() => {});
129
+ }
130
+ } catch (e) {}
131
+ }
132
 
133
  function setState(state) {
134
  eye.classList.remove('listening', 'thinking', 'speaking');
 
198
  updateLog(userText, halText);
199
 
200
  const audioBlob = await res.blob();
201
+ const url = URL.createObjectURL(audioBlob);
202
+ const audio = primedAudio || new Audio();
203
+ audio.muted = false;
204
+ audio.playsInline = true;
205
+ audio.setAttribute('playsinline', '');
206
+ audio.src = url;
207
  audio.onended = () => { setState('idle'); busy = false; };
208
  audio.onerror = () => { setState('idle'); busy = false; };
209
  setState('speaking');
210
+ try {
211
+ await audio.play();
212
+ } catch (err) {
213
+ console.error('playback blocked', err);
214
+ log.innerHTML += '<div class="hal">[tap eye to hear reply]</div>';
215
+ const resume = async () => {
216
+ eye.removeEventListener('touchend', resume);
217
+ eye.removeEventListener('click', resume);
218
+ try { await audio.play(); } catch (e) { setState('idle'); busy = false; }
219
+ };
220
+ eye.addEventListener('touchend', resume, { once: true });
221
+ eye.addEventListener('click', resume, { once: true });
222
+ }
223
  } catch (err) {
224
  console.error('request error', err);
225
  setState('idle');
 
227
  }
228
  }
229
 
230
+ eye.addEventListener('mousedown', e => { e.preventDefault(); primePlayback(); startRecording(); });
231
  eye.addEventListener('mouseup', e => { e.preventDefault(); stopAndSend(); });
232
  eye.addEventListener('mouseleave', () => { if (recorder && recorder.state === 'recording') stopAndSend(); });
233
+ eye.addEventListener('touchstart', e => { e.preventDefault(); primePlayback(); startRecording(); }, { passive: false });
234
  eye.addEventListener('touchend', e => { e.preventDefault(); stopAndSend(); }, { passive: false });
235
  eye.addEventListener('touchcancel', e => { e.preventDefault(); stopAndSend(); }, { passive: false });
236
  eye.addEventListener('contextmenu', e => e.preventDefault());