Claude: WebRTC Versuch

This commit is contained in:
chk
2026-06-02 23:10:13 +02:00
parent 9b1ae2ae14
commit 11811a2e03
7 changed files with 279 additions and 241 deletions

131
server.js
View File

@@ -1,85 +1,72 @@
'use strict';
const express = require('express');
const http = require('http');
const { WebSocketServer } = require('ws');
const path = require('path');
const { detectDevices } = require('./src/deviceDetect');
const { VideoStream } = require('./src/videoStream');
const http = require('http');
const path = require('path');
const { createSnapshotRouter } = require('./src/snapshotService');
const PORT = parseInt(process.env.PORT ?? '8080', 10);
const PORT = parseInt(process.env.PORT ?? '8444', 10);
const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984';
function buildStreams() {
const devices = detectDevices();
console.log(`Detected ${devices.length} camera(s):`, devices);
const app = express();
return devices.map((device, idx) => new VideoStream(device, {
name: `cam${idx}`,
width: parseInt(process.env[`CAM${idx}_WIDTH`] ?? '640', 10),
height: parseInt(process.env[`CAM${idx}_HEIGHT`] ?? '480', 10),
fps: parseInt(process.env[`CAM${idx}_FPS`] ?? '30', 10),
quality: parseInt(process.env[`CAM${idx}_QUALITY`] ?? '5', 10),
}));
}
app.use(express.static(path.join(__dirname, 'public')));
app.use('/api/snapshot', createSnapshotRouter(GO2RTC_URL));
function main() {
const streams = buildStreams();
// ── WebRTC signaling proxy ────────────────────────────────────────────────────
// Browser postet SDP-Offer hierher; wir leiten es an go2rtc weiter und
// geben die SDP-Answer zurück. Nur ein HTTP-Port nach aussen nötig.
app.post(
'/api/webrtc',
express.text({ type: 'application/sdp', limit: '64kb' }),
async (req, res) => {
const src = req.query.src ?? '';
try {
const upstream = await fetch(
`${GO2RTC_URL}/api/webrtc?src=${encodeURIComponent(src)}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: req.body,
}
);
if (!upstream.ok) {
const msg = await upstream.text();
return res.status(upstream.status).send(msg);
}
const answer = await upstream.text();
res.set('Content-Type', 'application/sdp');
res.send(answer);
} catch (err) {
res.status(503).json({ error: `go2rtc nicht erreichbar: ${err.message}` });
}
}
);
// Start all streams eagerly first client gets immediate frame
streams.forEach(s => s.start());
// ── Health ────────────────────────────────────────────────────────────────────
app.get('/health', async (_req, res) => {
let go2rtcOk = false;
try {
const r = await fetch(`${GO2RTC_URL}/api/streams`);
go2rtcOk = r.ok;
} catch { /* not reachable */ }
const app = express();
res.json({ status: go2rtcOk ? 'ok' : 'degraded', go2rtc: go2rtcOk });
});
app.use(express.static(path.join(__dirname, 'public')));
app.use('/api/snapshot', createSnapshotRouter(streams));
// ── Start ─────────────────────────────────────────────────────────────────────
const server = http.createServer(app);
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
cameras: streams.map((s, i) => ({
id: `cam${i}`,
device: s.device,
running: s.isRunning,
clients: s.clientCount,
hasFrame: s.latestFrame !== null,
})),
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`AppRobotWebcam on http://0.0.0.0:${PORT}`);
console.log(` go2rtc backend: ${GO2RTC_URL}`);
console.log(` WebRTC signaling proxy: POST /api/webrtc?src=cam0`);
console.log(` Snapshot API: GET /api/snapshot/cam0`);
});
const server = http.createServer(app);
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => {
const match = req.url?.match(/^\/ws\/cam(\d+)$/);
if (!match) { socket.destroy(); return; }
const idx = parseInt(match[1], 10);
if (!streams[idx]) { socket.destroy(); return; }
wss.handleUpgrade(req, socket, head, (ws) => {
const stream = streams[idx];
stream.addClient(ws);
ws.on('close', () => stream.removeClient(ws));
ws.on('error', () => stream.removeClient(ws));
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`AppRobotWebcam on http://0.0.0.0:${PORT}`);
streams.forEach((s, i) => {
console.log(` ws://0.0.0.0:${PORT}/ws/cam${i}${s.device}`);
console.log(` http://0.0.0.0:${PORT}/api/snapshot/cam${i}`);
});
});
const shutdown = (signal) => {
console.log(`\n${signal} received shutting down`);
streams.forEach(s => s.stop());
server.close(() => process.exit(0));
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
main();
const shutdown = (sig) => {
console.log(`\n${sig} shutting down`);
server.close(() => process.exit(0));
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));