Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Live Stream</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| html, body { width: 100%; height: 100%; overflow: hidden; background: #000; } | |
| video { | |
| width: 100vw; | |
| height: 100vh; | |
| object-fit: contain; | |
| background: #000; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <video id="video" autoplay muted playsinline></video> | |
| <script> | |
| const video = document.getElementById('video'); | |
| let ws, mediaSource, sourceBuffer; | |
| let bufferQueue = []; | |
| let reconnectTimer; | |
| // Use ?src= from URL hash, default to "test" for demo, "live" for real stream | |
| const streamSrc = location.hash.replace('#', '') || 'test'; | |
| function connect() { | |
| const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| ws = new WebSocket(`${proto}//${location.host}/api/ws?src=${streamSrc}`); | |
| ws.binaryType = 'arraybuffer'; | |
| ws.onopen = () => { | |
| mediaSource = new MediaSource(); | |
| video.src = URL.createObjectURL(mediaSource); | |
| }; | |
| ws.onmessage = (event) => { | |
| if (typeof event.data === 'string') { | |
| const msg = JSON.parse(event.data); | |
| if (msg.type === 'mse') { | |
| try { | |
| sourceBuffer = mediaSource.addSourceBuffer(msg.value); | |
| sourceBuffer.mode = 'segments'; | |
| sourceBuffer.addEventListener('updateend', () => { | |
| if (bufferQueue.length > 0 && !sourceBuffer.updating) { | |
| sourceBuffer.appendBuffer(bufferQueue.shift()); | |
| } | |
| if (video.buffered.length > 0 && video.buffered.end(0) - video.buffered.start(0) > 30) { | |
| sourceBuffer.remove(0, video.buffered.end(0) - 10); | |
| } | |
| }); | |
| video.muted = false; | |
| video.play().catch(() => {}); | |
| } catch (e) { | |
| console.error('Source buffer error:', e); | |
| } | |
| } | |
| } else { | |
| if (sourceBuffer && !sourceBuffer.updating) { | |
| try { | |
| sourceBuffer.appendBuffer(event.data); | |
| } catch (e) { | |
| bufferQueue.push(event.data); | |
| } | |
| } else { | |
| bufferQueue.push(event.data); | |
| } | |
| } | |
| }; | |
| ws.onclose = () => { cleanup(); scheduleReconnect(); }; | |
| ws.onerror = () => { cleanup(); scheduleReconnect(); }; | |
| } | |
| function cleanup() { | |
| bufferQueue = []; | |
| if (mediaSource && mediaSource.readyState === 'open') { | |
| try { mediaSource.endOfStream(); } catch(e) {} | |
| } | |
| sourceBuffer = null; | |
| mediaSource = null; | |
| } | |
| function scheduleReconnect() { | |
| clearTimeout(reconnectTimer); | |
| reconnectTimer = setTimeout(connect, 3000); | |
| } | |
| connect(); | |
| </script> | |
| </body> | |
| </html> | |