diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 1871475..dc8ed0f 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -75,6 +75,7 @@ func main() { "font-src 'self'", "script-src 'self'", "connect-src 'self'", + "media-src 'self'", // <- wichtig }, "; "), ) w.Header().Set("X-Content-Type-Options", "nosniff") diff --git a/web/static/js/stream.js b/web/static/js/stream.js index 61448cd..ad5b950 100644 --- a/web/static/js/stream.js +++ b/web/static/js/stream.js @@ -5,40 +5,76 @@ const viewersEl = document.getElementById('viewers'); const srcEl = document.getElementById('hlssrc'); - function updateLive(live){ - liveEl.className = 'pill ' + (live ? 'live' : 'off'); - liveEl.textContent = live ? 'LIVE' : 'Offline'; + const manifest = '/hls/' + encodeURIComponent(name) + '/index.m3u8'; + + function setLive(l){ + liveEl.className = 'pill ' + (l ? 'live' : 'off'); + liveEl.textContent = l ? 'LIVE' : 'Offline'; } - async function refresh(){ - let data; - try{ - const r = await fetch('/api/streams', { cache: 'no-store' }); - if (!r.ok) throw new Error('api '+r.status); - data = await r.json(); - }catch(e){ - console.warn('streams api error:', e); - setLive(false); - viewersEl.textContent = 'Zuschauer: -'; - return; - } - const r = await fetch('/api/streams'); - const d = await r.json(); - const it = d.items.find(x=>x.name===name); - const live = !!(it && it.live); - updateLive(live); - viewersEl.textContent = 'Zuschauer: ' + (it ? it.viewers : 0); - if(live){ - const src = '/hls/'+encodeURIComponent(name); - srcEl.textContent = src; - if (window.Hls && Hls.isSupported()){ - if(!window._hls){ window._hls = new Hls(); window._hls.attachMedia(v); } - window._hls.loadSource(src); + async function head(url){ + try { + const r = await fetch(url, { method: 'HEAD', cache:'no-store' }); + return r.ok; + } catch (_) { return false; } + } + + async function tryInitPlayer(){ + // 1) Manifest erreichbar? + const ok = await head(manifest); + srcEl.textContent = manifest; + setLive(ok); + if (!ok) return false; + + // 2) Player initialisieren + try { + if (window.Hls && Hls.isSupported()) { + if (!window._hls) { + window._hls = new Hls({ maxLiveSyncPlaybackRate: 1.0 }); + window._hls.on(Hls.Events.ERROR, (_, data)=>console.warn('hls.js error', data)); + window._hls.attachMedia(v); + } + window._hls.loadSource(manifest); } else if (v.canPlayType('application/vnd.apple.mpegurl')) { - v.src = src; + v.src = manifest; // Safari / iOS + } else { + console.warn('HLS nicht unterstützt'); + return false; } + return true; + } catch (e) { + console.warn('Player init fail', e); + return false; } } - refresh(); setInterval(refresh, 2500); + async function refreshMeta(){ + // UI-Infos (viewer count etc.) optional, darf nie Player blockieren + try { + const r = await fetch('/api/streams', { cache:'no-store' }); + if (!r.ok) return; + const d = await r.json(); + const it = d.items.find(x=>x.name===name); + if (it) { + setLive(!!it.live); + viewersEl.textContent = 'Zuschauer: ' + it.viewers; + } + } catch (_) {} + } + + // Video-Fehler sichtbar loggen + v.addEventListener('error', (e)=>console.warn('video error', e)); + v.addEventListener('loadedmetadata', ()=>console.log('metadata loaded')); + v.addEventListener('playing', ()=>console.log('playing')); + + // Boot-Sequenz mit Retries + (async function boot(){ + for (let i=0; i<10; i++) { + const ok = await tryInitPlayer(); + if (ok) break; + await new Promise(r=>setTimeout(r, 1500)); // kurz warten, bis HLS bereit ist + } + refreshMeta(); + setInterval(refreshMeta, 2500); + })(); })();