Claude: Fix
This commit is contained in:
@@ -72,13 +72,23 @@ Encoding, ICE-Negotiation, robuster Client mit Auto-Fallback (WebRTC→MSE→MJP
|
|||||||
- [ ] Prüfen ob Kamera natives H.264 liefert (`v4l2-ctl --list-formats`) → kein Re-Encode
|
- [ ] Prüfen ob Kamera natives H.264 liefert (`v4l2-ctl --list-formats`) → kein Re-Encode
|
||||||
|
|
||||||
### Phase 4 – Internet-Härtung (offen, vor Produktiv-Schaltung)
|
### Phase 4 – Internet-Härtung (offen, vor Produktiv-Schaltung)
|
||||||
- [ ] **TLS**: Reverse Proxy (Caddy/nginx/traefik) mit HTTPS vor Port 8444
|
- [ ] **TLS via Caddy** – empfohlen, weil Caddy WebSocket-Proxy nativ und zuverlässig kann.
|
||||||
(WebRTC im Browser läuft über Internet zuverlässig nur im secure context)
|
Aktuell verbindet Browser `/api/ws` direkt zu go2rtc Port 1984.
|
||||||
- [ ] **WebRTC-Candidate**: `stun:8555` testen; falls NAT-Probleme → feste public IP/Domain
|
Caddy bündelt beides hinter einer Domain:
|
||||||
in der go2rtc-Config eintragen (`candidates: [robot.example.com:8555]`)
|
```
|
||||||
- [ ] **TURN**: nur falls reines STUN + Port-Forward UDP 8555 nicht reicht → coturn
|
robot.example.com {
|
||||||
- [ ] **Zugriffsschutz**: Basic-Auth oder Token am Reverse Proxy (1–3 bekannte User)
|
handle /api/ws* { reverse_proxy localhost:1984 }
|
||||||
- [ ] **Firewall**: TCP 8444 + UDP 8555 forwarden; Port 1984 NICHT exponieren
|
handle /api/stream* { reverse_proxy localhost:1984 }
|
||||||
|
handle /video-*.js { reverse_proxy localhost:1984 }
|
||||||
|
handle { reverse_proxy localhost:8444 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Danach: viewer.js `go2rtcPort` auf 443 (wss://) setzen, Node GO2RTC_PORT=443.
|
||||||
|
- [ ] **WebRTC-Candidate**: `stun:8555` testen; bei NAT-Problemen → feste IP/Domain:
|
||||||
|
`candidates: [robot.example.com:8555]` in go2rtc-Config
|
||||||
|
- [ ] **TURN**: nur wenn STUN + UDP 8555 nicht reicht (sehr restriktive NATs)
|
||||||
|
- [ ] **Zugriffsschutz**: Basic-Auth am Caddy (1–3 bekannte User)
|
||||||
|
- [ ] **Firewall**: TCP 443 (Caddy) + UDP 8555 (WebRTC) forwarden; 1984 + 8444 intern
|
||||||
|
|
||||||
### Phase 5 – Robustheit (optional)
|
### Phase 5 – Robustheit (optional)
|
||||||
- [ ] Kamera hot-plug: go2rtc-Verhalten bei Device-Verlust prüfen
|
- [ ] Kamera hot-plug: go2rtc-Verhalten bei Device-Verlust prüfen
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// go2rtc Player-Modi – Fallback-Reihenfolge: WebRTC → MSE → MJPEG
|
// go2rtc Player-Modi – Fallback-Reihenfolge: WebRTC → MSE → MJPEG
|
||||||
// Schlägt WebRTC fehl, springt der go2rtc-Player automatisch weiter → kein schwarzes Bild.
|
|
||||||
const MODE = 'webrtc,mse,mjpeg';
|
const MODE = 'webrtc,mse,mjpeg';
|
||||||
|
|
||||||
// ── Logging-Hilfe (sichtbar in Browser DevTools → Console) ──────────────────
|
// ── Logging (sichtbar in Browser DevTools → Console → F12) ──────────────────
|
||||||
const LOG_PREFIX = '[AppRobotWebcam]';
|
const P = '[WebcamViewer]';
|
||||||
function log(cam, msg) { console.log(`${LOG_PREFIX}[${cam}] ${msg}`); }
|
const log = (c, m) => console.log(`${P}[${c}] ${m}`);
|
||||||
function warn(cam, msg) { console.warn(`${LOG_PREFIX}[${cam}] ⚠ ${msg}`); }
|
const warn = (c, m) => console.warn(`${P}[${c}] ⚠ ${m}`);
|
||||||
function logErr(cam, msg, e) { console.error(`${LOG_PREFIX}[${cam}] ✗ ${msg}`, e ?? ''); }
|
const err = (c, m, e) => console.error(`${P}[${c}] ✗ ${m}`, e ?? '');
|
||||||
|
|
||||||
// ── Kamera-View aufbauen ─────────────────────────────────────────────────────
|
// ── Kamera-View aufbauen ─────────────────────────────────────────────────────
|
||||||
function buildCamera(camId, container) {
|
function buildCamera(camId, go2rtcPort, container) {
|
||||||
const wsUrl = `/api/ws?src=${encodeURIComponent(camId)}`;
|
// WebSocket direkt zu go2rtc – kein Proxy-Zwischenschritt, garantiert stabil.
|
||||||
log(camId, `View erstellt mode=${MODE} ws=${wsUrl}`);
|
// Protokoll: ws:// auf LAN (http). Für Internet mit TLS wird aus ws: wss: (Caddy).
|
||||||
|
const wsUrl = `ws://${location.hostname}:${go2rtcPort}/api/ws?src=${encodeURIComponent(camId)}`;
|
||||||
|
log(camId, `View erstellt mode="${MODE}" ws=${wsUrl}`);
|
||||||
|
|
||||||
const box = document.createElement('div');
|
const box = document.createElement('div');
|
||||||
box.className = 'cam-box';
|
box.className = 'cam-box';
|
||||||
|
|
||||||
// go2rtc Web-Component: verbindet sich via WebSocket zu /api/ws (→ Node-Proxy → go2rtc)
|
|
||||||
const stream = document.createElement('video-stream');
|
const stream = document.createElement('video-stream');
|
||||||
stream.mode = MODE;
|
stream.mode = MODE;
|
||||||
|
|
||||||
// Events vom inneren <video>-Element abfangen (capture-Phase = vor dem Element selbst)
|
// Events vom inneren <video>-Element (capture-Phase)
|
||||||
stream.addEventListener('play', () => log(camId, '▶ spielt'), true);
|
stream.addEventListener('play', () => log(camId, '▶ spielt'), true);
|
||||||
stream.addEventListener('playing', () => log(camId, '▶ Bild läuft'), true);
|
stream.addEventListener('playing', () => log(camId, '▶ Bild läuft'), true);
|
||||||
stream.addEventListener('pause', () => warn(camId, 'pausiert'), true);
|
stream.addEventListener('pause', () => warn(camId, 'pausiert'), true);
|
||||||
stream.addEventListener('stalled', () => warn(camId, 'stalled (kein Daten)'), true);
|
stream.addEventListener('stalled', () => warn(camId, 'stalled (keine Daten)'), true);
|
||||||
stream.addEventListener('waiting', () => warn(camId, 'waiting (Buffer leer)'), true);
|
stream.addEventListener('waiting', () => warn(camId, 'waiting (Buffer leer)'), true);
|
||||||
stream.addEventListener('error', (e) => logErr(camId, 'Video-Fehler', e), true);
|
stream.addEventListener('error', (e) => err(camId, 'Video-Fehler', e), true);
|
||||||
|
|
||||||
// src setzen startet die Verbindung
|
log(camId, `Verbinde WebSocket → ${wsUrl}`);
|
||||||
log(camId, `Verbinde → ${wsUrl}`);
|
stream.src = wsUrl; // setzt den VideoRTC-src und startet die Verbindung
|
||||||
stream.src = wsUrl;
|
|
||||||
|
|
||||||
box.appendChild(stream);
|
box.appendChild(stream);
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ function buildCamera(camId, container) {
|
|||||||
const snapBtn = document.createElement('button');
|
const snapBtn = document.createElement('button');
|
||||||
snapBtn.textContent = 'Snapshot';
|
snapBtn.textContent = 'Snapshot';
|
||||||
snapBtn.onclick = () => {
|
snapBtn.onclick = () => {
|
||||||
log(camId, 'Snapshot download → /api/snapshot/' + camId);
|
log(camId, `Snapshot → /api/snapshot/${camId}`);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = `/api/snapshot/${camId}`;
|
a.href = `/api/snapshot/${camId}`;
|
||||||
a.download = `${camId}_${Date.now()}.jpg`;
|
a.download = `${camId}_${Date.now()}.jpg`;
|
||||||
@@ -56,61 +55,55 @@ function buildCamera(camId, container) {
|
|||||||
box.appendChild(actions);
|
box.appendChild(actions);
|
||||||
|
|
||||||
container.appendChild(box);
|
container.appendChild(box);
|
||||||
|
|
||||||
// Connectivity-Check nach 5 s: ist die WebSocket-Verbindung zu /api/ws nutzbar?
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const r = await fetch('/api/streams');
|
|
||||||
if (r.ok) {
|
|
||||||
const d = await r.json();
|
|
||||||
log(camId, `go2rtc streams: ${Object.keys(d).join(', ')}`);
|
|
||||||
} else {
|
|
||||||
warn(camId, `/api/streams → HTTP ${r.status}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logErr(camId, '/api/streams nicht erreichbar (Proxy defekt?)', err);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Init ─────────────────────────────────────────────────────────────────────
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
||||||
async function init() {
|
async function init() {
|
||||||
log('init', 'Starte...');
|
log('init', 'Starte...');
|
||||||
|
|
||||||
|
// go2rtc-Port vom Server holen (damit er nicht im Client hart kodiert ist)
|
||||||
|
let go2rtcPort = 1984;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/config.json');
|
||||||
|
const d = await r.json();
|
||||||
|
go2rtcPort = d.go2rtcPort ?? 1984;
|
||||||
|
log('init', `go2rtc WebSocket-Port: ${go2rtcPort}`);
|
||||||
|
} catch (e) {
|
||||||
|
warn('init', `Konnte /config.json nicht laden, nehme Port ${go2rtcPort}`);
|
||||||
|
}
|
||||||
|
|
||||||
// go2rtc Web-Component muss geladen sein bevor .src gesetzt wird
|
// go2rtc Web-Component muss geladen sein bevor .src gesetzt wird
|
||||||
try {
|
try {
|
||||||
await customElements.whenDefined('video-stream');
|
await customElements.whenDefined('video-stream');
|
||||||
log('init', '<video-stream> definiert');
|
log('init', '<video-stream> definiert');
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
logErr('init', '<video-stream> nicht geladen – /video-stream.js erreichbar?', err);
|
err('init', '<video-stream> nicht geladen – /video-stream.js erreichbar?', e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById('cameras');
|
const container = document.getElementById('cameras');
|
||||||
const statusText = document.getElementById('statusText');
|
const statusText = document.getElementById('statusText');
|
||||||
|
|
||||||
// Kamera-Liste von go2rtc (via Node-Proxy /api/snapshot → /api/streams)
|
// Kamera-Liste von go2rtc via Node-Proxy
|
||||||
let cams = [];
|
let cams = [];
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/snapshot');
|
const r = await fetch('/api/snapshot');
|
||||||
log('init', `/api/snapshot → HTTP ${r.status}`);
|
log('init', `/api/snapshot → HTTP ${r.status}`);
|
||||||
|
if (r.ok) {
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (Array.isArray(d.cameras) && d.cameras.length) {
|
cams = (d.cameras ?? []).map(c => c.id);
|
||||||
cams = d.cameras.map(c => c.id);
|
log('init', `Kameras: ${cams.join(', ') || '(keine)'}`);
|
||||||
log('init', `Kameras: ${cams.join(', ')}`);
|
|
||||||
} else {
|
|
||||||
warn('init', 'Keine Kameras in go2rtc – Config OK? go2rtc läuft?');
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
logErr('init', '/api/snapshot nicht erreichbar – Fallback cam0/cam1', err);
|
err('init', '/api/snapshot Fehler – Fallback', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cams.length === 0) {
|
if (cams.length === 0) {
|
||||||
warn('init', 'Fallback: nehme cam0 und cam1 an');
|
warn('init', 'Fallback auf cam0, cam1');
|
||||||
cams = ['cam0', 'cam1'];
|
cams = ['cam0', 'cam1'];
|
||||||
}
|
}
|
||||||
|
|
||||||
cams.forEach(id => buildCamera(id, container));
|
cams.forEach(id => buildCamera(id, go2rtcPort, container));
|
||||||
statusText.textContent = `${cams.length} Kamera${cams.length !== 1 ? 's' : ''} · WebRTC`;
|
statusText.textContent = `${cams.length} Kamera${cams.length !== 1 ? 's' : ''} · WebRTC`;
|
||||||
log('init', 'Fertig');
|
log('init', 'Fertig');
|
||||||
}
|
}
|
||||||
|
|||||||
43
server.js
43
server.js
@@ -8,14 +8,15 @@ const { createSnapshotRouter } = require('./src/snapshotService');
|
|||||||
|
|
||||||
const PORT = parseInt(process.env.PORT ?? '8444', 10);
|
const PORT = parseInt(process.env.PORT ?? '8444', 10);
|
||||||
const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984';
|
const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984';
|
||||||
|
const GO2RTC_PORT = parseInt(process.env.GO2RTC_PORT ?? '1984', 10);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// ── Stabile Snapshot-API (vor dem Proxy registrieren!) ────────────────────────
|
// ── 1. Eigene Endpunkte (vor dem Proxy registrieren) ─────────────────────────
|
||||||
// Für das Homing-Projekt: GET /api/snapshot/cam0 → JPEG
|
|
||||||
|
// Stabile Snapshot-API für das Homing-Projekt
|
||||||
app.use('/api/snapshot', createSnapshotRouter(GO2RTC_URL));
|
app.use('/api/snapshot', createSnapshotRouter(GO2RTC_URL));
|
||||||
|
|
||||||
// ── Health ────────────────────────────────────────────────────────────────────
|
|
||||||
app.get('/health', async (_req, res) => {
|
app.get('/health', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${GO2RTC_URL}/api/streams`);
|
const r = await fetch(`${GO2RTC_URL}/api/streams`);
|
||||||
@@ -26,36 +27,40 @@ app.get('/health', async (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Reverse-Proxy zu go2rtc ───────────────────────────────────────────────────
|
// Gibt dem Viewer die go2rtc-Port-Nummer mit – Browser baut WS direkt zu go2rtc.
|
||||||
// Reicht nur die nötigen Pfade durch (go2rtc-Admin bleibt damit unerreichbar):
|
// Trennung HTTP (Node) / WebSocket (go2rtc) ist sauberer als ein fragiler WS-Proxy.
|
||||||
// /api/ws WebRTC/MSE-Signaling (WebSocket)
|
app.get('/config.json', (_req, res) => {
|
||||||
// /api/frame.jpeg Snapshots
|
res.json({ go2rtcPort: GO2RTC_PORT });
|
||||||
// /api/stream.* MJPEG/MP4-Fallback
|
});
|
||||||
// /api/streams Stream-Liste
|
|
||||||
// /video-rtc.js, /video-stream.js go2rtc's offizieller Player
|
// ── 2. HTTP-Proxy zu go2rtc (nur für Script-Dateien und API ohne WS) ─────────
|
||||||
|
// /api/ws NICHT hier proxy-en – das macht der Browser direkt (s. viewer.js).
|
||||||
const go2rtcProxy = createProxyMiddleware({
|
const go2rtcProxy = createProxyMiddleware({
|
||||||
target: GO2RTC_URL,
|
target: GO2RTC_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
|
||||||
// Plain-Pfade: dürfen NICHT mit Globs (/api/**) gemischt werden (HPM v3)
|
|
||||||
// '/api' matcht alles ab /api/... — kein ** nötig
|
|
||||||
pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'],
|
pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'],
|
||||||
logger: console,
|
logger: console,
|
||||||
|
on: {
|
||||||
|
error: (err, req, res) => {
|
||||||
|
console.error('[HPM] proxy error:', err.message);
|
||||||
|
if (!res.headersSent) res.status(502).json({ error: 'go2rtc nicht erreichbar' });
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
app.use(go2rtcProxy);
|
app.use(go2rtcProxy);
|
||||||
|
|
||||||
// ── Eigener Viewer ─────────────────────────────────────────────────────────────
|
// ── 3. Statische Dateien (eigener Viewer) ────────────────────────────────────
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
// ── Start ───────────────────────────────────────────────────────────────────────
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
server.on('upgrade', go2rtcProxy.upgrade); // WebSocket-Signaling durchreichen
|
|
||||||
|
|
||||||
server.listen(PORT, '0.0.0.0', () => {
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`AppRobotWebcam on http://0.0.0.0:${PORT}`);
|
console.log(`AppRobotWebcam http://0.0.0.0:${PORT}`);
|
||||||
console.log(` go2rtc backend: ${GO2RTC_URL}`);
|
console.log(` go2rtc HTTP: ${GO2RTC_URL}`);
|
||||||
|
console.log(` go2rtc WS: ws://[host]:${GO2RTC_PORT} (Browser verbindet direkt)`);
|
||||||
console.log(` Viewer: http://0.0.0.0:${PORT}/`);
|
console.log(` Viewer: http://0.0.0.0:${PORT}/`);
|
||||||
console.log(` Snapshot (API): http://0.0.0.0:${PORT}/api/snapshot/cam0`);
|
console.log(` Snapshot API: http://0.0.0.0:${PORT}/api/snapshot/cam0`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const shutdown = (sig) => {
|
const shutdown = (sig) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user