aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-03-14 09:18:38 -0700
committerAdam Soltys <[email protected]>2025-03-14 09:18:38 -0700
commit4d458fafd70411e3826bf6c6b8257e24a54b34c9 (patch)
treefeb7fb95d8bb175c1c6e88ef7fc1a2e5b0f4be3d /src
parentd6d36b52f16d02bd112483ae9fe69876623935b6 (diff)
downloadcoinos-ui-4d458fafd70411e3826bf6c6b8257e24a54b34c9.tar.gz
coinos-ui-4d458fafd70411e3826bf6c6b8257e24a54b34c9.zip
new scanner
Diffstat (limited to 'src')
-rw-r--r--src/routes/(app)/scan/+page.svelte186
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>