// static/streams-ui.js — public streams loader and profile-link handling
import { globalAudioManager } from './global-audio-manager.js';
// Global variable to track if we should force refresh the stream list
let shouldForceRefresh = false;
// Function to refresh the stream list
window.refreshStreamList = function(force = true) {
shouldForceRefresh = force;
loadAndRenderStreams();
return new Promise((resolve) => {
// Resolve after a short delay to allow the stream list to update
setTimeout(resolve, 500);
});
};
// Removed loadingStreams and lastStreamsPageVisible guards for instant fetch
export function initStreamsUI() {
initStreamLinks();
window.addEventListener('popstate', () => {
highlightActiveProfileLink();
maybeLoadStreamsOnShow();
});
document.addEventListener('visibilitychange', maybeLoadStreamsOnShow);
maybeLoadStreamsOnShow();
// Register with global audio manager to handle stop requests from other players
globalAudioManager.addListener('streams', () => {
// Debug messages disabled
stopPlayback();
});
}
function maybeLoadStreamsOnShow() {
// Expose globally for nav.js
const streamPage = document.getElementById('stream-page');
const isVisible = streamPage && !streamPage.hidden;
if (isVisible) {
loadAndRenderStreams();
}
}
window.maybeLoadStreamsOnShow = maybeLoadStreamsOnShow;
// Global variables for audio control
let currentlyPlayingAudio = null;
// Global variable to track the active SSE connection
let activeSSEConnection = null;
// Global cleanup function for SSE connections
const cleanupConnections = () => {
if (window._streamsSSE) {
if (window._streamsSSE.abort) {
window._streamsSSE.abort();
}
window._streamsSSE = null;
}
if (window.connectionTimeout) {
clearTimeout(window.connectionTimeout);
window.connectionTimeout = null;
}
activeSSEConnection = null;
};
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initStreamsUI();
// Also try to load streams immediately in case the page is already loaded
setTimeout(() => {
loadAndRenderStreams();
}, 100);
});
function loadAndRenderStreams() {
const ul = document.getElementById('stream-list');
if (!ul) {
// Debug messages disabled
return;
}
// Debug messages disabled
// Don't start a new connection if one is already active and we're not forcing a refresh
if (activeSSEConnection && !shouldForceRefresh) {
return;
}
// If we're forcing a refresh, clean up the existing connection
if (shouldForceRefresh && activeSSEConnection) {
// Clean up any existing connections
cleanupConnections();
shouldForceRefresh = false; // Reset the flag after handling
}
// Clear any existing error messages or retry buttons
ul.innerHTML = '
Loading public streams...
';
// Add a timestamp to prevent caching issues
const timestamp = new Date().getTime();
// Use the same protocol as the current page to avoid mixed content issues
const baseUrl = window.location.origin;
const sseUrl = `${baseUrl}/streams-sse?t=${timestamp}`;
let gotAny = false;
let streams = [];
window.connectionTimeout = null;
// Clean up any existing connections
cleanupConnections();
// Reset the retry count if we have a successful connection
window.streamRetryCount = 0;
if (window.connectionTimeout) {
clearTimeout(window.connectionTimeout);
window.connectionTimeout = null;
}
// Use fetch with ReadableStream for better CORS handling
const controller = new AbortController();
const signal = controller.signal;
// Store the controller for cleanup
window._streamsSSE = controller;
// Track the active connection
activeSSEConnection = controller;
// Set a connection timeout with debug info
const connectionStartTime = Date.now();
const connectionTimeoutId = setTimeout(() => {
if (!gotAny) {
// 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;
// Debug messages disabled
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();
// 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);
}
}
}, 15000); // 15 second timeout (increased from 10s)
// Store the timeout ID for cleanup
window.connectionTimeout = connectionTimeoutId;
// Make the fetch request with proper error handling
fetch(sseUrl, {
method: 'GET',
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
credentials: 'same-origin',
signal: signal,
mode: 'cors',
redirect: 'follow'
})
.then(response => {
if (!response.ok) {
// Try to get the response text for error details
return response.text().then(text => {
const error = new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
error.response = { status: response.status, statusText: response.statusText, body: text };
throw error;
}).catch(() => {
const error = new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
error.response = { status: response.status, statusText: response.statusText };
throw error;
});
}
if (!response.body) {
throw new Error('Response body is null or undefined');
}
// Get the readable stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
// Process the stream
function processStream({ done, value }) {
// Debug messages disabled
if (done) {
// Debug messages disabled
// Process any remaining data in the buffer
if (buffer.trim()) {
// Debug messages disabled
try {
const data = JSON.parse(buffer);
// Debug messages disabled
processSSEEvent(data);
} catch (e) {
// Debug messages disabled
}
}
return;
}
// Decode the chunk and add to buffer
buffer += decoder.decode(value, { stream: true });
// Process complete events in the buffer
const events = buffer.split('\n\n');
buffer = events.pop() || ''; // Keep incomplete event in buffer
for (const event of events) {
if (!event.trim()) continue;
// Extract data field from SSE format
const dataMatch = event.match(/^data: (\{.*\})$/m);
if (dataMatch && dataMatch[1]) {
try {
const data = JSON.parse(dataMatch[1]);
processSSEEvent(data);
} catch (e) {
// Debug messages disabled
}
}
}
// Read the next chunk
return reader.read().then(processStream);
}
// Start reading the stream
return reader.read().then(processStream);
})
.catch(error => {
// Only handle the error if it's not an abort error
if (error.name !== 'AbortError') {
// Clean up the controller reference
window._streamsSSE = null;
activeSSEConnection = null;
// Clear the connection timeout
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = null;
}
// Show a user-friendly error message
const ul = document.getElementById('stream-list');
if (ul) {
let errorMessage = 'Error loading streams. ';
if (error.message && error.message.includes('Failed to fetch')) {
errorMessage += 'Unable to connect to the server. Please check your internet connection.';
} else if (error.message && error.message.includes('CORS')) {
errorMessage += 'A server configuration issue occurred. Please try again later.';
} else {
errorMessage += 'Please try again later.';
}
ul.innerHTML = `