This commit is contained in:
2026-01-03 14:02:07 +00:00
parent c08ee70fe8
commit 111f4b347e
2 changed files with 86 additions and 30 deletions

116
script.js
View File

@@ -2180,35 +2180,61 @@ function initListenerMode() {
// AudioContext will be created when user enables audio to avoid suspension
// Create or reuse audio element to handle the MediaSource
// ALWAYS create a fresh audio element to avoid MediaSource/MediaElementSource conflicts
// This is critical for page refreshes - you can only create MediaElementSource once per element
let audio;
// Clean up old audio element if it exists
if (window.listenerAudio) {
// Reuse existing audio element from previous initialization
audio = window.listenerAudio;
console.log('♻️ Reusing existing audio element');
// Clean up old MediaSource if it exists
if (audio.src) {
URL.revokeObjectURL(audio.src);
audio.removeAttribute('src');
audio.load(); // Reset the element
console.log('🧹 Cleaning up old audio element and AudioContext nodes');
try {
window.listenerAudio.pause();
if (window.listenerAudio.src) {
URL.revokeObjectURL(window.listenerAudio.src);
}
window.listenerAudio.removeAttribute('src');
window.listenerAudio.remove(); // Remove from DOM
} catch (e) {
console.warn('Error cleaning up old audio:', e);
}
} else {
// Create a new hidden media element.
// Note: MSE (MediaSource) support is often more reliable on <video> than <audio>.
audio = document.createElement('video');
audio.autoplay = false; // Don't autoplay - we use the Enable Audio button
audio.muted = false;
audio.controls = false;
audio.playsInline = true;
audio.setAttribute('playsinline', '');
audio.style.display = 'none';
document.body.appendChild(audio);
console.log('🆕 Created new media element (video) for listener');
// AudioContext will be created later on user interaction
// Reset all AudioContext-related nodes
if (listenerMediaElementSourceNode) {
try {
listenerMediaElementSourceNode.disconnect();
} catch (e) { }
listenerMediaElementSourceNode = null;
}
if (listenerAnalyserNode) {
try {
listenerAnalyserNode.disconnect();
} catch (e) { }
listenerAnalyserNode = null;
}
if (listenerGainNode) {
try {
listenerGainNode.disconnect();
} catch (e) { }
listenerGainNode = null;
}
window.listenerAudio = null;
window.listenerMediaSource = null;
window.listenerAudioEnabled = false;
}
// Create a new hidden media element.
// Note: MSE (MediaSource) support is often more reliable on <video> than <audio>.
audio = document.createElement('video');
audio.autoplay = false; // Don't autoplay - we use the Enable Audio button
audio.muted = false;
audio.controls = false;
audio.playsInline = true;
audio.setAttribute('playsinline', '');
audio.style.display = 'none';
document.body.appendChild(audio);
console.log('🆕 Created fresh media element (video) for listener');
// Initialize MediaSource for streaming binary chunks
const mediaSource = new MediaSource();
audio.src = URL.createObjectURL(mediaSource);
@@ -2227,7 +2253,7 @@ function initListenerMode() {
mediaSource.addEventListener('sourceopen', () => {
console.log('📦 MediaSource opened');
const mimeType = window.currentStreamMimeType || currentStreamMimeType || 'audio/webm;codecs=opus';
if (!MediaSource.isTypeSupported(mimeType)) {
console.error(`❌ Browser does not support ${mimeType}`);
const statusEl = document.getElementById('connection-status');
@@ -2470,8 +2496,38 @@ async function enableListenerAudio() {
return window.listenerAudio.buffered && window.listenerAudio.buffered.length > 0;
};
// Attempt playback IMMEDIATELY to capture user gesture
// We do this before waiting for data so we don't lose the "user interaction" token
// CRITICAL: Wait for buffered data before calling play()
// This prevents NotSupportedError when buffer is empty
if (!hasBufferedData()) {
console.log('⏳ Waiting for audio data to buffer before playback...');
if (audioText) audioText.textContent = 'BUFFERING...';
// Wait for data with timeout (max 5 seconds)
const waitForData = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('Timeout waiting for audio data'));
}, 5000);
const checkInterval = setInterval(() => {
if (hasBufferedData()) {
clearInterval(checkInterval);
clearTimeout(timeout);
console.log('✅ Audio data buffered, ready to play');
resolve();
}
}, 100);
});
try {
await waitForData;
} catch (e) {
console.warn('⚠️ Timeout waiting for buffer data:', e.message);
}
} else {
console.log('✅ Audio already has buffered data');
}
console.log('▶️ Attempting to play audio...');
const playPromise = window.listenerAudio.play();
@@ -2482,13 +2538,13 @@ async function enableListenerAudio() {
if (audioText) {
audioText.textContent = chunkCount > 0 ? 'BUFFERING...' : 'WAITING FOR STREAM...';
}
// Start a background checker to update UI
const checkInterval = setInterval(() => {
if (hasBufferedData()) {
clearInterval(checkInterval);
console.log('✅ Audio data buffered');
const chunkCount = Number.isFinite(listenerChunksReceived) ? listenerChunksReceived : 0;
const chunkCount = Number.isFinite(listenerChunksReceived) ? listenerChunksReceived : 0;
} else if (audioText && chunkCount > 0 && audioText.textContent === 'WAITING FOR STREAM...') {
audioText.textContent = 'BUFFERING...';
}
@@ -2532,9 +2588,9 @@ async function enableListenerAudio() {
} else if (error.name === 'NotSupportedError') {
errorMsg = 'Format not supported or buffer empty (NotSupportedError).';
}
stashedStatus.textContent = '⚠️ ' + errorMsg;
if (error.name === 'NotSupportedError') {
// Two common causes:
// 1) WebM/Opus MSE isn't supported by this browser