import { ref, onMounted, onUnmounted } from 'vue' export function useQrDetection(videoRef, overlayCanvasRef) { let barcodeDetector = null // must be plain variable, NOT a Vue ref (Proxy breaks native private fields) const isDetecting = ref(false) const error = ref('') let scanRafId = null // Function to initialize detector const initDetector = async () => { if (!barcodeDetector) { if ('BarcodeDetector' in window) { try { const formats = await window.BarcodeDetector.getSupportedFormats() if (formats.includes('qr_code')) { barcodeDetector = new window.BarcodeDetector({ formats: ['qr_code'] }) } else { barcodeDetector = new window.BarcodeDetector() } } catch (e) { barcodeDetector = new window.BarcodeDetector() } } else { error.value = 'Barcode Detection API not supported on this device/browser.' } } } const paintDetections = (codes) => { const canvas = overlayCanvasRef.value const video = videoRef.value if (!canvas || !video) return const ctx = canvas.getContext('2d') const { width, height } = canvas.getBoundingClientRect() // Update canvas size if needed (to match CSS size) if (canvas.width !== width || canvas.height !== height) { canvas.width = width canvas.height = height } ctx.clearRect(0, 0, width, height) if (!codes || codes.length === 0) return const vw = video.videoWidth const vh = video.videoHeight if (!vw || !vh) return // Calculate object-fit: cover scaling const videoRatio = vw / vh const canvasRatio = width / height let drawWidth, drawHeight, startX, startY if (canvasRatio > videoRatio) { // Canvas is wider than video (video cropped top/bottom) drawWidth = width drawHeight = width / videoRatio startX = 0 startY = (height - drawHeight) / 2 } else { // Canvas is taller than video (video cropped left/right) drawHeight = height drawWidth = height * videoRatio startY = 0 startX = (width - drawWidth) / 2 } const scale = drawWidth / vw // Styles const styles = getComputedStyle(document.documentElement) const accent = styles.getPropertyValue('--primary-accent').trim() || '#00f2fe' ctx.lineWidth = 4 ctx.strokeStyle = accent ctx.fillStyle = accent codes.forEach(code => { const points = code.cornerPoints if (!points || points.length < 4) return ctx.beginPath() const transform = (p) => { let x = p.x * scale + startX let y = p.y * scale + startY return { x, y } } const p0 = transform(points[0]) ctx.moveTo(p0.x, p0.y) for (let i = 1; i < points.length; i++) { const p = transform(points[i]) ctx.lineTo(p.x, p.y) } ctx.closePath() ctx.stroke() // Draw corners points.forEach(p => { const tp = transform(p) ctx.beginPath() ctx.arc(tp.x, tp.y, 4, 0, Math.PI * 2) ctx.fill() }) }) } const startDetection = async (onDetectCallback) => { error.value = '' try { await initDetector() if (!barcodeDetector) { if (!error.value) error.value = 'Barcode Detector failed to initialize' return } isDetecting.value = true const detectLoop = async () => { const video = videoRef.value if (!isDetecting.value) return if (!video || video.readyState < 2) { scanRafId = requestAnimationFrame(detectLoop) return } try { const codes = await barcodeDetector.detect(video) paintDetections(codes) if (codes.length > 0 && onDetectCallback) { onDetectCallback(codes) } } catch (e) { // Silent catch for intermittent detection frames failing } if (isDetecting.value) { scanRafId = requestAnimationFrame(detectLoop) } } detectLoop() // start loop } catch (e) { error.value = `Detection error: ${e.message}` } } const stopDetection = () => { isDetecting.value = false if (scanRafId) cancelAnimationFrame(scanRafId) // Clear canvas if (overlayCanvasRef.value) { const ctx = overlayCanvasRef.value.getContext('2d') ctx.clearRect(0, 0, overlayCanvasRef.value.width, overlayCanvasRef.value.height) } } onUnmounted(() => { stopDetection() }) return { error, isDetecting, startDetection, stopDetection } }