// VIDEO PLAYER let pc = null; let resourceURL = null; // from Location header (for DELETE) let retryTimer = null; function setStatus(t, cls) { statusEl.textContent = t; statusEl.classList.remove('ok','warn','err'); if (cls) statusEl.classList.add(cls); } function note(t) { msgEl.textContent = t || ''; } function waitIce(pc) { return new Promise((resolve) => { if (pc.iceGatheringState === 'complete') return resolve(); pc.addEventListener('icegatheringstatechange', () => { if (pc.iceGatheringState === 'complete') resolve(); }); }); } async function teardown() { try { if (resourceURL) await fetch(resourceURL, { method:'DELETE' }); } catch {} resourceURL = null; if (pc) { pc.ontrack = null; pc.onconnectionstatechange = null; try { pc.close(); } catch {} pc = null; } if (videoEl.srcObject) { try { videoEl.srcObject.getTracks().forEach(t => t.stop?.()); } catch {} videoEl.srcObject = null; } } function scheduleRetry(ms=10000) { if (retryTimer) return; retryTimer = setTimeout(() => { retryTimer = null; start().catch(()=>scheduleRetry()); }, ms); } async function start() { setStatus('Connecting…'); note(''); await teardown(); pc = new RTCPeerConnection({ bundlePolicy: 'max-bundle' }); pc.addTransceiver('video', { direction: 'recvonly' }); pc.addTransceiver('audio', { direction: 'recvonly' }); const inbound = new MediaStream(); pc.ontrack = (e) => { inbound.addTrack(e.track); if (!videoEl.srcObject) videoEl.srcObject = inbound; }; pc.onconnectionstatechange = () => { const st = pc.connectionState; if (st === 'connected') { setStatus('Live', 'ok'); videoEl.play().catch(()=>{}); } else if (st === 'failed' || st === 'disconnected') { setStatus('Reconnecting…', 'warn'); scheduleRetry(); } else if (st === 'closed') { setStatus('Closed', 'err'); scheduleRetry(); } else if (st === 'connecting') { setStatus('Connecting…'); } }; const offer = await pc.createOffer(); offer.sdp = offer.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1;sprop-stereo=1;") await pc.setLocalDescription(offer); await waitIce(pc); // ---- brute-force append stereo=1 ---- let sdp = pc.localDescription.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1;sprop-stereo=1;"); const resp = await fetch(WHEP_URL, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: sdp }); if (!resp.ok) { setStatus('Error', 'err'); throw new Error(`WHEP POST ${resp.status}`); } const answerSdp = await resp.text(); resourceURL = resp.headers.get('Location') || resp.headers.get('location'); await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp }); if (videoEl.muted) note('Autoplay is muted. Click the speaker icon to unmute.'); } window.addEventListener('beforeunload', teardown); // boot start().catch(err => { setStatus('Error', 'err'); note(String(err)); scheduleRetry(); });