| | <!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> |