Files
appRobotWebcam/public/viewer.js
2026-06-02 23:15:13 +02:00

167 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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));
});