1+ import jsQR from "jsqr" ;
12import { useEffect , useRef , useState } from "react" ;
23
34interface UseQrScannerOptions {
@@ -9,7 +10,6 @@ interface UseQrScannerOptions {
910interface UseQrScannerReturn {
1011 videoRef : React . RefObject < HTMLVideoElement | null > ;
1112 error : string | null ;
12- supported : boolean ;
1313}
1414
1515const DETECTION_INTERVAL_MS = 100 ; // ~10 FPS
@@ -21,19 +21,18 @@ export function useQrScanner({
2121} : UseQrScannerOptions ) : UseQrScannerReturn {
2222 const videoRef = useRef < HTMLVideoElement | null > ( null ) ;
2323 const streamRef = useRef < MediaStream | null > ( null ) ;
24+ const canvasRef = useRef < HTMLCanvasElement | null > ( null ) ;
2425 const onDetectRef = useRef ( onDetect ) ;
2526 const [ error , setError ] = useState < string | null > ( null ) ;
2627
27- const supported = "BarcodeDetector" in window ;
28-
2928 // Keep callback ref current without restarting effects
3029 useEffect ( ( ) => {
3130 onDetectRef . current = onDetect ;
3231 } ) ;
3332
3433 // Camera lifecycle
3534 useEffect ( ( ) => {
36- if ( ! enabled || ! supported ) return ;
35+ if ( ! enabled ) return ;
3736
3837 let cancelled = false ;
3938
@@ -71,13 +70,17 @@ export function useQrScanner({
7170 video . srcObject = null ;
7271 }
7372 } ;
74- } , [ enabled , supported ] ) ;
73+ } , [ enabled ] ) ;
7574
7675 // Detection loop
7776 useEffect ( ( ) => {
78- if ( ! enabled || paused || ! supported ) return ;
77+ if ( ! enabled || paused ) return ;
78+
79+ if ( ! canvasRef . current ) {
80+ canvasRef . current = document . createElement ( "canvas" ) ;
81+ }
82+ const canvas = canvasRef . current ;
7983
80- const detector = new BarcodeDetector ( { formats : [ "qr_code" ] } ) ;
8184 let rafId : number ;
8285 let lastTime = 0 ;
8386 let detected = false ;
@@ -93,24 +96,31 @@ export function useQrScanner({
9396 if ( ! video || video . readyState < HTMLMediaElement . HAVE_ENOUGH_DATA )
9497 return ;
9598
96- detector
97- . detect ( video )
98- . then ( ( barcodes ) => {
99- if ( detected || barcodes . length === 0 ) return ;
100- detected = true ;
101- onDetectRef . current ( barcodes [ 0 ] . rawValue ) ;
102- } )
103- . catch ( ( ) => {
104- // Detection errors are non-fatal, continue loop
105- } ) ;
99+ const { videoWidth, videoHeight } = video ;
100+ if ( videoWidth === 0 || videoHeight === 0 ) return ;
101+
102+ const ctx = canvas . getContext ( "2d" , { willReadFrequently : true } ) ;
103+ if ( ! ctx ) return ;
104+
105+ canvas . width = videoWidth ;
106+ canvas . height = videoHeight ;
107+ ctx . drawImage ( video , 0 , 0 , videoWidth , videoHeight ) ;
108+
109+ const imageData = ctx . getImageData ( 0 , 0 , videoWidth , videoHeight ) ;
110+ const result = jsQR ( imageData . data , videoWidth , videoHeight ) ;
111+
112+ if ( result ) {
113+ detected = true ;
114+ onDetectRef . current ( result . data ) ;
115+ }
106116 }
107117
108118 rafId = requestAnimationFrame ( tick ) ;
109119
110120 return ( ) => {
111121 cancelAnimationFrame ( rafId ) ;
112122 } ;
113- } , [ enabled , paused , supported ] ) ;
123+ } , [ enabled , paused ] ) ;
114124
115- return { videoRef, error, supported } ;
125+ return { videoRef, error } ;
116126}
0 commit comments