diff options
| author | Adam Soltys <[email protected]> | 2025-03-14 09:18:38 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2025-03-14 09:18:38 -0700 |
| commit | 4d458fafd70411e3826bf6c6b8257e24a54b34c9 (patch) | |
| tree | feb7fb95d8bb175c1c6e88ef7fc1a2e5b0f4be3d /src | |
| parent | d6d36b52f16d02bd112483ae9fe69876623935b6 (diff) | |
| download | coinos-ui-4d458fafd70411e3826bf6c6b8257e24a54b34c9.tar.gz coinos-ui-4d458fafd70411e3826bf6c6b8257e24a54b34c9.zip | |
new scanner
Diffstat (limited to 'src')
| -rw-r--r-- | src/routes/(app)/scan/+page.svelte | 186 |
1 files changed, 157 insertions, 29 deletions
diff --git a/src/routes/(app)/scan/+page.svelte b/src/routes/(app)/scan/+page.svelte index edff522e..aab7bcaa 100644 --- a/src/routes/(app)/scan/+page.svelte +++ b/src/routes/(app)/scan/+page.svelte @@ -1,54 +1,182 @@ <script> - import { tick } from "svelte"; - import Icon from "$comp/Icon.svelte"; - import QrScanner from "qr-scanner"; - import { onMount, onDestroy } from "svelte"; import { back } from "$lib/utils"; import { goto } from "$app/navigation"; - - let scanner, vid = $state(), resizing; + import { onMount, onDestroy } from "svelte"; + import { BarcodeDetector } from "barcode-detector/ponyfill"; let resize = () => { if (resizing) clearTimeout(resizing); resizing = setTimeout(initialize, 1000); }; - let initialize = async () => { - if (scanner) { - scanner.stop(); - await new Promise((r) => setTimeout(r, 1000)); + let videoEl; + let canvasEl; + let barcodeDetector; + let scanningFrameId; + let cameras = []; + let selectedCamera = ""; + let detectionResult = null; // will hold the detected QR code value + + // Enumerate available video input devices + async function getCameraDevices() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + cameras = devices.filter((device) => device.kind === "videoinput"); + if (!selectedCamera && cameras.length > 0) { + // Prefer a rear-facing camera if available + const envCamera = cameras.find( + (camera) => + camera.label.toLowerCase().includes("back") || + camera.label.toLowerCase().includes("env"), + ); + selectedCamera = envCamera ? envCamera.deviceId : cameras[0].deviceId; + } + } catch (err) { + console.error("Error enumerating devices:", err); } + } - let options = { highlightScanRegion: true, highlightCodeOutline: true }; - let cb = ({ data }) => - scanner.stop() || goto(`/send/${encodeURIComponent(data)}`); - scanner = new QrScanner(vid, cb, options); - await tick(); - scanner.start(); - }; + // Start the camera stream for a given deviceId + async function startCamera(deviceId) { + stopCameraStream(); + detectionResult = null; // reset detection on restart + try { + const constraints = deviceId + ? { video: { deviceId: { exact: deviceId } } } + : { video: { facingMode: "environment" } }; + const stream = await navigator.mediaDevices.getUserMedia(constraints); + videoEl.srcObject = stream; + await videoEl.play(); + } catch (err) { + console.error("Error accessing camera:", err); + } + } + + // Stop any active video stream and cancel the scanning loop + function stopCameraStream() { + if (videoEl && videoEl.srcObject) { + const stream = videoEl.srcObject; + if (stream.getTracks) { + stream.getTracks().forEach((track) => track.stop()); + } + } + if (scanningFrameId) { + cancelAnimationFrame(scanningFrameId); + } + } + + function handleDetection(barcode) { + detectionResult = barcode.rawValue; + stopCameraStream(); + goto(`/send/${encodeURIComponent(detectionResult)}`); + } + + // The scanning loop: draw the video frame onto a hidden canvas and detect QR codes. + function scanLoop() { + if (!videoEl.videoWidth || !videoEl.videoHeight) { + scanningFrameId = requestAnimationFrame(scanLoop); + return; + } + + const context = canvasEl.getContext("2d"); + if (!context) return; + // Ensure canvas dimensions match the video + canvasEl.width = videoEl.videoWidth; + canvasEl.height = videoEl.videoHeight; + context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height); + + barcodeDetector + .detect(canvasEl) + .then((barcodes) => { + if (barcodes.length > 0) { + handleDetection(barcodes[0]); + return; // Stop scanning on detection. + } + }) + .catch((err) => { + console.error("Detection error:", err); + }); + + // Continue scanning if no detection has occurred. + if (!detectionResult) { + scanningFrameId = requestAnimationFrame(scanLoop); + } + } + + // Handle change of camera selection + async function onCameraChange(event) { + selectedCamera = event.target.value; + await startCamera(selectedCamera); + videoEl.addEventListener("loadeddata", () => { + canvasEl.width = videoEl.videoWidth; + canvasEl.height = videoEl.videoHeight; + scanLoop(); + }); + } + + // Provide a way to restart scanning + async function restart() { + detectionResult = null; + await startCamera(selectedCamera); + videoEl.addEventListener("loadeddata", () => { + canvasEl.width = videoEl.videoWidth; + canvasEl.height = videoEl.videoHeight; + scanLoop(); + }); + } + + let initialize = async () => { + barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] }); + await getCameraDevices(); + await startCamera(selectedCamera); + videoEl.addEventListener("loadeddata", () => { + canvasEl.width = videoEl.videoWidth; + canvasEl.height = videoEl.videoHeight; + scanLoop(); + }); + }; onMount(initialize); - onDestroy(() => scanner?.stop()); + + onDestroy(() => { + stopCameraStream(); + }); </script> <svelte:window onresize={resize} /> - <div class="flex w-full mb-4 p-4"> - <div class="mx-auto rounded-3xl"> - <video - bind:this={vid} - class="border-4 rounded-3xl border-black max-h-[calc(100vh*0.7)]" -></video> + <div class="mx-auto rounded-3xl space-y-5"> + <video + bind:this={videoEl} + class="border-4 rounded-3xl border-black max-h-[calc(100vh*0.7)]" + ></video> + <canvas bind:this={canvasEl} style="display: none;"></canvas> + <div class="flex justify-center"> + <button class="btn !w-auto" onclick={back}>Cancel</button> + </div> + <div> + <select + id="cameraSelect" + bind:value={selectedCamera} + onchange={onCameraChange} + > + {#each cameras as camera} + <option value={camera.deviceId}> + {camera.label || `Camera ${camera.deviceId}`} + </option> + {/each} + </select> + </div> </div> </div> -<div class="flex justify-center"> - <button class="btn !w-auto" onclick={back}>Cancel</button> -</div> - <style> video { - transition: transform 0.3s ease-out; + width: 100%; + max-width: 600px; + border: 4px solid black; + border-radius: 1rem; transform: scale(1); + transition: transform 0.3s ease-out; } </style> |
