Fix double audio playback and add UID handling for personal stream

- Fixed double playback issue on stream page by properly scoping event delegation in streams-ui.js
- Added init-personal-stream.js to handle UID for personal stream playback
- Improved error handling and logging for audio playback
- Added proper event propagation control to prevent duplicate event handling
This commit is contained in:
oib
2025-07-18 16:51:39 +02:00
parent 17616ac5b8
commit 402e920bc6
24 changed files with 4074 additions and 1090 deletions

View File

@ -65,17 +65,26 @@ function loadAndRenderStreams() {
let streams = [];
let connectionTimeout = null;
// Close previous connection and clear any pending timeouts
// Clean up previous connection and timeouts
if (window._streamsSSE) {
console.log('[streams-ui] Aborting previous connection');
console.group('[streams-ui] Cleaning up previous connection');
console.log('Previous connection exists, aborting...');
if (window._streamsSSE.abort) {
window._streamsSSE.abort();
console.log('Previous connection aborted');
} else {
console.log('No abort method on previous connection');
}
window._streamsSSE = null;
console.groupEnd();
}
if (connectionTimeout) {
console.log('[streams-ui] Clearing previous connection timeout');
clearTimeout(connectionTimeout);
connectionTimeout = null;
} else {
console.log('[streams-ui] No previous connection timeout to clear');
}
console.log(`[streams-ui] Creating fetch-based SSE connection to ${sseUrl}`);
@ -87,14 +96,41 @@ function loadAndRenderStreams() {
// Store the controller for cleanup
window._streamsSSE = controller;
// Set a connection timeout
connectionTimeout = setTimeout(() => {
// Set a connection timeout with debug info
const connectionStartTime = Date.now();
const connectionTimeoutId = setTimeout(() => {
if (!gotAny) {
console.log('[streams-ui] Connection timeout reached, forcing retry...');
// Only log in development (localhost) or if explicitly enabled
const isLocalDevelopment = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
if (isLocalDevelopment || window.DEBUG_STREAMS) {
const duration = Date.now() - connectionStartTime;
console.group('[streams-ui] Connection timeout reached');
console.log(`Duration: ${duration}ms`);
console.log('Current time:', new Date().toISOString());
console.log('Streams received:', streams.length);
console.log('Active intervals:', window.activeIntervals ? window.activeIntervals.size : 'N/A');
console.log('Active timeouts:', window.activeTimeouts ? window.activeTimeouts.size : 'N/A');
console.groupEnd();
}
// Clean up and retry with backoff
controller.abort();
loadAndRenderStreams();
// Only retry if we haven't exceeded max retries
const retryCount = window.streamRetryCount || 0;
if (retryCount < 3) { // Max 3 retries
window.streamRetryCount = retryCount + 1;
const backoffTime = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
setTimeout(loadAndRenderStreams, backoffTime);
} else if (process.env.NODE_ENV === 'development' || window.DEBUG_STREAMS) {
console.warn('Max retries reached for stream loading');
}
}
}, 10000); // 10 second timeout
}, 15000); // 15 second timeout (increased from 10s)
// Store the timeout ID for cleanup
connectionTimeout = connectionTimeoutId;
console.log('[streams-ui] Making fetch request to:', sseUrl);
@ -102,7 +138,7 @@ function loadAndRenderStreams() {
console.log('[streams-ui] Creating fetch request with URL:', sseUrl);
// Make the fetch request
// Make the fetch request with proper error handling
fetch(sseUrl, {
method: 'GET',
headers: {
@ -112,7 +148,6 @@ function loadAndRenderStreams() {
},
credentials: 'same-origin',
signal: signal,
// Add mode and redirect options for better error handling
mode: 'cors',
redirect: 'follow'
})
@ -200,44 +235,49 @@ function loadAndRenderStreams() {
return reader.read().then(processStream);
})
.catch(error => {
console.error('[streams-ui] Fetch request failed:', error);
// Only handle the error if it's not an AbortError (from our own abort)
if (error.name === 'AbortError') {
console.log('[streams-ui] Request was aborted as expected');
return;
}
console.error('[streams-ui] Stream loading failed:', error);
// Log additional error details
if (error.name === 'TypeError') {
console.error('[streams-ui] This is likely a network error or CORS issue');
if (error.message.includes('fetch')) {
console.error('[streams-ui] The fetch request was blocked or failed to reach the server');
}
if (error.message.includes('CORS')) {
console.error('[streams-ui] CORS error detected. Check server CORS configuration');
}
}
if (error.name === 'AbortError') {
console.log('[streams-ui] Request was aborted');
} else {
console.error('[streams-ui] Error details:', {
name: error.name,
message: error.message,
stack: error.stack,
constructor: error.constructor.name,
errorCode: error.code,
errorNumber: error.errno,
response: error.response
});
// Show a user-friendly error message
const ul = document.getElementById('stream-list');
if (ul) {
let errorMessage = 'Error loading streams. ';
// Show a user-friendly error message
const ul = document.getElementById('stream-list');
if (ul) {
ul.innerHTML = `
<li class="error">
<p>Error loading streams. Please try again later.</p>
<p><small>Technical details: ${error.name}: ${error.message}</small></p>
</li>
`;
if (error.message.includes('Failed to fetch')) {
errorMessage += 'Unable to connect to the server. Please check your internet connection.';
} else if (error.message.includes('CORS')) {
errorMessage += 'A server configuration issue occurred. Please try again later.';
} else {
errorMessage += 'Please try again later.';
}
handleSSEError(error);
ul.innerHTML = `
<li class="error">
<p>${errorMessage}</p>
<button id="retry-loading" class="retry-button">
<span class="retry-icon">↻</span> Try Again
</button>
</li>
`;
// Add retry handler
const retryButton = document.getElementById('retry-loading');
if (retryButton) {
retryButton.addEventListener('click', () => {
ul.innerHTML = '<li>Loading streams...</li>';
loadAndRenderStreams();
});
}
}
});
@ -279,7 +319,7 @@ function loadAndRenderStreams() {
<div class="audio-controls">
<button class="play-pause-btn" data-uid="${escapeHtml(uid)}" aria-label="Play">▶️</button>
</div>
<p class="stream-info" style='color:gray;font-size:90%'>[${sizeMb} MB, ${mtime}]</p>
<p class="stream-info" style='color:var(--text-muted);font-size:90%'>[${sizeMb} MB, ${mtime}]</p>
</article>
`;
ul.appendChild(li);
@ -288,7 +328,7 @@ function loadAndRenderStreams() {
console.error(`[streams-ui] Error rendering stream ${uid}:`, error);
const errorLi = document.createElement('li');
errorLi.textContent = `Error loading stream: ${uid}`;
errorLi.style.color = 'red';
errorLi.style.color = 'var(--error)';
ul.appendChild(errorLi);
}
});
@ -352,7 +392,7 @@ export function renderStreamList(streams) {
const uid = stream.uid || '';
const sizeKb = stream.size ? (stream.size / 1024).toFixed(1) : '?';
const mtime = stream.mtime ? new Date(stream.mtime * 1000).toLocaleString() : '';
return `<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${uid}</a> <span style='color:gray;font-size:90%'>[${sizeKb} KB, ${mtime}]</span></li>`;
return `<li><a href="/?profile=${encodeURIComponent(uid)}" class="profile-link">▶ ${uid}</a> <span style='color:var(--text-muted);font-size:90%'>[${sizeKb} KB, ${mtime}]</span></li>`;
})
.join('');
} else {
@ -704,50 +744,55 @@ function cleanupAudio() {
}
}
// Event delegation for play/pause buttons
document.addEventListener('click', async (e) => {
const playPauseBtn = e.target.closest('.play-pause-btn');
if (!playPauseBtn) return;
// Event delegation for play/pause buttons - only handle buttons within the stream list
const streamList = document.getElementById('stream-list');
if (streamList) {
streamList.addEventListener('click', async (e) => {
const playPauseBtn = e.target.closest('.play-pause-btn');
// Skip if not a play button or if it's the personal stream's play button
if (!playPauseBtn || playPauseBtn.closest('#me-page')) return;
// Prevent event from bubbling up to document-level handlers
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
// Prevent default to avoid any potential form submission or link following
e.preventDefault();
e.stopPropagation();
const uid = playPauseBtn.dataset.uid;
if (!uid) {
console.error('No UID found for play button');
return;
}
console.log(`[streams-ui] Play/pause clicked for UID: ${uid}, currentUid: ${currentUid}, isPlaying: ${isPlaying}`);
// If clicking the currently playing button, toggle pause/play
if (currentUid === uid) {
if (isPlaying) {
console.log('[streams-ui] Pausing current audio');
await audioElement.pause();
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
} else {
console.log('[streams-ui] Resuming current audio');
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
} catch (error) {
console.error('[streams-ui] Error resuming audio:', error);
// If resume fails, try reloading the audio
await loadAndPlayAudio(uid, playPauseBtn);
}
const uid = playPauseBtn.dataset.uid;
if (!uid) {
console.error('No UID found for play button');
return;
}
return;
}
// If a different stream is playing, stop it and start the new one
console.log(`[streams-ui] Switching to new audio stream: ${uid}`);
stopPlayback();
await loadAndPlayAudio(uid, playPauseBtn);
});
console.log(`[streams-ui] Play/pause clicked for UID: ${uid}, currentUid: ${currentUid}, isPlaying: ${isPlaying}`);
// If clicking the currently playing button, toggle pause/play
if (currentUid === uid) {
if (isPlaying) {
console.log('[streams-ui] Pausing current audio');
await audioElement.pause();
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
} else {
console.log('[streams-ui] Resuming current audio');
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
} catch (error) {
console.error('[streams-ui] Error resuming audio:', error);
// If resume fails, try reloading the audio
await loadAndPlayAudio(uid, playPauseBtn);
}
}
return;
}
// If a different stream is playing, stop it and start the new one
console.log(`[streams-ui] Switching to new audio stream: ${uid}`);
stopPlayback();
await loadAndPlayAudio(uid, playPauseBtn);
});
}
// Handle audio end event to update button state
document.addEventListener('play', (e) => {