asteroid/template/audio-player-frame.ctml

315 lines
11 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);
});
}
// Attach listeners to initial audio element
document.addEventListener('DOMContentLoaded', function() {
const audioElement = document.getElementById('persistent-audio');
if (audioElement) {
attachAudioListeners(audioElement);
}
});
</script>
</body>
</html>