'use strict'; const ICE_SERVERS = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, ]; // Wartet bis ICE-Gathering fertig ist (max timeoutMs) function waitIceComplete(pc, timeoutMs = 5000) { return new Promise(resolve => { if (pc.iceGatheringState === 'complete') { resolve(); return; } const check = () => { if (pc.iceGatheringState === 'complete') resolve(); }; pc.addEventListener('icegatheringstatechange', check); setTimeout(() => { pc.removeEventListener('icegatheringstatechange', check); resolve(); }, timeoutMs); }); } async function startWebRTC(camId, videoEl, statusEl) { statusEl.textContent = 'Verbinde...'; let pc; try { pc = new RTCPeerConnection({ iceServers: ICE_SERVERS }); // Nur Video empfangen, kein Audio pc.addTransceiver('video', { direction: 'recvonly' }); pc.ontrack = ({ streams }) => { if (!streams[0]) return; videoEl.srcObject = streams[0]; videoEl.play().catch(() => {}); }; pc.oniceconnectionstatechange = () => { const s = pc.iceConnectionState; statusEl.textContent = { connected: 'Live ✓', completed: 'Live ✓' }[s] ?? s; if (s === 'failed' || s === 'closed') { pc.close(); setTimeout(() => startWebRTC(camId, videoEl, statusEl), 4000); } }; // SDP Offer erstellen und warten bis alle ICE-Kandidaten gesammelt sind const offer = await pc.createOffer(); await pc.setLocalDescription(offer); await waitIceComplete(pc); // Signaling über Node.js-Proxy (kein separater go2rtc-Port nach aussen nötig) const resp = await fetch(`/api/webrtc?src=${encodeURIComponent(camId)}`, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp, }); if (!resp.ok) throw new Error(`Signaling HTTP ${resp.status}: ${await resp.text()}`); await pc.setRemoteDescription({ type: 'answer', sdp: await resp.text() }); } catch (err) { statusEl.textContent = `Fehler: ${err.message}`; console.error(`[${camId}]`, err); pc?.close(); setTimeout(() => startWebRTC(camId, videoEl, statusEl), 5000); } } function createCameraView(camId, container) { const box = document.createElement('div'); box.className = 'cam-box'; const video = document.createElement('video'); video.autoplay = true; video.playsInline = true; video.muted = true; video.style.cssText = 'display:block;width:640px;height:480px;background:#000'; box.appendChild(video); const label = document.createElement('div'); label.className = 'cam-label'; label.textContent = camId; box.appendChild(label); const status = document.createElement('div'); status.className = 'cam-info'; box.appendChild(status); const actions = document.createElement('div'); actions.className = 'cam-actions'; const snapBtn = document.createElement('button'); snapBtn.textContent = 'Snapshot'; snapBtn.onclick = () => { const a = document.createElement('a'); a.href = `/api/snapshot/${camId}`; a.download = `${camId}_${Date.now()}.jpg`; a.click(); }; actions.appendChild(snapBtn); box.appendChild(actions); container.appendChild(box); startWebRTC(camId, video, status); } // Kamera-Liste von go2rtc (via Node.js-Proxy), dann Views aufbauen fetch('/api/snapshot') .then(r => r.json()) .then(data => { const container = document.getElementById('cameras'); const cams = data.cameras ?? []; if (cams.length === 0) { document.getElementById('statusText').textContent = 'Keine Kameras in go2rtc'; return; } cams.forEach(c => createCameraView(c.id, container)); document.getElementById('statusText').textContent = `${cams.length} Kamera${cams.length !== 1 ? 's' : ''} · WebRTC`; }) .catch(() => { // Fallback wenn go2rtc noch nicht läuft const container = document.getElementById('cameras'); ['cam0', 'cam1'].forEach(id => createCameraView(id, container)); document.getElementById('statusText').textContent = 'go2rtc nicht erreichbar – versuche trotzdem'; });