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

@@ -42,6 +42,9 @@
video-stream { display: block; width: 640px; height: 480px; background: #111; }
video-stream video { width: 100%; height: 100%; object-fit: contain; }
/* Eingefrorener Frame während des Hi-Res-Tests (Phase 1) */
.cam-freeze { display: block; width: 640px; height: 480px; background: #111; }
.cam-label {
position: absolute; top: 5px; left: 8px; z-index: 2;
background: rgba(0,0,0,.65); padding: 2px 7px; border-radius: 3px;
@@ -65,6 +68,16 @@
cursor: pointer; border-radius: 3px;
}
.cam-toggle:hover { background: rgba(60,60,60,.85); }
/* Hi-Res-Test-Button (Phase 1) links neben dem Ein/Aus-Schalter */
.cam-hdtest {
position: absolute; top: 5px; right: 40px; z-index: 2;
background: rgba(0,0,0,.65); color: #8cf; border: 1px solid #468;
height: 22px; padding: 0 7px; font-family: monospace; font-size: 0.7rem;
cursor: pointer; border-radius: 3px;
}
.cam-hdtest:hover:not(:disabled) { background: rgba(40,60,90,.85); }
.cam-hdtest:disabled { opacity: 0.4; cursor: default; }
</style>
</head>
<body>

View File

@@ -72,6 +72,90 @@ function stopStream(cam, auto = false) {
if (auto) showNotice();
}
// ── Hi-Res-Test (Phase 1): Geräte-Freigabe messen ─────────────────────────────
// Ablauf (doc/05_screenShot_roadmap.md, Phase 1):
// 1. aktuellen Live-Frame auf <canvas> einfrieren + „HD Image Work" einblenden
// 2. <video-stream> entfernen → cam verliert seinen Consumer (das „Umhängen")
// 3. GET /api/snapshot/:id/release-test → Server misst, wann das Gerät frei wird
// 4. egal wie es ausgeht: Canvas weg, <video-stream> wieder einsetzen (Live zurück)
// cam selbst wird nie verändert; im schlimmsten Fall nur ein Reconnect.
function showFreezeCanvas(cam) {
removeFreezeCanvas(cam);
const W = 640, H = 480;
const canvas = document.createElement('canvas');
canvas.className = 'cam-freeze';
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, W, H);
// letzten gezeigten Frame einfrieren (go2rtc rendert je nach Modus video/img/canvas)
const src = cam.box.querySelector('video-stream video, video-stream img, video-stream canvas');
if (src) {
try { ctx.drawImage(src, 0, 0, W, H); } catch (e) { logErr(cam.id, 'drawImage (Freeze)', e); }
}
// Badge „HD Image Work" unten rechts, ~30 % der Bildbreite, halbtransparent
const bw = W * 0.30, bh = 34, m = 12;
const bx = W - bw - m, by = H - bh - m;
ctx.fillStyle = 'rgba(0,0,0,.6)';
ctx.fillRect(bx, by, bw, bh);
ctx.fillStyle = '#8f8';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('HD Image Work', bx + bw / 2, by + bh / 2);
cam.box.insertBefore(canvas, cam.box.firstChild);
cam.freezeCanvas = canvas;
}
function removeFreezeCanvas(cam) {
if (cam.freezeCanvas) { cam.freezeCanvas.remove(); cam.freezeCanvas = null; }
}
async function runReleaseTest(cam) {
if (cam.testing) return;
cam.testing = true;
cam.hdBtn.disabled = true;
log(cam.id, '── Hi-Res-Test (Phase 1) gestartet ──');
// 1. + 2. Frame einfrieren, dann cam loslassen (verliert seinen Consumer)
showFreezeCanvas(cam);
stopStream(cam);
setInfo(cam, 'HD-Test: messe Freigabe…', 'warn');
try {
// 3. Server pollt /api/streams und misst die Freigabezeit (rein lesend).
// Client-Timeout (15s) > Server-Maximum (10s): hängt der Request, läuft
// trotzdem der finally-Recovery → cam kommt immer auf Live zurück.
const r = await fetch(`/api/snapshot/${encodeURIComponent(cam.id)}/release-test`,
{ signal: AbortSignal.timeout(15000) });
const data = await r.json();
console.log(`${P}[${cam.id}] release-test JSON:`, data);
if (data.freed) {
log(cam.id, `✓ Gerät frei nach ${data.msUntilFree} ms ` +
`(0-Consumer@${data.zeroConsumerAt}ms → Producer-Stop@${data.producerStoppedAt}ms)`);
setInfo(cam, `frei nach ${data.msUntilFree}ms`, 'ok');
} else {
warn(cam.id, 'Gerät NICHT freigegeben (freed=false) go2rtc hält den Producer warm. ' +
'Ansatz so nicht tragfähig (siehe Roadmap Phase 1).');
setInfo(cam, 'nicht freigegeben (warm)', 'crit');
}
} catch (e) {
logErr(cam.id, 'release-test fehlgeschlagen', e);
setInfo(cam, 'HD-Test Fehler', 'crit');
} finally {
// 4. Recovery: was auch passiert, zurück auf Live
removeFreezeCanvas(cam);
startStream(cam);
cam.testing = false;
cam.hdBtn.disabled = false;
log(cam.id, '── Hi-Res-Test beendet, zurück auf Live ──');
}
}
// ── Health-Anzeige ───────────────────────────────────────────────────────────
function setInfo(cam, text, cls) {
cam.infoEl.textContent = text;
@@ -223,15 +307,26 @@ function buildCamera(camId, container) {
const toggle = document.createElement('button');
toggle.className = 'cam-toggle';
const hd = document.createElement('button');
hd.className = 'cam-hdtest';
hd.textContent = 'HD?';
hd.title = 'Hi-Res-Test (Phase 1): Geräte-Freigabe messen';
const cam = {
id: camId, box, infoEl: info, toggleBtn: toggle,
id: camId, box, infoEl: info, toggleBtn: toggle, hdBtn: hd,
active: false, startedAt: 0, playingSince: null, statsLast: null, badTicks: 0, autoOff: false,
testing: false, freezeCanvas: null,
};
toggle.onclick = () => { cam.autoOff = false; cam.active ? stopStream(cam) : startStream(cam); showNotice(); };
toggle.onclick = () => {
if (cam.testing) return; // während HD-Test gesperrt
cam.autoOff = false; cam.active ? stopStream(cam) : startStream(cam); showNotice();
};
hd.onclick = () => runReleaseTest(cam);
box.appendChild(label);
box.appendChild(info);
box.appendChild(toggle);
box.appendChild(hd);
container.appendChild(box);
cameras.push(cam);