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
All checks were successful
Pin to IPFS / pin (push) Successful in 15s
This commit is contained in:
parent
09c36ac6c1
commit
570606b6db
2 changed files with 70 additions and 20 deletions
80
app.js
80
app.js
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
10
style.css
10
style.css
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue