Claude: Screenshot Phase 1

This commit is contained in:
chk
2026-06-04 19:53:04 +02:00
parent 0e706428ce
commit 132e0ec597
4 changed files with 202 additions and 3 deletions

View File

@@ -2,14 +2,93 @@
const express = require('express');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Stabile Snapshot-Schnittstelle für das Homing-Projekt.
// Entkoppelt den Consumer von go2rtc-Interna proxied intern auf /api/frame.jpeg.
//
// GET /api/snapshot → JSON-Liste der Kameras (aus go2rtc /api/streams)
// GET /api/snapshot/cam0 → aktuelles JPEG (aus go2rtc /api/frame.jpeg?src=cam0)
// GET /api/snapshot/cam0/release-test → Phase-1-Messung (nur lesend, s.u.)
function createSnapshotRouter(go2rtcUrl) {
const router = express.Router();
// ── PHASE 1: Geräte-Freigabe messen (rein LESEND, kein Grab) ────────────────
// Linchpin-Test aus doc/05_screenShot_roadmap.md: Gibt go2rtc das Gerät frei,
// wenn cam0 den letzten Consumer verliert und wie schnell?
//
// Voraussetzung: der Client hat seinen <video-stream> für :id bereits entfernt
// (das „Umhängen"), BEVOR er diesen Endpunkt ruft. cam0 wird hier NICHT verändert
// wir pollen nur /api/streams und beobachten, wann der Producer stoppt.
//
// Antwort z.B.: { freed: true, msUntilFree: 1700, zeroConsumerAt, producerStoppedAt, samples }
router.get('/:id/release-test', async (req, res) => {
const { id } = req.params;
const POLL_MS = 200;
const MAX_MS = 10000;
const t0 = Date.now();
const samples = [];
let zeroConsumerAt = null; // ms ab t0, sobald 0 Consumer beobachtet
let producerStoppedAt = null; // ms ab t0, sobald kein laufender Producer mehr
console.log(`[release-test][${id}] Start polle /api/streams alle ${POLL_MS}ms (max ${MAX_MS}ms)`);
while (Date.now() - t0 < MAX_MS) {
const elapsed = Date.now() - t0;
let nConsumers = null;
let producerRunning = null;
try {
// Per-Poll-Timeout: hängt go2rtc, darf das nicht den ganzen Endpunkt
// blockieren (sonst kommt der Client nie zurück auf Live → Regel 4).
const r = await fetch(`${go2rtcUrl}/api/streams`, { signal: AbortSignal.timeout(1000) });
if (r.ok) {
const streams = await r.json();
const s = streams[id];
if (s) {
// Shape vgl. server.js-Monitor: producers[].state ('running'|'stop'|…), consumers[]
const producers = s.producers ?? [];
const consumers = s.consumers ?? [];
nConsumers = consumers.length;
producerRunning = producers.some((p) => (p.state ?? '') === 'running');
} else {
// Stream gar nicht (mehr) gelistet → kein Producer, keine Consumer
nConsumers = 0;
producerRunning = false;
}
}
} catch (e) {
// einzelner Poll-Fehler ist nicht fatal weiter messen
console.warn(`[release-test][${id}] Poll @${elapsed}ms fehlgeschlagen: ${e.message}`);
}
samples.push({ t: elapsed, consumers: nConsumers, producerRunning });
if (zeroConsumerAt === null && nConsumers === 0) {
zeroConsumerAt = elapsed;
console.log(`[release-test][${id}] 0 Consumer @${elapsed}ms`);
}
if (zeroConsumerAt !== null && producerRunning === false && producerStoppedAt === null) {
producerStoppedAt = elapsed;
console.log(`[release-test][${id}] Producer gestoppt @${elapsed}ms → Gerät frei`);
break;
}
await sleep(POLL_MS);
}
const freed = producerStoppedAt !== null;
const msUntilFree =
freed && zeroConsumerAt !== null ? producerStoppedAt - zeroConsumerAt : null;
console.log(
`[release-test][${id}] Ergebnis: freed=${freed} msUntilFree=${msUntilFree} ` +
`(0-Consumer@${zeroConsumerAt}ms, Producer-Stop@${producerStoppedAt}ms)`
);
res.json({ id, freed, msUntilFree, zeroConsumerAt, producerStoppedAt, samples });
});
router.get('/', async (_req, res) => {
try {
const r = await fetch(`${go2rtcUrl}/api/streams`);