'use strict'; const ICE_SERVERS = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, ]; function log(camId, msg) { console.log(`[${camId}] ${msg}`); } 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); log('ice', `gathering timeout nach ${timeoutMs}ms – sende trotzdem`); resolve(); }, timeoutMs); }); } async function startWebRTC(camId, videoEl, statusEl) { setStatus(statusEl, 'Verbinde...', '#888'); log(camId, `WebRTC start → /api/webrtc?src=${camId}`); let pc; try { pc = new RTCPeerConnection({ iceServers: ICE_SERVERS }); pc.addTransceiver('video', { direction: 'recvonly' }); pc.onicecandidate = ({ candidate }) => { if (candidate) log(camId, `ICE candidate: ${candidate.type} ${candidate.address ?? '?'}`); }; pc.onicegatheringstatechange = () => log(camId, `ICE gathering: ${pc.iceGatheringState}`); pc.oniceconnectionstatechange = () => { const s = pc.iceConnectionState; log(camId, `ICE connection: ${s}`); const colors = { connected: '#4c4', completed: '#4c4', checking: '#fa0', failed: '#c44', disconnected: '#c44' }; setStatus(statusEl, s, colors[s] ?? '#888'); if (s === 'failed' || s === 'closed') { pc.close(); setTimeout(() => startWebRTC(camId, videoEl, statusEl), 4000); } }; pc.onconnectionstatechange = () => log(camId, `connection: ${pc.connectionState}`); pc.ontrack = ({ streams }) => { log(camId, `Track erhalten: ${streams.length} stream(s)`); if (streams[0]) { videoEl.srcObject = streams[0]; videoEl.play().catch(e => log(camId, `play() Fehler: ${e.message}`)); setStatus(statusEl, 'Live ✓', '#4c4'); } }; log(camId, 'Erstelle SDP Offer...'); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); log(camId, `ICE gathering wartet (max 5s)...`); await waitIceComplete(pc); log(camId, `Sende Offer (${pc.localDescription.sdp.length} Bytes) an Server...`); const resp = await fetch(`/api/webrtc?src=${encodeURIComponent(camId)}`, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp, }); if (!resp.ok) { const body = await resp.text(); throw new Error(`Signaling HTTP ${resp.status}: ${body}`); } const sdpAnswer = await resp.text(); log(camId, `Answer erhalten (${sdpAnswer.length} Bytes)`); await pc.setRemoteDescription({ type: 'answer', sdp: sdpAnswer }); log(camId, 'Remote description gesetzt – warte auf ICE...'); } catch (err) { console.error(`[${camId}] Fehler:`, err); setStatus(statusEl, `${err.message}`, '#c44'); pc?.close(); setTimeout(() => startWebRTC(camId, videoEl, statusEl), 5000); } } function setStatus(el, text, color) { el.textContent = text; el.style.color = color ?? '#999'; } function createCameraView(camId, container) { log(camId, 'View erstellt'); 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:#111'; 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 via /api/snapshot (proxied go2rtc /api/streams) log('init', 'Frage Kamera-Liste ab...'); fetch('/api/snapshot') .then(r => { log('init', `/api/snapshot → HTTP ${r.status}`); return r.json(); }) .then(data => { log('init', `Kameras: ${JSON.stringify(data.cameras)}`); const container = document.getElementById('cameras'); const cams = data.cameras ?? []; if (cams.length === 0) { document.getElementById('statusText').textContent = 'Keine Kameras (go2rtc läuft?)'; console.warn('go2rtc meldet keine Streams. Prüfe http://server:1984'); return; } cams.forEach(c => createCameraView(c.id, container)); document.getElementById('statusText').textContent = `${cams.length} Kamera${cams.length !== 1 ? 's' : ''} · WebRTC`; }) .catch(err => { console.error('[init] /api/snapshot Fehler:', err); document.getElementById('statusText').textContent = 'API-Fehler – Fallback'; const container = document.getElementById('cameras'); ['cam0', 'cam1'].forEach(id => createCameraView(id, container)); });