From 570606b6db021e69cd4c3ed5f9df86a3d8f35c67 Mon Sep 17 00:00:00 2001 From: Ole Date: Sun, 14 Jun 2026 17:55:35 +0200 Subject: [PATCH] =?UTF-8?q?Scan-Feedback=20roter/gr=C3=BCner=20Rahmen=20+?= =?UTF-8?q?=20BarcodeDetector-Fallback=20+=20gestufte=20Kameraaufl=C3=B6su?= =?UTF-8?q?ng=20+=20HTTPS-Hinweis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 80 +++++++++++++++++++++++++++++++++++++++++-------------- style.css | 10 +++++++ 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/app.js b/app.js index f0b5542..1454e49 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,16 @@ 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')) { document.body.classList.add('debug-mode'); } @@ -72,29 +82,41 @@ /* ─── Camera ─── */ let camStream = null; let camRaf = null; + let camVideoEl = null; async function camStart(videoId) { await camStop(); - try { - camStream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } } - }); - } catch (_) { - camStream = await navigator.mediaDevices.getUserMedia({ video: true }); + let stream = null; + const resolutions = [ + { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } }, + { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, + { 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); - v.srcObject = camStream; + camVideoEl = v; + v.srcObject = stream; v.setAttribute('playsinline', ''); v.classList.add('active'); await v.play(); try { - const track = camStream.getVideoTracks()[0]; + const track = stream.getVideoTracks()[0]; if (track) track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }); } catch (_) {} return v; } function camStop() { + if (camVideoEl) { camVideoEl.classList.remove('scan-active', 'scan-done'); camVideoEl = null; } if (camRaf) { cancelAnimationFrame(camRaf); camRaf = null; } if (camStream) { camStream.getTracks().forEach(t => t.stop()); camStream = null; } } @@ -104,21 +126,39 @@ const c = $(canvasId); const ctx = c.getContext('2d', { willReadFrequently: true }); 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) { if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; } if (now - lastScan < 100) { camRaf = requestAnimationFrame(tick); return; } lastScan = now; - 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: 'dontInvert' }); - if (code && code.data) { - try { - const bytes = decode45(code.data); + try { + const raw = await detect(); + if (raw) { + const bytes = decode45(raw); const p = await unpack(bytes); - if (p.v === 1) { callback(p, code.data); return; } - } catch(e) {} - } + if (p.v === 1) { + 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); }); } @@ -291,7 +331,7 @@ }); $('btn-scan-answer').textContent = 'Scannen beenden'; } catch (e) { - $('host-scan-status').textContent = 'Kamera-Fehler'; + $('host-scan-status').textContent = e.message; } }); @@ -353,7 +393,7 @@ await mesh.joinFromQR(raw); }); } catch (e) { - $('join-scan-status').textContent = 'Kamera nicht verfügbar'; + $('join-scan-status').textContent = e.message; } }); diff --git a/style.css b/style.css index 096c288..8348883 100644 --- a/style.css +++ b/style.css @@ -192,6 +192,15 @@ h3 { max-width: 320px; border-radius: var(--radius); 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 */ @@ -341,6 +350,7 @@ h3 { border-radius: var(--radius); display: none; margin: 8px auto; + transition: border-color 0.15s, box-shadow 0.15s; } #host-camera.active { display: block;