111 lines
3.3 KiB
JavaScript
111 lines
3.3 KiB
JavaScript
'use strict';
|
||
|
||
const WS_RECONNECT_MS = 2000;
|
||
|
||
function createCameraView(idx, container) {
|
||
// DOM
|
||
const box = document.createElement('div');
|
||
box.className = 'cam-box';
|
||
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 640;
|
||
canvas.height = 480;
|
||
box.appendChild(canvas);
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'cam-label';
|
||
label.textContent = `cam${idx}`;
|
||
box.appendChild(label);
|
||
|
||
const info = document.createElement('div');
|
||
info.className = 'cam-info';
|
||
info.textContent = 'Verbinde...';
|
||
box.appendChild(info);
|
||
|
||
const actions = document.createElement('div');
|
||
actions.className = 'cam-actions';
|
||
const snapBtn = document.createElement('button');
|
||
snapBtn.textContent = 'Snapshot';
|
||
actions.appendChild(snapBtn);
|
||
box.appendChild(actions);
|
||
|
||
container.appendChild(box);
|
||
|
||
// Snapshot download
|
||
snapBtn.addEventListener('click', () => {
|
||
const a = document.createElement('a');
|
||
a.href = `/api/snapshot/cam${idx}`;
|
||
a.download = `cam${idx}_${Date.now()}.jpg`;
|
||
a.click();
|
||
});
|
||
|
||
// Rendering
|
||
const ctx = canvas.getContext('2d');
|
||
let frameCount = 0;
|
||
let lastFpsTs = Date.now();
|
||
let fps = 0;
|
||
|
||
function drawFrame(arrayBuffer) {
|
||
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
|
||
createImageBitmap(blob)
|
||
.then((bmp) => {
|
||
if (canvas.width !== bmp.width || canvas.height !== bmp.height) {
|
||
canvas.width = bmp.width;
|
||
canvas.height = bmp.height;
|
||
}
|
||
ctx.drawImage(bmp, 0, 0);
|
||
bmp.close();
|
||
|
||
frameCount++;
|
||
const now = Date.now();
|
||
if (now - lastFpsTs >= 1000) {
|
||
fps = Math.round(frameCount * 1000 / (now - lastFpsTs));
|
||
frameCount = 0;
|
||
lastFpsTs = now;
|
||
info.textContent = `${fps} fps`;
|
||
}
|
||
})
|
||
.catch(() => {/* ignore decode errors */});
|
||
}
|
||
|
||
// WebSocket connection with auto-reconnect
|
||
function connect() {
|
||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const ws = new WebSocket(`${proto}//${location.host}/ws/cam${idx}`);
|
||
ws.binaryType = 'arraybuffer';
|
||
|
||
ws.onopen = () => { info.textContent = 'Verbunden'; };
|
||
ws.onclose = () => {
|
||
info.textContent = `Getrennt – neu in ${WS_RECONNECT_MS / 1000}s`;
|
||
setTimeout(connect, WS_RECONNECT_MS);
|
||
};
|
||
ws.onerror = () => { info.textContent = 'Verbindungsfehler'; };
|
||
ws.onmessage = (evt) => drawFrame(evt.data);
|
||
}
|
||
|
||
connect();
|
||
}
|
||
|
||
// Fetch camera list from server, then build one view per camera
|
||
fetch('/api/snapshot')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const container = document.getElementById('cameras');
|
||
const count = data.cameras?.length ?? 0;
|
||
|
||
if (count === 0) {
|
||
document.getElementById('statusText').textContent = 'Keine Kameras erkannt';
|
||
return;
|
||
}
|
||
|
||
for (let i = 0; i < count; i++) createCameraView(i, container);
|
||
document.getElementById('statusText').textContent =
|
||
`${count} Kamera${count !== 1 ? 's' : ''} erkannt`;
|
||
})
|
||
.catch(() => {
|
||
// Fallback: show 2 cameras if API fails
|
||
const container = document.getElementById('cameras');
|
||
for (let i = 0; i < 2; i++) createCameraView(i, container);
|
||
document.getElementById('statusText').textContent = 'Kamera-API nicht erreichbar';
|
||
});
|