328 lines
12 KiB
Plaintext
328 lines
12 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
|
</head>
|
|
<body class="persistent-player-container">
|
|
<div class="persistent-player">
|
|
<span class="player-label">
|
|
<span class="live-stream-indicator">🟢 </span>
|
|
LIVE:
|
|
</span>
|
|
|
|
<div class="quality-selector">
|
|
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
|
<label for="stream-quality">Quality:</label>
|
|
<select id="stream-quality" onchange="changeStreamQuality()">
|
|
<option value="aac">AAC 96k</option>
|
|
<option value="mp3">MP3 128k</option>
|
|
<option value="low">MP3 64k</option>
|
|
</select>
|
|
</div>
|
|
|
|
<audio id="persistent-audio" controls preload="metadata" crossorigin="anonymous">
|
|
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
|
</audio>
|
|
|
|
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
|
|
|
<button id="reconnect-btn" onclick="reconnectStream()" class="persistent-reconnect-btn" title="Reconnect if audio stops working">
|
|
<img src="/asteroid/static/icons/sync.png" alt="Reconnect" style="width: 18px; height: 18px; vertical-align: middle; filter: invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%);">
|
|
</button>
|
|
|
|
<button onclick="disableFramesetMode()" class="persistent-disable-btn">
|
|
✕ Disable
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Status indicator for connection issues -->
|
|
<div id="stream-status" style="display: none; background: #550000; color: #ff6666; padding: 4px 10px; text-align: center; font-size: 0.85em;"></div>
|
|
|
|
<script>
|
|
// Configure audio element for better streaming
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const audioElement = document.getElementById('persistent-audio');
|
|
|
|
// Try to enable low-latency mode if supported
|
|
if ('mediaSession' in navigator) {
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
title: 'Asteroid Radio Live Stream',
|
|
artist: 'Asteroid Radio',
|
|
album: 'Live Broadcast'
|
|
});
|
|
}
|
|
|
|
// Note: Main event listeners are attached via attachAudioListeners()
|
|
// which is called at the bottom of this script
|
|
|
|
const selector = document.getElementById('stream-quality');
|
|
const streamQuality = localStorage.getItem('stream-quality') || 'aac';
|
|
if (selector && selector.value !== streamQuality) {
|
|
selector.value = streamQuality;
|
|
selector.dispatchEvent(new Event('change'));
|
|
}
|
|
});
|
|
|
|
// Stream quality configuration
|
|
function getStreamConfig(streamBaseUrl, encoding) {
|
|
const config = {
|
|
aac: {
|
|
url: streamBaseUrl + '/asteroid.aac',
|
|
type: 'audio/aac'
|
|
},
|
|
mp3: {
|
|
url: streamBaseUrl + '/asteroid.mp3',
|
|
type: 'audio/mpeg'
|
|
},
|
|
low: {
|
|
url: streamBaseUrl + '/asteroid-low.mp3',
|
|
type: 'audio/mpeg'
|
|
}
|
|
};
|
|
return config[encoding];
|
|
}
|
|
|
|
// Change stream quality
|
|
function changeStreamQuality() {
|
|
const selector = document.getElementById('stream-quality');
|
|
const streamBaseUrl = document.getElementById('stream-base-url').value;
|
|
const config = getStreamConfig(streamBaseUrl, selector.value);
|
|
|
|
// Save preference
|
|
localStorage.setItem('stream-quality', selector.value);
|
|
|
|
const audioElement = document.getElementById('persistent-audio');
|
|
const sourceElement = document.getElementById('audio-source');
|
|
|
|
const wasPlaying = !audioElement.paused;
|
|
|
|
sourceElement.src = config.url;
|
|
sourceElement.type = config.type;
|
|
audioElement.load();
|
|
|
|
if (wasPlaying) {
|
|
audioElement.play().catch(e => console.log('Autoplay prevented:', e));
|
|
}
|
|
}
|
|
|
|
// Update mini now playing display
|
|
async function updateMiniNowPlaying() {
|
|
try {
|
|
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
|
if (response.ok) {
|
|
const text = await response.text();
|
|
document.getElementById('mini-now-playing').textContent = text;
|
|
}
|
|
} catch(error) {
|
|
console.log('Could not fetch now playing:', error);
|
|
}
|
|
}
|
|
|
|
// Update every 10 seconds
|
|
setTimeout(updateMiniNowPlaying, 1000);
|
|
setInterval(updateMiniNowPlaying, 10000);
|
|
|
|
// Disable frameset mode function
|
|
function disableFramesetMode() {
|
|
// Clear preference
|
|
localStorage.removeItem('useFrameset');
|
|
// Redirect parent window to regular view
|
|
window.parent.location.href = '/asteroid/';
|
|
}
|
|
|
|
// Show status message
|
|
function showStatus(message, isError) {
|
|
const status = document.getElementById('stream-status');
|
|
if (status) {
|
|
status.textContent = message;
|
|
status.style.display = 'block';
|
|
status.style.background = isError ? '#550000' : '#005500';
|
|
status.style.color = isError ? '#ff6666' : '#66ff66';
|
|
if (!isError) {
|
|
setTimeout(() => { status.style.display = 'none'; }, 3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function hideStatus() {
|
|
const status = document.getElementById('stream-status');
|
|
if (status) {
|
|
status.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Reconnect stream - recreates audio element to fix wedged state
|
|
function reconnectStream() {
|
|
console.log('Reconnecting stream...');
|
|
showStatus('🔄 Reconnecting...', false);
|
|
|
|
const container = document.querySelector('.persistent-player');
|
|
const oldAudio = document.getElementById('persistent-audio');
|
|
const streamBaseUrl = document.getElementById('stream-base-url').value;
|
|
const streamQuality = localStorage.getItem('stream-quality') || 'aac';
|
|
const config = getStreamConfig(streamBaseUrl, streamQuality);
|
|
|
|
if (!container || !oldAudio) {
|
|
showStatus('❌ Could not reconnect - reload page', true);
|
|
return;
|
|
}
|
|
|
|
// Save current volume and muted state
|
|
const savedVolume = oldAudio.volume;
|
|
const savedMuted = oldAudio.muted;
|
|
console.log('Saving volume:', savedVolume, 'muted:', savedMuted);
|
|
|
|
// Reset spectrum analyzer if it exists
|
|
if (window.resetSpectrumAnalyzer) {
|
|
window.resetSpectrumAnalyzer();
|
|
}
|
|
|
|
// Stop and remove old audio
|
|
oldAudio.pause();
|
|
oldAudio.src = '';
|
|
oldAudio.load();
|
|
|
|
// Create new audio element
|
|
const newAudio = document.createElement('audio');
|
|
newAudio.id = 'persistent-audio';
|
|
newAudio.controls = true;
|
|
newAudio.preload = 'metadata';
|
|
newAudio.crossOrigin = 'anonymous';
|
|
|
|
// Restore volume and muted state
|
|
newAudio.volume = savedVolume;
|
|
newAudio.muted = savedMuted;
|
|
|
|
// Create source
|
|
const source = document.createElement('source');
|
|
source.id = 'audio-source';
|
|
source.src = config.url;
|
|
source.type = config.type;
|
|
newAudio.appendChild(source);
|
|
|
|
// Replace old audio with new
|
|
oldAudio.replaceWith(newAudio);
|
|
|
|
// Re-attach event listeners
|
|
attachAudioListeners(newAudio);
|
|
|
|
// Try to play
|
|
setTimeout(() => {
|
|
newAudio.play()
|
|
.then(() => {
|
|
console.log('Reconnected successfully');
|
|
showStatus('✓ Reconnected!', false);
|
|
// Reinitialize spectrum analyzer - try in this frame first
|
|
if (window.initSpectrumAnalyzer) {
|
|
setTimeout(() => window.initSpectrumAnalyzer(), 500);
|
|
}
|
|
// Also try in content frame (where spectrum canvas usually is)
|
|
try {
|
|
const contentFrame = window.parent.frames['content-frame'];
|
|
if (contentFrame && contentFrame.initSpectrumAnalyzer) {
|
|
setTimeout(() => {
|
|
if (contentFrame.resetSpectrumAnalyzer) {
|
|
contentFrame.resetSpectrumAnalyzer();
|
|
}
|
|
contentFrame.initSpectrumAnalyzer();
|
|
console.log('Spectrum analyzer reinitialized in content frame');
|
|
}, 600);
|
|
}
|
|
} catch(e) {
|
|
console.log('Could not reinit spectrum in content frame:', e);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.log('Reconnect play failed:', err);
|
|
showStatus('Click play to start stream', false);
|
|
});
|
|
}, 300);
|
|
}
|
|
|
|
// Error retry counter and reconnect state
|
|
var streamErrorCount = 0;
|
|
var reconnectTimeout = null;
|
|
var isReconnecting = false;
|
|
|
|
// Attach event listeners to audio element
|
|
function attachAudioListeners(audioElement) {
|
|
audioElement.addEventListener('waiting', function() {
|
|
console.log('Audio buffering...');
|
|
});
|
|
|
|
audioElement.addEventListener('playing', function() {
|
|
console.log('Audio playing');
|
|
hideStatus();
|
|
streamErrorCount = 0; // Reset error count on successful play
|
|
isReconnecting = false; // Reset reconnecting flag
|
|
if (reconnectTimeout) {
|
|
clearTimeout(reconnectTimeout);
|
|
reconnectTimeout = null;
|
|
}
|
|
});
|
|
|
|
audioElement.addEventListener('error', function(e) {
|
|
console.error('Audio error:', e);
|
|
if (isReconnecting) return; // Already reconnecting, skip
|
|
streamErrorCount++;
|
|
// Calculate delay with exponential backoff (3s, 6s, 12s, max 30s)
|
|
var delay = Math.min(3000 * Math.pow(2, streamErrorCount - 1), 30000);
|
|
showStatus('⚠️ Stream error. Reconnecting in ' + (delay/1000) + 's... (attempt ' + streamErrorCount + ')', true);
|
|
isReconnecting = true;
|
|
reconnectTimeout = setTimeout(function() {
|
|
reconnectStream();
|
|
}, delay);
|
|
});
|
|
|
|
audioElement.addEventListener('stalled', function() {
|
|
if (isReconnecting) return; // Already reconnecting, skip
|
|
console.log('Audio stalled, will auto-reconnect in 5 seconds...');
|
|
showStatus('⚠️ Stream stalled - reconnecting...', true);
|
|
isReconnecting = true;
|
|
setTimeout(function() {
|
|
if (audioElement.readyState < 3) {
|
|
reconnectStream();
|
|
} else {
|
|
isReconnecting = false;
|
|
}
|
|
}, 5000);
|
|
});
|
|
|
|
// Handle ended event - stream shouldn't end, so reconnect
|
|
audioElement.addEventListener('ended', function() {
|
|
if (isReconnecting) return; // Already reconnecting, skip
|
|
console.log('Stream ended unexpectedly, reconnecting...');
|
|
showStatus('⚠️ Stream ended - reconnecting...', true);
|
|
isReconnecting = true;
|
|
setTimeout(function() {
|
|
reconnectStream();
|
|
}, 2000);
|
|
});
|
|
|
|
// Handle pause event - detect browser throttling muted streams
|
|
audioElement.addEventListener('pause', function() {
|
|
// If paused while muted and we didn't initiate it, browser may have throttled
|
|
if (audioElement.muted && !isReconnecting) {
|
|
console.log('Stream paused while muted (possible browser throttling), will reconnect in 3 seconds...');
|
|
showStatus('⚠️ Stream paused - reconnecting...', true);
|
|
isReconnecting = true;
|
|
setTimeout(function() {
|
|
reconnectStream();
|
|
}, 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Attach listeners to initial audio element
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const audioElement = document.getElementById('persistent-audio');
|
|
if (audioElement) {
|
|
attachAudioListeners(audioElement);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|