/ template / audio-player-frame.ctml
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>