Files
front-Censo/src/components/BarcodeScanner.tsx
T

171 lines
5.0 KiB
TypeScript
Raw Normal View History

2025-10-23 14:12:18 -06:00
"use client";
2025-11-12 17:05:47 -06:00
import { useEffect, useRef } from "react";
import toast from "react-hot-toast";
2025-11-12 17:50:38 -06:00
import { BrowserMultiFormatReader } from "@zxing/browser";
2025-11-12 17:59:56 -06:00
import { NotFoundException } from "@zxing/library";
2025-10-27 17:41:24 -06:00
interface BarcodeScannerProps {
onScan: (code: string) => void;
}
export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
2025-11-12 17:05:47 -06:00
const videoRef = useRef<HTMLVideoElement>(null);
const lastScan = useRef<string | null>(null);
2025-10-23 14:12:18 -06:00
useEffect(() => {
2025-11-12 17:05:47 -06:00
let stream: MediaStream | null = null;
let animationFrame: number;
2025-11-12 17:50:38 -06:00
let zxingReader: BrowserMultiFormatReader | null = null;
let detector: any = null;
/** 🚀 Intentar usar BarcodeDetector API (moderno) */
const startBarcodeDetector = async () => {
const supported = "BarcodeDetector" in window;
if (!supported) {
console.warn("BarcodeDetector no soportado, usando ZXing...");
return startZXing();
}
2025-10-27 17:41:24 -06:00
2025-11-12 17:05:47 -06:00
try {
stream = await navigator.mediaDevices.getUserMedia({
2025-11-12 17:59:56 -06:00
video: { facingMode: { ideal: "environment" } }, // 👈 fuerza cámara trasera
2025-11-12 17:05:47 -06:00
});
if (!videoRef.current) return;
videoRef.current.srcObject = stream;
2025-11-12 17:50:38 -06:00
await new Promise<void>((resolve) => {
videoRef.current!.onloadedmetadata = async () => {
try {
await videoRef.current!.play();
resolve();
2025-11-12 17:59:56 -06:00
} catch {
2025-11-12 17:50:38 -06:00
resolve();
}
};
2025-11-12 17:05:47 -06:00
});
2025-11-12 17:50:38 -06:00
detector = new (window as any).BarcodeDetector({
formats: ["code_128", "ean_13", "qr_code"],
2025-11-12 17:05:47 -06:00
});
2025-11-12 17:50:38 -06:00
const detect = async () => {
if (!videoRef.current) return;
2025-11-12 17:05:47 -06:00
2025-11-12 17:50:38 -06:00
if (videoRef.current.readyState < 2) {
animationFrame = requestAnimationFrame(detect);
return;
}
2025-11-12 17:05:47 -06:00
try {
const barcodes = await detector.detect(videoRef.current);
if (barcodes.length > 0) {
2025-11-12 17:50:38 -06:00
const raw = barcodes[0].rawValue.trim();
if (raw && raw !== lastScan.current) {
lastScan.current = raw;
const processed = raw.startsWith("0") ? raw.slice(1) : raw;
2025-11-12 17:05:47 -06:00
onScan(processed);
}
}
2025-11-12 17:50:38 -06:00
} catch (err: any) {
if (
err.message?.includes("Invalid element") ||
err.name === "InvalidStateError"
) {
2025-11-12 17:59:56 -06:00
// ignorar mientras no haya frame válido
2025-11-12 17:50:38 -06:00
} else {
console.error("Error detectando código:", err);
}
2025-11-12 17:05:47 -06:00
}
2025-11-12 17:50:38 -06:00
2025-11-12 17:05:47 -06:00
animationFrame = requestAnimationFrame(detect);
};
detect();
} catch (err) {
2025-11-12 17:50:38 -06:00
console.error("Error con BarcodeDetector:", err);
2025-11-12 17:05:47 -06:00
toast.error("No se pudo acceder a la cámara");
2025-11-12 17:50:38 -06:00
startZXing();
}
};
const startZXing = async () => {
try {
zxingReader = new BrowserMultiFormatReader();
const devices = await BrowserMultiFormatReader.listVideoInputDevices();
if (devices.length === 0) {
toast.error("No se detectó cámara disponible");
return;
}
2025-11-12 17:59:56 -06:00
// 🔍 Intentar encontrar la cámara trasera
const backCamera =
devices.find((d) =>
d.label.toLowerCase().includes("back")
) || devices.find((d) =>
d.label.toLowerCase().includes("rear")
) || devices[devices.length - 1]; // fallback
const selectedDeviceId = backCamera.deviceId;
console.log("Usando cámara:", backCamera.label || selectedDeviceId);
2025-11-12 17:50:38 -06:00
await zxingReader.decodeFromVideoDevice(
selectedDeviceId,
videoRef.current!,
(result, err) => {
if (result) {
const code = result.getText().trim();
if (code && code !== lastScan.current) {
lastScan.current = code;
const processed = code.startsWith("0") ? code.slice(1) : code;
onScan(processed);
}
2025-11-12 17:59:56 -06:00
} else if (err && !(err instanceof NotFoundException)) {
console.error("Error ZXing:", err);
2025-11-12 17:50:38 -06:00
}
}
);
} catch (error) {
console.error("Error al iniciar ZXing:", error);
toast.error("Error al iniciar escáner");
2025-10-27 17:41:24 -06:00
}
};
2025-10-23 14:12:18 -06:00
2025-11-12 17:50:38 -06:00
startBarcodeDetector();
2025-10-27 17:41:24 -06:00
2025-11-12 17:59:59 -06:00
2025-10-27 17:41:24 -06:00
return () => {
2025-11-12 17:05:47 -06:00
if (animationFrame) cancelAnimationFrame(animationFrame);
if (stream) stream.getTracks().forEach((t) => t.stop());
2025-11-12 17:50:38 -06:00
if (zxingReader) {
if (typeof (zxingReader as any).stopContinuousDecode === "function") {
(zxingReader as any).stopContinuousDecode();
} else if (typeof (zxingReader as any).reset === "function") {
(zxingReader as any).reset();
}
}
2025-10-27 17:41:24 -06:00
};
}, [onScan]);
2025-10-23 14:12:18 -06:00
return (
2025-11-12 17:05:47 -06:00
<video
ref={videoRef}
2025-10-23 14:12:18 -06:00
style={{
width: "100%",
2025-11-12 17:05:47 -06:00
maxWidth: "480px",
2025-10-27 17:41:24 -06:00
height: "300px",
border: "2px solid #0056b3",
2025-11-12 17:05:47 -06:00
borderRadius: "12px",
objectFit: "cover",
2025-10-23 14:12:18 -06:00
}}
2025-11-12 17:50:38 -06:00
muted
playsInline
2025-10-23 14:12:18 -06:00
/>
);
2025-10-27 18:17:53 -06:00
}
2025-11-12 17:59:59 -06:00
//Chatgpt
//Gemini