claude: webcam

This commit is contained in:
chk
2026-06-03 19:50:16 +02:00
parent 1a712ed877
commit 77c20bc3f1
9 changed files with 346 additions and 365 deletions

View File

@@ -1,126 +1,25 @@
'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');
// go2rtc-Player-Modi in Fallback-Reihenfolge.
// webrtc zuerst (niedrigste Latenz), dann MSE, dann MJPEG das verhindert
// schwarze Seiten: schlägt WebRTC fehl, springt der Player automatisch weiter.
const MODE = 'webrtc,mse,mjpeg';
function buildCamera(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:#111';
box.appendChild(video);
// go2rtc Web-Component verbindet sich relativ auf /api/ws (→ Node-Proxy)
const stream = document.createElement('video-stream');
stream.mode = MODE;
stream.src = `/api/ws?src=${encodeURIComponent(camId)}`;
box.appendChild(stream);
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');
@@ -135,32 +34,28 @@ function createCameraView(camId, container) {
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;
async function init() {
// Warten bis die go2rtc-Web-Component definiert ist (sonst greift der .src-Setter nicht)
await customElements.whenDefined('video-stream');
const container = document.getElementById('cameras');
const statusText = document.getElementById('statusText');
let cams = ['cam0', 'cam1']; // Fallback
try {
const r = await fetch('/api/snapshot');
const d = await r.json();
if (Array.isArray(d.cameras) && d.cameras.length) {
cams = d.cameras.map(c => c.id);
}
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));
});
} catch (err) {
console.warn('Kamera-Liste nicht abrufbar, nutze Fallback:', err.message);
}
cams.forEach(id => buildCamera(id, container));
statusText.textContent = `${cams.length} Kamera${cams.length !== 1 ? 's' : ''} · WebRTC`;
}
init();