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

124 lines
4.0 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' },
];
// 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';
});