124 lines
4.0 KiB
JavaScript
124 lines
4.0 KiB
JavaScript
'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';
|
||
});
|