This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
const viewersEl = document.getElementById('viewers');
|
const viewersEl = document.getElementById('viewers');
|
||||||
const srcEl = document.getElementById('hlssrc');
|
const srcEl = document.getElementById('hlssrc');
|
||||||
|
|
||||||
|
let playerLive = false; // <- neue Quelle der Wahrheit
|
||||||
|
|
||||||
function setLive(on){
|
function setLive(on){
|
||||||
liveEl.className = 'pill ' + (on ? 'live' : 'off');
|
liveEl.className = 'pill ' + (on ? 'live' : 'off');
|
||||||
liveEl.textContent = on ? 'LIVE' : 'Offline';
|
liveEl.textContent = on ? 'LIVE' : 'Offline';
|
||||||
@@ -14,71 +16,84 @@
|
|||||||
const enc = encodeURIComponent(name);
|
const enc = encodeURIComponent(name);
|
||||||
const candidates = [
|
const candidates = [
|
||||||
`/hls/${enc}/index.m3u8`,
|
`/hls/${enc}/index.m3u8`,
|
||||||
`/hls/${enc}/main_stream.m3u8`, // Fallback, wenn es keinen Master gibt
|
`/hls/${enc}/main_stream.m3u8`,
|
||||||
];
|
];
|
||||||
for (const url of candidates) {
|
for (const url of candidates) {
|
||||||
try {
|
try { const r = await fetch(url, { cache: 'no-store' }); if (r.ok) return url; } catch {}
|
||||||
const r = await fetch(url, { cache: 'no-store' });
|
|
||||||
if (r.ok) return url; // WICHTIG: GET statt HEAD
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initPlayer() {
|
async function initPlayer() {
|
||||||
const url = await chooseManifest();
|
const url = await chooseManifest();
|
||||||
if (!url) { setLive(false); return false; }
|
if (!url) { playerLive = false; setLive(false); return false; }
|
||||||
|
|
||||||
srcEl.textContent = url;
|
srcEl.textContent = url;
|
||||||
setLive(true);
|
|
||||||
|
|
||||||
try { v.muted = true; v.playsInline = true; } catch(_) {}
|
try { v.muted = true; v.playsInline = true; } catch {}
|
||||||
|
|
||||||
if (window.Hls && Hls.isSupported()) {
|
if (window.Hls && Hls.isSupported()) {
|
||||||
if (!window._hls) {
|
if (!window._hls) {
|
||||||
window._hls = new Hls({ liveDurationInfinity: true });
|
window._hls = new Hls({ liveDurationInfinity: true });
|
||||||
window._hls.on(Hls.Events.MANIFEST_PARSED, (_e, data) => {
|
window._hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
console.log('HLS manifest parsed. levels=', data.levels?.length);
|
// Manifest geladen → sehr gutes Live-Signal
|
||||||
|
playerLive = true;
|
||||||
|
setLive(true);
|
||||||
});
|
});
|
||||||
window._hls.on(Hls.Events.ERROR, (_e, data) => {
|
window._hls.on(Hls.Events.ERROR, (_e, data) => {
|
||||||
console.warn('hls.js error', data);
|
console.warn('hls.js error', data);
|
||||||
|
// fatale Fehler → als offline markieren
|
||||||
|
if (data?.fatal) { playerLive = false; setLive(false); }
|
||||||
});
|
});
|
||||||
window._hls.attachMedia(v);
|
window._hls.attachMedia(v);
|
||||||
}
|
}
|
||||||
window._hls.loadSource(url);
|
window._hls.loadSource(url);
|
||||||
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
v.src = url; // Safari / iOS
|
v.src = url; // Safari/iOS
|
||||||
} else {
|
} else {
|
||||||
console.warn('HLS nicht unterstützt');
|
console.warn('HLS nicht unterstützt');
|
||||||
|
playerLive = false; setLive(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try { await v.play(); } catch(e){ console.log('autoplay blockiert (ok):', e?.name||e); }
|
try { await v.play(); } catch(e){ /* Autoplay blockiert ist ok */ }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zusätzliche Player-Signale
|
||||||
|
v.addEventListener('loadedmetadata', () => { playerLive = true; setLive(true); });
|
||||||
|
v.addEventListener('playing', () => { playerLive = true; setLive(true); });
|
||||||
|
v.addEventListener('error', () => { playerLive = false; setLive(false); });
|
||||||
|
|
||||||
async function refreshMeta(){
|
async function refreshMeta(){
|
||||||
|
// API nur als Zusatz – überschreibt NIE ein „echtes“ playerLive=true
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/streams', { cache: 'no-store' });
|
const r = await fetch('/api/streams', { cache:'no-store' });
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
const it = d.items.find(x => x.name === name);
|
const it = d.items.find(x => x.name === name);
|
||||||
if (it) {
|
if (!it) return;
|
||||||
setLive(!!it.live);
|
|
||||||
viewersEl.textContent = 'Zuschauer: ' + it.viewers;
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.addEventListener('error', e => console.warn('video error', e));
|
const apiLive = !!it.live;
|
||||||
v.addEventListener('loadedmetadata', () => console.log('loadedmetadata'));
|
const combinedLive = playerLive || apiLive;
|
||||||
v.addEventListener('playing', () => console.log('playing'));
|
setLive(combinedLive);
|
||||||
|
|
||||||
|
let viewers = it.viewers ?? 0;
|
||||||
|
if (combinedLive && viewers === 0) {
|
||||||
|
// Wir sehen selbst Video → mindestens 1 Betrachter
|
||||||
|
viewers = '≥1';
|
||||||
|
}
|
||||||
|
viewersEl.textContent = 'Zuschauer: ' + viewers;
|
||||||
|
} catch (_) {
|
||||||
|
// Bei API-Fehler nichts überschreiben
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(async function boot(){
|
(async function boot(){
|
||||||
for (let i=0; i<10; i++){
|
for (let i=0; i<10; i++){
|
||||||
const ok = await initPlayer();
|
const ok = await initPlayer();
|
||||||
if (ok) break;
|
if (ok) break;
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
await new Promise(r => setTimeout(r, 1200));
|
||||||
}
|
}
|
||||||
refreshMeta();
|
refreshMeta();
|
||||||
setInterval(refreshMeta, 2500);
|
setInterval(refreshMeta, 2500);
|
||||||
|
|||||||
Reference in New Issue
Block a user