audio-player-frame.ctml
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css"> 7 </head> 8 <body class="persistent-player-container"> 9 <div class="persistent-player"> 10 <span class="player-label"> 11 <span class="live-stream-indicator">🟢 </span> 12 LIVE: 13 </span> 14 15 <div class="quality-selector"> 16 <input type="hidden" id="stream-base-url" lquery="(val stream-base-url)"> 17 <label for="stream-quality">Quality:</label> 18 <select id="stream-quality" onchange="changeStreamQuality()"> 19 <option value="aac">AAC 96k</option> 20 <option value="mp3">MP3 128k</option> 21 <option value="low">MP3 64k</option> 22 </select> 23 </div> 24 25 <audio id="persistent-audio" controls preload="metadata" crossorigin="anonymous"> 26 <source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)"> 27 </audio> 28 29 <span class="now-playing-mini" id="mini-now-playing">Loading...</span> 30 31 <button id="reconnect-btn" onclick="reconnectStream()" class="persistent-reconnect-btn" title="Reconnect if audio stops working"> 32 🔄 33 </button> 34 35 <button onclick="disableFramesetMode()" class="persistent-disable-btn"> 36 ✕ Disable 37 </button> 38 </div> 39 40 <!-- Status indicator for connection issues --> 41 <div id="stream-status" style="display: none; background: #550000; color: #ff6666; padding: 4px 10px; text-align: center; font-size: 0.85em;"></div> 42 43 <script> 44 // Configure audio element for better streaming 45 document.addEventListener('DOMContentLoaded', function() { 46 const audioElement = document.getElementById('persistent-audio'); 47 48 // Try to enable low-latency mode if supported 49 if ('mediaSession' in navigator) { 50 navigator.mediaSession.metadata = new MediaMetadata({ 51 title: 'Asteroid Radio Live Stream', 52 artist: 'Asteroid Radio', 53 album: 'Live Broadcast' 54 }); 55 } 56 57 // Add event listeners for debugging 58 audioElement.addEventListener('waiting', function() { 59 console.log('Audio buffering...'); 60 }); 61 62 audioElement.addEventListener('playing', function() { 63 console.log('Audio playing'); 64 }); 65 66 audioElement.addEventListener('error', function(e) { 67 console.error('Audio error:', e); 68 }); 69 70 const selector = document.getElementById('stream-quality'); 71 const streamQuality = localStorage.getItem('stream-quality') || 'aac'; 72 if (selector && selector.value !== streamQuality) { 73 selector.value = streamQuality; 74 selector.dispatchEvent(new Event('change')); 75 } 76 }); 77 78 // Stream quality configuration 79 function getStreamConfig(streamBaseUrl, encoding) { 80 const config = { 81 aac: { 82 url: streamBaseUrl + '/asteroid.aac', 83 type: 'audio/aac' 84 }, 85 mp3: { 86 url: streamBaseUrl + '/asteroid.mp3', 87 type: 'audio/mpeg' 88 }, 89 low: { 90 url: streamBaseUrl + '/asteroid-low.mp3', 91 type: 'audio/mpeg' 92 } 93 }; 94 return config[encoding]; 95 } 96 97 // Change stream quality 98 function changeStreamQuality() { 99 const selector = document.getElementById('stream-quality'); 100 const streamBaseUrl = document.getElementById('stream-base-url').value; 101 const config = getStreamConfig(streamBaseUrl, selector.value); 102 103 // Save preference 104 localStorage.setItem('stream-quality', selector.value); 105 106 const audioElement = document.getElementById('persistent-audio'); 107 const sourceElement = document.getElementById('audio-source'); 108 109 const wasPlaying = !audioElement.paused; 110 111 sourceElement.src = config.url; 112 sourceElement.type = config.type; 113 audioElement.load(); 114 115 if (wasPlaying) { 116 audioElement.play().catch(e => console.log('Autoplay prevented:', e)); 117 } 118 } 119 120 // Update mini now playing display 121 async function updateMiniNowPlaying() { 122 try { 123 const response = await fetch('/api/asteroid/partial/now-playing-inline'); 124 if (response.ok) { 125 const text = await response.text(); 126 document.getElementById('mini-now-playing').textContent = text; 127 } 128 } catch(error) { 129 console.log('Could not fetch now playing:', error); 130 } 131 } 132 133 // Update every 10 seconds 134 setTimeout(updateMiniNowPlaying, 1000); 135 setInterval(updateMiniNowPlaying, 10000); 136 137 // Disable frameset mode function 138 function disableFramesetMode() { 139 // Clear preference 140 localStorage.removeItem('useFrameset'); 141 // Redirect parent window to regular view 142 window.parent.location.href = '/asteroid/'; 143 } 144 145 // Show status message 146 function showStatus(message, isError) { 147 const status = document.getElementById('stream-status'); 148 if (status) { 149 status.textContent = message; 150 status.style.display = 'block'; 151 status.style.background = isError ? '#550000' : '#005500'; 152 status.style.color = isError ? '#ff6666' : '#66ff66'; 153 if (!isError) { 154 setTimeout(() => { status.style.display = 'none'; }, 3000); 155 } 156 } 157 } 158 159 function hideStatus() { 160 const status = document.getElementById('stream-status'); 161 if (status) { 162 status.style.display = 'none'; 163 } 164 } 165 166 // Reconnect stream - recreates audio element to fix wedged state 167 function reconnectStream() { 168 console.log('Reconnecting stream...'); 169 showStatus('🔄 Reconnecting...', false); 170 171 const container = document.querySelector('.persistent-player'); 172 const oldAudio = document.getElementById('persistent-audio'); 173 const streamBaseUrl = document.getElementById('stream-base-url').value; 174 const streamQuality = localStorage.getItem('stream-quality') || 'aac'; 175 const config = getStreamConfig(streamBaseUrl, streamQuality); 176 177 if (!container || !oldAudio) { 178 showStatus('❌ Could not reconnect - reload page', true); 179 return; 180 } 181 182 // Save current volume and muted state 183 const savedVolume = oldAudio.volume; 184 const savedMuted = oldAudio.muted; 185 console.log('Saving volume:', savedVolume, 'muted:', savedMuted); 186 187 // Reset spectrum analyzer if it exists 188 if (window.resetSpectrumAnalyzer) { 189 window.resetSpectrumAnalyzer(); 190 } 191 192 // Stop and remove old audio 193 oldAudio.pause(); 194 oldAudio.src = ''; 195 oldAudio.load(); 196 197 // Create new audio element 198 const newAudio = document.createElement('audio'); 199 newAudio.id = 'persistent-audio'; 200 newAudio.controls = true; 201 newAudio.preload = 'metadata'; 202 newAudio.crossOrigin = 'anonymous'; 203 204 // Restore volume and muted state 205 newAudio.volume = savedVolume; 206 newAudio.muted = savedMuted; 207 208 // Create source 209 const source = document.createElement('source'); 210 source.id = 'audio-source'; 211 source.src = config.url; 212 source.type = config.type; 213 newAudio.appendChild(source); 214 215 // Replace old audio with new 216 oldAudio.replaceWith(newAudio); 217 218 // Re-attach event listeners 219 attachAudioListeners(newAudio); 220 221 // Try to play 222 setTimeout(() => { 223 newAudio.play() 224 .then(() => { 225 console.log('Reconnected successfully'); 226 showStatus('✓ Reconnected!', false); 227 // Reinitialize spectrum analyzer - try in this frame first 228 if (window.initSpectrumAnalyzer) { 229 setTimeout(() => window.initSpectrumAnalyzer(), 500); 230 } 231 // Also try in content frame (where spectrum canvas usually is) 232 try { 233 const contentFrame = window.parent.frames['content-frame']; 234 if (contentFrame && contentFrame.initSpectrumAnalyzer) { 235 setTimeout(() => { 236 if (contentFrame.resetSpectrumAnalyzer) { 237 contentFrame.resetSpectrumAnalyzer(); 238 } 239 contentFrame.initSpectrumAnalyzer(); 240 console.log('Spectrum analyzer reinitialized in content frame'); 241 }, 600); 242 } 243 } catch(e) { 244 console.log('Could not reinit spectrum in content frame:', e); 245 } 246 }) 247 .catch(err => { 248 console.log('Reconnect play failed:', err); 249 showStatus('Click play to start stream', false); 250 }); 251 }, 300); 252 } 253 254 // Attach event listeners to audio element 255 function attachAudioListeners(audioElement) { 256 audioElement.addEventListener('waiting', function() { 257 console.log('Audio buffering...'); 258 }); 259 260 audioElement.addEventListener('playing', function() { 261 console.log('Audio playing'); 262 hideStatus(); 263 }); 264 265 audioElement.addEventListener('error', function(e) { 266 console.error('Audio error:', e); 267 showStatus('⚠️ Stream error - click 🔄 to reconnect', true); 268 }); 269 270 audioElement.addEventListener('stalled', function() { 271 console.log('Audio stalled'); 272 showStatus('⚠️ Stream stalled - click 🔄 if no audio', true); 273 }); 274 } 275 276 // Attach listeners to initial audio element 277 document.addEventListener('DOMContentLoaded', function() { 278 const audioElement = document.getElementById('persistent-audio'); 279 if (audioElement) { 280 attachAudioListeners(audioElement); 281 } 282 }); 283 </script> 284 </body> 285 </html>