Claude: Phase 2 button
This commit is contained in:
@@ -351,19 +351,80 @@ function showNotice() {
|
|||||||
bar.style.display = 'flex';
|
bar.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Snapshot aller Kameras ───────────────────────────────────────────────────
|
// ── HD-Snapshot aller Kameras (parallel) ─────────────────────────────────────
|
||||||
function snapshotAll() {
|
// cam0 und cam1 liegen auf getrennten Geräten → gleichzeitiger Grab sicher.
|
||||||
|
// Alle Live-Streams werden synchron eingefroren und losgelassen, dann beide
|
||||||
|
// /hires-Requests parallel gefeuert. finally stellt immer alle zurück.
|
||||||
|
async function snapshotAllHires() {
|
||||||
|
if (cameras.some(c => c.testing)) return;
|
||||||
|
|
||||||
|
const snapBtn = document.getElementById('snapAllBtn');
|
||||||
|
if (snapBtn) snapBtn.disabled = true;
|
||||||
|
cameras.forEach(c => { c.testing = true; c.hdBtn.disabled = true; });
|
||||||
|
log('snap', `HD-Grab alle: ${cameras.map(c => c.id).join(', ')}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Alle Freeze-Canvases gleichzeitig aufbauen (je ein /api/snapshot-Fetch)
|
||||||
|
await Promise.all(cameras.map(c => showFreezeCanvas(c, 'Capturing HD…')));
|
||||||
|
|
||||||
|
// 2. Alle Live-Streams synchron loslassen → alle Consumer fallen gleichzeitig auf 0
|
||||||
|
cameras.forEach(c => stopStream(c));
|
||||||
|
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const ids = cameras.map(c => c.id);
|
|
||||||
log('snap', `Snapshot alle: ${ids.join(', ')}`);
|
// 3. Alle /hires-Grabs parallel – Fehler einer Kamera blockieren die andere nicht
|
||||||
ids.forEach(id => {
|
await Promise.allSettled(cameras.map(async c => {
|
||||||
|
try {
|
||||||
|
const r = await fetch(
|
||||||
|
`/api/snapshot/${encodeURIComponent(c.id)}/hires`,
|
||||||
|
{ signal: AbortSignal.timeout(20000) }
|
||||||
|
);
|
||||||
|
if (!r.ok) {
|
||||||
|
const body = await r.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error ?? `HTTP ${r.status}`);
|
||||||
|
}
|
||||||
|
const blob = await r.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
if (c.freezeCanvas) {
|
||||||
|
const ctx = c.freezeCanvas.getContext('2d');
|
||||||
|
await new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => { ctx.drawImage(img, 0, 0, 640, 480); resolve(); };
|
||||||
|
img.onerror = resolve;
|
||||||
|
img.src = blobUrl;
|
||||||
|
});
|
||||||
|
updateBadge(c, 'HD ✓', '#8f8');
|
||||||
|
}
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = `/api/snapshot/${id}`;
|
a.href = blobUrl;
|
||||||
a.download = `${id}_${ts}.jpg`;
|
a.download = `${c.id}_hires_${ts}.jpg`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
|
||||||
|
setInfo(c, 'HD gespeichert', 'ok');
|
||||||
|
log(c.id, `HD-Grab OK – ${blob.size} bytes`);
|
||||||
|
} catch (e) {
|
||||||
|
logErr(c.id, 'HD-Grab fehlgeschlagen', e);
|
||||||
|
setInfo(c, `HD Fehler: ${e.message}`, 'crit');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 4. Immer: alle zurück auf Live
|
||||||
|
await sleep(600);
|
||||||
|
cameras.forEach(c => {
|
||||||
|
removeFreezeCanvas(c);
|
||||||
|
startStream(c);
|
||||||
|
c.testing = false;
|
||||||
|
c.hdBtn.disabled = false;
|
||||||
});
|
});
|
||||||
|
if (snapBtn) snapBtn.disabled = false;
|
||||||
|
log('snap', '── HD-Grab alle beendet, alle zurück auf Live ──');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Kamera-View aufbauen ─────────────────────────────────────────────────────
|
// ── Kamera-View aufbauen ─────────────────────────────────────────────────────
|
||||||
@@ -443,7 +504,7 @@ async function init() {
|
|||||||
if (camIds.length === 0) { warn('init', 'Fallback cam0, cam1'); camIds = ['cam0', 'cam1']; }
|
if (camIds.length === 0) { warn('init', 'Fallback cam0, cam1'); camIds = ['cam0', 'cam1']; }
|
||||||
|
|
||||||
const snapBtn = document.getElementById('snapAllBtn');
|
const snapBtn = document.getElementById('snapAllBtn');
|
||||||
if (snapBtn) { snapBtn.onclick = snapshotAll; snapBtn.disabled = false; }
|
if (snapBtn) { snapBtn.onclick = snapshotAllHires; snapBtn.disabled = false; }
|
||||||
|
|
||||||
camIds.forEach(id => buildCamera(id, container));
|
camIds.forEach(id => buildCamera(id, container));
|
||||||
statusText.textContent = `${camIds.length} Kamera${camIds.length !== 1 ? 's' : ''} · WebRTC`;
|
statusText.textContent = `${camIds.length} Kamera${camIds.length !== 1 ? 's' : ''} · WebRTC`;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function readJpegWidth(buf) {
|
|||||||
// GET /api/snapshot/cam0/hires → 1280×960 JPEG via cam0_hires (Phase 2)
|
// GET /api/snapshot/cam0/hires → 1280×960 JPEG via cam0_hires (Phase 2)
|
||||||
function createSnapshotRouter(go2rtcUrl) {
|
function createSnapshotRouter(go2rtcUrl) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
let hiresLock = false; // Mutex: nie zwei Hi-Res-Grabs gleichzeitig
|
const hiresLocks = {}; // Mutex pro Kamera: { cam0: false, cam1: false, … }
|
||||||
|
|
||||||
// ── PHASE 1: Geräte-Freigabe messen (rein LESEND, kein Grab) ────────────────
|
// ── PHASE 1: Geräte-Freigabe messen (rein LESEND, kein Grab) ────────────────
|
||||||
// Linchpin-Test aus doc/05_screenShot_roadmap.md: Gibt go2rtc das Gerät frei,
|
// Linchpin-Test aus doc/05_screenShot_roadmap.md: Gibt go2rtc das Gerät frei,
|
||||||
@@ -118,10 +118,10 @@ function createSnapshotRouter(go2rtcUrl) {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const hiresId = `${id}_hires`;
|
const hiresId = `${id}_hires`;
|
||||||
|
|
||||||
if (hiresLock) {
|
if (hiresLocks[id]) {
|
||||||
return res.status(429).json({ error: 'Hi-Res-Grab läuft bereits – bitte warten' });
|
return res.status(429).json({ error: `Hi-Res-Grab für ${id} läuft bereits – bitte warten` });
|
||||||
}
|
}
|
||||||
hiresLock = true;
|
hiresLocks[id] = true;
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -206,7 +206,7 @@ function createSnapshotRouter(go2rtcUrl) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!res.headersSent) res.status(503).json({ error: `hires: ${err.message}` });
|
if (!res.headersSent) res.status(503).json({ error: `hires: ${err.message}` });
|
||||||
} finally {
|
} finally {
|
||||||
hiresLock = false;
|
hiresLocks[id] = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user