Scan-Feedback roter/grüner Rahmen + BarcodeDetector-Fallback + gestufte Kameraauflösung + HTTPS-Hinweis
All checks were successful
Pin to IPFS / pin (push) Successful in 15s

This commit is contained in:
Ole 2026-06-14 17:55:35 +02:00
parent 09c36ac6c1
commit 570606b6db
2 changed files with 70 additions and 20 deletions

80
app.js
View file

@ -8,6 +8,16 @@
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
let barcodeDetector = null;
(async () => {
if ('BarcodeDetector' in window) {
try {
const f = await BarcodeDetector.getSupportedFormats();
if (f.includes('qr_code')) barcodeDetector = new BarcodeDetector({ formats: ['qr_code'] });
} catch (_) {}
}
})();
if (location.search.includes('debug')) { if (location.search.includes('debug')) {
document.body.classList.add('debug-mode'); document.body.classList.add('debug-mode');
} }
@ -72,29 +82,41 @@
/* ─── Camera ─── */ /* ─── Camera ─── */
let camStream = null; let camStream = null;
let camRaf = null; let camRaf = null;
let camVideoEl = null;
async function camStart(videoId) { async function camStart(videoId) {
await camStop(); await camStop();
try { let stream = null;
camStream = await navigator.mediaDevices.getUserMedia({ const resolutions = [
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } } { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } },
}); { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
} catch (_) { { video: true }
camStream = await navigator.mediaDevices.getUserMedia({ video: true }); ];
for (const vc of resolutions) {
try { stream = await navigator.mediaDevices.getUserMedia({ video: vc }); break; } catch (_) {}
} }
if (!stream) {
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
throw new Error('Kamera benötigt HTTPS verwende localhost oder lade ein Zertifikat');
}
throw new Error('Kamera nicht verfügbar oder verweigert');
}
camStream = stream;
const v = $(videoId); const v = $(videoId);
v.srcObject = camStream; camVideoEl = v;
v.srcObject = stream;
v.setAttribute('playsinline', ''); v.setAttribute('playsinline', '');
v.classList.add('active'); v.classList.add('active');
await v.play(); await v.play();
try { try {
const track = camStream.getVideoTracks()[0]; const track = stream.getVideoTracks()[0];
if (track) track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }); if (track) track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] });
} catch (_) {} } catch (_) {}
return v; return v;
} }
function camStop() { function camStop() {
if (camVideoEl) { camVideoEl.classList.remove('scan-active', 'scan-done'); camVideoEl = null; }
if (camRaf) { cancelAnimationFrame(camRaf); camRaf = null; } if (camRaf) { cancelAnimationFrame(camRaf); camRaf = null; }
if (camStream) { camStream.getTracks().forEach(t => t.stop()); camStream = null; } if (camStream) { camStream.getTracks().forEach(t => t.stop()); camStream = null; }
} }
@ -104,21 +126,39 @@
const c = $(canvasId); const c = $(canvasId);
const ctx = c.getContext('2d', { willReadFrequently: true }); const ctx = c.getContext('2d', { willReadFrequently: true });
let lastScan = 0; let lastScan = 0;
v.classList.add('scan-active');
const detect = barcodeDetector
? async () => {
const codes = await barcodeDetector.detect(v);
return codes.length ? codes[0].rawValue : null;
}
: async () => {
c.width = v.videoWidth; c.height = v.videoHeight;
ctx.drawImage(v, 0, 0);
const img = ctx.getImageData(0, 0, c.width, c.height);
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'attemptBoth' });
return code ? code.data : null;
};
camRaf = requestAnimationFrame(async function tick(now) { camRaf = requestAnimationFrame(async function tick(now) {
if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; } if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; }
if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; } if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; }
lastScan = now; lastScan = now;
c.width = v.videoWidth; c.height = v.videoHeight; try {
ctx.drawImage(v, 0, 0); const raw = await detect();
const img = ctx.getImageData(0, 0, c.width, c.height); if (raw) {
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' }); const bytes = decode45(raw);
if (code && code.data) {
try {
const bytes = decode45(code.data);
const p = await unpack(bytes); const p = await unpack(bytes);
if (p.v === 1) { callback(p, code.data); return; } if (p.v === 1) {
} catch(e) {} v.classList.remove('scan-active');
} v.classList.add('scan-done');
setTimeout(() => v.classList.remove('scan-done'), 600);
callback(p, raw);
return;
}
}
} catch (_) {}
camRaf = requestAnimationFrame(tick); camRaf = requestAnimationFrame(tick);
}); });
} }
@ -291,7 +331,7 @@
}); });
$('btn-scan-answer').textContent = 'Scannen beenden'; $('btn-scan-answer').textContent = 'Scannen beenden';
} catch (e) { } catch (e) {
$('host-scan-status').textContent = 'Kamera-Fehler'; $('host-scan-status').textContent = e.message;
} }
}); });
@ -353,7 +393,7 @@
await mesh.joinFromQR(raw); await mesh.joinFromQR(raw);
}); });
} catch (e) { } catch (e) {
$('join-scan-status').textContent = 'Kamera nicht verfügbar'; $('join-scan-status').textContent = e.message;
} }
}); });

View file

@ -192,6 +192,15 @@ h3 {
max-width: 320px; max-width: 320px;
border-radius: var(--radius); border-radius: var(--radius);
border: 2px solid var(--border); border: 2px solid var(--border);
transition: border-color 0.15s, box-shadow 0.15s;
}
video.scan-active {
border-color: #ff4444 !important;
box-shadow: 0 0 12px rgba(255,68,68,0.4);
}
video.scan-done {
border-color: #44ff44 !important;
box-shadow: 0 0 12px rgba(68,255,68,0.4);
} }
/* Protocol status */ /* Protocol status */
@ -341,6 +350,7 @@ h3 {
border-radius: var(--radius); border-radius: var(--radius);
display: none; display: none;
margin: 8px auto; margin: 8px auto;
transition: border-color 0.15s, box-shadow 0.15s;
} }
#host-camera.active { #host-camera.active {
display: block; display: block;