| <!DOCTYPE html> |
| <html> |
|
|
| <head> |
| <title>FoxDot Stream Test</title> |
| <style> |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| padding: 40px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| min-height: 100vh; |
| margin: 0; |
| } |
| |
| .container { |
| max-width: 600px; |
| margin: 0 auto; |
| background: rgba(255, 255, 255, 0.1); |
| backdrop-filter: blur(10px); |
| padding: 30px; |
| border-radius: 15px; |
| box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); |
| } |
| |
| h1 { |
| text-align: center; |
| margin-bottom: 10px; |
| } |
| |
| .subtitle { |
| text-align: center; |
| opacity: 0.8; |
| margin-bottom: 30px; |
| } |
| |
| audio { |
| width: 100%; |
| margin: 20px 0; |
| border-radius: 10px; |
| } |
| |
| .status { |
| padding: 15px; |
| background: rgba(0, 0, 0, 0.3); |
| border-radius: 8px; |
| margin: 15px 0; |
| } |
| |
| .status-indicator { |
| display: inline-block; |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| margin-right: 8px; |
| animation: pulse 2s ease-in-out infinite; |
| } |
| |
| .status-connecting { |
| background: #ffd700; |
| } |
| |
| .status-playing { |
| background: #00ff00; |
| } |
| |
| .status-error { |
| background: #ff0000; |
| animation: none; |
| } |
| |
| .status-buffering { |
| background: #ffa500; |
| } |
| |
| @keyframes pulse { |
| |
| 0%, |
| 100% { |
| opacity: 1; |
| } |
| |
| 50% { |
| opacity: 0.5; |
| } |
| } |
| |
| .info { |
| background: rgba(0, 0, 0, 0.2); |
| padding: 15px; |
| border-radius: 8px; |
| margin-top: 20px; |
| font-size: 14px; |
| } |
| |
| .info code { |
| background: rgba(0, 0, 0, 0.3); |
| padding: 2px 6px; |
| border-radius: 3px; |
| font-family: 'Courier New', monospace; |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="container"> |
| <h1>π΅ FoxDot Audio Stream</h1> |
| <p class="subtitle">Live audio streaming from SuperCollider</p> |
|
|
| <audio id="audioPlayer" controls autoplay> |
| <source src="http://localhost:8000/stream.mp3" type="audio/mpeg"> |
| Your browser does not support the audio element. |
| </audio> |
|
|
| <div class="status"> |
| <span id="statusIndicator" class="status-indicator status-connecting"></span> |
| <strong>Status:</strong> <span id="statusText">Connecting...</span> |
| </div> |
|
|
| <div class="info"> |
| <strong>Stream Info:</strong><br> |
| URL: <code id="streamUrl">http://localhost:8000/stream.mp3</code><br> |
| Format: <code>MP3 (128kbps)</code><br> |
| Source: <code>SuperCollider β JACK β FFmpeg β Icecast</code> |
| </div> |
|
|
| <div class="info" id="debugInfo" style="display: none;"> |
| <strong>Debug Info:</strong><br> |
| <div id="debugText"></div> |
| </div> |
| </div> |
|
|
| <script> |
| const audio = document.getElementById('audioPlayer'); |
| const statusText = document.getElementById('statusText'); |
| const statusIndicator = document.getElementById('statusIndicator'); |
| const debugInfo = document.getElementById('debugInfo'); |
| const debugText = document.getElementById('debugText'); |
| |
| function updateStatus(status, message) { |
| statusText.innerText = message; |
| statusIndicator.className = 'status-indicator status-' + status; |
| } |
| |
| function log(message) { |
| const timestamp = new Date().toLocaleTimeString(); |
| debugText.innerHTML += `[${timestamp}] ${message}<br>`; |
| debugInfo.style.display = 'block'; |
| console.log(message); |
| } |
| |
| audio.onloadstart = () => { |
| updateStatus('connecting', 'Loading stream...'); |
| log('Loading stream from /stream'); |
| }; |
| |
| audio.oncanplay = () => { |
| updateStatus('playing', 'Ready to play'); |
| log('Stream is ready to play'); |
| }; |
| |
| audio.onplaying = () => { |
| updateStatus('playing', 'Playing βͺ'); |
| log('Stream is playing'); |
| }; |
| |
| audio.onwaiting = () => { |
| updateStatus('buffering', 'Buffering...'); |
| log('Buffering stream data'); |
| }; |
| |
| audio.onpause = () => { |
| updateStatus('connecting', 'Paused'); |
| log('Stream paused'); |
| }; |
| |
| audio.onerror = (e) => { |
| updateStatus('error', 'Error loading stream'); |
| const error = audio.error; |
| let errorMsg = 'Unknown error'; |
| if (error) { |
| switch (error.code) { |
| case 1: errorMsg = 'MEDIA_ERR_ABORTED - Playback aborted'; break; |
| case 2: errorMsg = 'MEDIA_ERR_NETWORK - Network error'; break; |
| case 3: errorMsg = 'MEDIA_ERR_DECODE - Decoding error'; break; |
| case 4: errorMsg = 'MEDIA_ERR_SRC_NOT_SUPPORTED - Stream not supported'; break; |
| } |
| } |
| log(`ERROR: ${errorMsg}`); |
| |
| |
| fetch('/stream', { method: 'HEAD' }) |
| .then(response => { |
| log(`Stream endpoint status: ${response.status}`); |
| log(`Content-Type: ${response.headers.get('content-type')}`); |
| }) |
| .catch(err => { |
| log(`Cannot reach stream endpoint: ${err.message}`); |
| }); |
| }; |
| |
| |
| document.getElementById('streamUrl').innerText = window.location.origin + '/stream'; |
| |
| |
| log('Page loaded, attempting to connect to stream'); |
| </script> |
| </body> |
|
|
| </html> |