@@ -37,17 +37,74 @@ const QRScanner: React.FC<QRScannerProps> = ({ onScan, onClose, isOpen }) => {
3737 setIsLoading ( true )
3838 setError ( '' )
3939
40- // Get camera stream directly for better compatibility
41- const stream = await navigator . mediaDevices . getUserMedia ( {
42- video : {
43- facingMode : 'environment' ,
44- width : { ideal : 1280 , max : 1920 } ,
45- height : { ideal : 720 , max : 1080 } ,
46- } ,
47- } )
40+ if ( ! window . isSecureContext && window . location . hostname !== 'localhost' && window . location . hostname !== '127.0.0.1' ) {
41+ throw new Error ( 'Camera access requires HTTPS or localhost. Please use a secure connection.' )
42+ }
43+
44+ if ( ! navigator . mediaDevices || ! navigator . mediaDevices . getUserMedia ) {
45+ throw new Error ( 'Camera API is not available in this browser.' )
46+ }
4847
49- // Set up video element with the stream so the scanner can reuse it
48+ if ( scannerRef . current ) {
49+ scannerRef . current . destroy ( )
50+ scannerRef . current = null
51+ }
52+
53+ if ( videoRef . current . srcObject instanceof MediaStream ) {
54+ const existingStream = videoRef . current . srcObject as MediaStream
55+ existingStream . getTracks ( ) . forEach ( track => track . stop ( ) )
56+ videoRef . current . srcObject = null
57+ }
58+
59+ let stream : MediaStream
60+
61+ try {
62+ stream = await navigator . mediaDevices . getUserMedia ( {
63+ video : {
64+ facingMode : { ideal : 'environment' } ,
65+ width : { ideal : 1280 , max : 1920 } ,
66+ height : { ideal : 720 , max : 1080 }
67+ }
68+ } )
69+ } catch {
70+ stream = await navigator . mediaDevices . getUserMedia ( { video : true } )
71+ }
72+
73+ // Set up video element with the stream and ensure playback starts
5074 videoRef . current . srcObject = stream
75+ videoRef . current . setAttribute ( 'playsinline' , 'true' )
76+ videoRef . current . muted = true
77+
78+ await new Promise < void > ( ( resolve , reject ) => {
79+ const video = videoRef . current
80+ if ( ! video ) {
81+ reject ( new Error ( 'Video element not available' ) )
82+ return
83+ }
84+
85+ if ( video . readyState >= 2 ) {
86+ resolve ( )
87+ return
88+ }
89+
90+ const timeoutId = window . setTimeout ( ( ) => {
91+ video . onloadedmetadata = null
92+ reject ( new Error ( 'Timed out while waiting for camera stream' ) )
93+ } , 7000 )
94+
95+ video . onloadedmetadata = ( ) => {
96+ window . clearTimeout ( timeoutId )
97+ video . onloadedmetadata = null
98+ resolve ( )
99+ }
100+ } )
101+
102+ await videoRef . current . play ( )
103+
104+ // Persist the stream to ensure it can be stopped on cleanup
105+ setVideoStream ( stream )
106+ setHasPermission ( true )
107+ setIsLoading ( false )
51108
52109 // Initialize QR scanner on the video element
53110 const scanner = new QrScanner (
@@ -61,21 +118,20 @@ const QRScanner: React.FC<QRScannerProps> = ({ onScan, onClose, isOpen }) => {
61118 preferredCamera : 'environment' ,
62119 highlightScanRegion : true ,
63120 highlightCodeOutline : true ,
64- returnDetailedScanResult : true ,
121+ returnDetailedScanResult : true
65122 }
66123 )
67124
68125 // Configure scanner options
69126 scanner . setInversionMode ( 'both' )
70127 scannerRef . current = scanner
71128
72- await scanner . start ( )
73-
74- // Persist the stream to ensure it can be stopped on cleanup
75- setVideoStream ( stream )
76-
77- setHasPermission ( true )
78- setIsLoading ( false )
129+ await Promise . race ( [
130+ scanner . start ( ) ,
131+ new Promise ( ( _ , reject ) => {
132+ window . setTimeout ( ( ) => reject ( new Error ( 'Camera initialization timed out. Please try again.' ) ) , 7000 )
133+ } )
134+ ] )
79135
80136 } catch ( err : any ) {
81137 // Ensure any partially opened resources are released
@@ -99,7 +155,7 @@ const QRScanner: React.FC<QRScannerProps> = ({ onScan, onClose, isOpen }) => {
99155 setError ( 'Camera not supported in this browser. Try using Chrome, Firefox, or Safari.' )
100156 } else if ( err . name === 'NotReadableError' ) {
101157 setError ( 'Camera is already in use by another application' )
102- } else if ( err . message ?. includes ( 'secure context' ) ) {
158+ } else if ( err . message ?. includes ( 'secure context' ) || err . message ?. includes ( 'HTTPS or localhost' ) ) {
103159 setError ( 'Camera access requires HTTPS or localhost. Please use a secure connection.' )
104160 } else {
105161 setError ( `Failed to initialize camera: ${ err . message || 'Unknown error' } ` )
@@ -211,11 +267,16 @@ const QRScanner: React.FC<QRScannerProps> = ({ onScan, onClose, isOpen }) => {
211267 { isLoading && (
212268 < Box
213269 sx = { {
270+ position : 'absolute' ,
271+ inset : 0 ,
214272 display : 'flex' ,
215273 flexDirection : 'column' ,
216274 alignItems : 'center' ,
217275 gap : 2 ,
218276 color : 'white' ,
277+ justifyContent : 'center' ,
278+ backgroundColor : 'rgba(0, 0, 0, 0.55)' ,
279+ zIndex : 2
219280 } }
220281 >
221282 < CircularProgress color = "inherit" />
@@ -293,7 +354,7 @@ const QRScanner: React.FC<QRScannerProps> = ({ onScan, onClose, isOpen }) => {
293354 width : '100%' ,
294355 height : '100%' ,
295356 objectFit : 'cover' ,
296- display : isLoading || error ? 'none' : 'block' ,
357+ display : error ? 'none' : 'block'
297358 } }
298359 playsInline
299360 muted
0 commit comments