mirror of
https://github.com/nimiq/qr-scanner.git
synced 2026-09-22 18:23:54 +00:00
Use native BarcodeDetector when supported
This commit is contained in:
7 files changed
+135
-87
No files matched your search
@@ -5,7 +5,8 @@ Javascript QR Code Scanner based on [Cosmo Wolfe's javascript port](https://gith
|
||||
In this library, several improvements have been applied over the original port:
|
||||
|
||||
- Web cam scanning support out of the box
|
||||
- Lightweight: ~50.5 kB (~13.0 kB gzipped) minified with Google's closure compiler.
|
||||
- Uses the browser's native [BarcodeDetector](https://web.dev/shape-detection/) [if available](https://github.com/WICG/shape-detection-api#overview)
|
||||
- Lightweight: ~50.5 kB (~13.0 kB gzipped) minified with Google's closure compiler. If the native `BarcodeDetector` is available, only ~6.8 kB (~2.7 kB gzipped) are loaded.
|
||||
- Improved performance and reduced memory footprint.
|
||||
- Runs in a WebWorker which keeps the main / UI thread responsive.
|
||||
- Can be configured for better performance on colored QR codes.
|
||||
@@ -179,7 +180,7 @@ URLs pointing to an image (if they are on the same origin or [CORS enabled](http
|
||||
|
||||
As an optional second parameter a region defined by `x`, `y`, `width` and `height` can be specified to which the search for a QR code should be restricted. By default, the region spans the whole image.
|
||||
|
||||
As an optional third parameter a manually created QR scanner worker instance to be reused can be specified. This improves performance if you're scanning a lot of images. A worker can be manually created via `new Worker(QrScanner.WORKER_PATH)`. By default, no worker is reused for single image scanning.
|
||||
As an optional third parameter a manually created QR scanner engine instance to be reused can be specified. This improves performance if you're scanning a lot of images. An engine can be manually created via `QrScanner.createQrEngine(QrScanner.WORKER_PATH)` (async). By default, no engine is reused for single image scanning.
|
||||
|
||||
As an optional fourth parameter a manually created canvas to be reused can be specified. This improves performance if you're scanning a lot of images. A canvas can be manually created via a `<canvas>` tag in your markup or `document.createElement('canvas')`. By default, no canvas is reused for single image scanning.
|
||||
|
||||
|
||||
Vendored
+12
-11
@@ -1,15 +1,16 @@
|
||||
class e{static hasCamera(){return navigator.mediaDevices?navigator.mediaDevices.enumerateDevices().then(a=>a.some(a=>"videoinput"===a.kind)).catch(()=>!1):Promise.resolve(!1)}constructor(a,b,c=this._onDecodeError.bind(this),d=e.DEFAULT_CANVAS_SIZE,f="environment"){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;this._preferredFacingMode=f;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(d=c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):
|
||||
this._onDecodeError=c;this.$canvas.width=d;this.$canvas.height=d;this._sourceRect={x:0,y:0,width:d,height:d};this._updateSourceRect=this._updateSourceRect.bind(this);this._onPlay=this._onPlay.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this.$video.playsInline=!0;this.$video.muted=!0;this.$video.disablePictureInPicture=!0;this.$video.addEventListener("loadedmetadata",this._updateSourceRect);this.$video.addEventListener("play",this._onPlay);document.addEventListener("visibilitychange",
|
||||
this._onVisibilityChange);this._qrWorker=new Worker(e.WORKER_PATH)}hasFlash(){if(!("ImageCapture"in window))return Promise.resolve(!1);let a=this.$video.srcObject?this.$video.srcObject.getVideoTracks()[0]:null;return a?(new ImageCapture(a)).getPhotoCapabilities().then(a=>a.fillLightMode.includes("flash")).catch(a=>{console.warn(a);return!1}):Promise.reject("Camera not started or not available")}isFlashOn(){return this._flashOn}toggleFlash(){return this._setFlash(!this._flashOn)}turnFlashOff(){return this._setFlash(!1)}turnFlashOn(){return this._setFlash(!0)}destroy(){this.$video.removeEventListener("loadedmetadata",
|
||||
this._updateSourceRect);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);this.stop();this._qrWorker.postMessage({type:"close"})}start(){if(this._active&&!this._paused)return Promise.resolve();"https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https.");this._active=!0;this._paused=!1;if(document.hidden)return Promise.resolve();clearTimeout(this._offTimeout);
|
||||
this._onVisibilityChange);this._qrEnginePromise=e.createQrEngine(e.WORKER_PATH)}hasFlash(){if(!("ImageCapture"in window))return Promise.resolve(!1);let a=this.$video.srcObject?this.$video.srcObject.getVideoTracks()[0]:null;return a?(new ImageCapture(a)).getPhotoCapabilities().then(a=>a.fillLightMode.includes("flash")).catch(a=>{console.warn(a);return!1}):Promise.reject("Camera not started or not available")}isFlashOn(){return this._flashOn}toggleFlash(){return this._setFlash(!this._flashOn)}turnFlashOff(){return this._setFlash(!1)}turnFlashOn(){return this._setFlash(!0)}destroy(){this.$video.removeEventListener("loadedmetadata",
|
||||
this._updateSourceRect);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);this.stop();e._postWorkerMessage(this._qrEnginePromise,"close")}start(){if(this._active&&!this._paused)return Promise.resolve();"https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https.");this._active=!0;this._paused=!1;if(document.hidden)return Promise.resolve();clearTimeout(this._offTimeout);
|
||||
this._offTimeout=null;if(this.$video.srcObject)return this.$video.play(),Promise.resolve();let a=this._preferredFacingMode;return this._getCameraStream(a,!0).catch(()=>{a="environment"===a?"user":"environment";return this._getCameraStream()}).then(b=>{a=this._getFacingMode(b)||a;this.$video.srcObject=b;this.$video.play();this._setVideoMirror(a)}).catch(a=>{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(){this._paused=!0;this._active&&(this.$video.pause(),this._offTimeout||(this._offTimeout=
|
||||
setTimeout(()=>{let a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(let b of a)b.stop();this._offTimeout=this.$video.srcObject=null},300)))}static scanImage(a,b=null,c=null,d=null,f=!1,g=!1){let h=!1,l=new Promise((l,g)=>{c||(c=new Worker(e.WORKER_PATH),h=!0,c.postMessage({type:"inversionMode",data:"both"}));let n,m,k;m=a=>{"qrResult"===a.data.type&&(c.removeEventListener("message",m),c.removeEventListener("error",k),clearTimeout(n),null!==a.data.data?l(a.data.data):g(e.NO_QR_CODE_FOUND))};
|
||||
k=a=>{c.removeEventListener("message",m);c.removeEventListener("error",k);clearTimeout(n);g("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",m);c.addEventListener("error",k);n=setTimeout(()=>k("timeout"),1E4);e._loadImage(a).then(a=>{a=e._getImageData(a,b,d,f);c.postMessage({type:"decode",data:a},[a.data.buffer])}).catch(k)});b&&g&&(l=l.catch(()=>e.scanImage(a,null,c,d,f)));return l=l.finally(()=>{h&&c.postMessage({type:"close"})})}setGrayscaleWeights(a,b,c,d=!0){this._qrWorker.postMessage({type:"grayscaleWeights",
|
||||
data:{red:a,green:b,blue:c,useIntegerApproximation:d}})}setInversionMode(a){this._qrWorker.postMessage({type:"inversionMode",data:a})}_onPlay(){this._updateSourceRect();this._scanFrame()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_updateSourceRect(){let a=Math.round(2/3*Math.min(this.$video.videoWidth,this.$video.videoHeight));this._sourceRect.width=this._sourceRect.height=a;this._sourceRect.x=(this.$video.videoWidth-a)/2;this._sourceRect.y=(this.$video.videoHeight-
|
||||
a)/2}_scanFrame(){if(!this._active||this.$video.paused||this.$video.ended)return!1;requestAnimationFrame(()=>{1>=this.$video.readyState?this._scanFrame():e.scanImage(this.$video,this._sourceRect,this._qrWorker,this.$canvas,!0).then(this._onDecode,a=>{this._active&&this._onDecodeError(a)}).then(()=>this._scanFrame())})}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(a,b=!1){let c=[{width:{min:1024}},{width:{min:768}},{}];a&&(b&&(a={exact:a}),c.forEach(b=>b.facingMode=a));
|
||||
return this._getMatchingCameraStream(c)}_getMatchingCameraStream(a){return navigator.mediaDevices&&0!==a.length?navigator.mediaDevices.getUserMedia({video:a.shift()}).catch(()=>this._getMatchingCameraStream(a)):Promise.reject("Camera not found.")}_setFlash(a){return this.hasFlash().then(b=>b?this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:a}]}):Promise.reject("No flash available")).then(()=>this._flashOn=a)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===
|
||||
a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _getImageData(a,b=null,c=null,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,g=b&&b.y?b.y:0,h=b&&b.width?b.width:a.width||a.videoWidth;b=b&&b.height?b.height:a.height||a.videoHeight;d||c.width===h&&c.height===b||(c.width=h,c.height=b);d=c.getContext("2d",{alpha:!1});d.imageSmoothingEnabled=!1;d.drawImage(a,f,g,h,
|
||||
b,0,0,c.width,c.height);return d.getImageData(0,0,c.width,c.height)}static _loadImage(a){if(a instanceof HTMLCanvasElement||a instanceof HTMLVideoElement||window.ImageBitmap&&a instanceof window.ImageBitmap||window.OffscreenCanvas&&a instanceof window.OffscreenCanvas)return Promise.resolve(a);if(a instanceof Image)return e._awaitImageLoad(a).then(()=>a);if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):
|
||||
a;return e._awaitImageLoad(b).then(()=>{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src);return b})}return Promise.reject("Unsupported image type.")}static _awaitImageLoad(a){return new Promise((b,c)=>{if(a.complete&&0!==a.naturalWidth)b();else{let d,f;d=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);b()};f=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);c("Image load error")};a.addEventListener("load",d);a.addEventListener("error",f)}})}}
|
||||
e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e.WORKER_PATH="qr-scanner-worker.min.js";export default e
|
||||
setTimeout(()=>{let a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(let b of a)b.stop();this._offTimeout=this.$video.srcObject=null},300)))}static scanImage(a,b=null,c=null,d=null,f=!1,k=!1){let g=c instanceof Worker,h=Promise.all([c||e.createQrEngine(),e._loadImage(a)]).then(([a,h])=>{c=a;let k;[d,k]=this._drawToCanvas(h,b,d,f);return c instanceof Worker?(g||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let f,l,g;l=d=>{"qrResult"===d.data.type&&(c.removeEventListener("message",
|
||||
l),c.removeEventListener("error",g),clearTimeout(f),null!==d.data.data?a(d.data.data):b(e.NO_QR_CODE_FOUND))};g=a=>{c.removeEventListener("message",l);c.removeEventListener("error",g);clearTimeout(f);b("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",l);c.addEventListener("error",g);f=setTimeout(()=>g("timeout"),1E4);let h=k.getImageData(0,0,d.width,d.height);c.postMessage({type:"decode",data:h},[h.data.buffer])})):new Promise((a,b)=>{let f=setTimeout(()=>b("Scanner error: timeout"),
|
||||
1E4);c.detect(d).then(c=>{c.length?a(c[0].rawValue):b(e.NO_QR_CODE_FOUND)}).catch(a=>b("Scanner error: "+(a.message||a))).finally(()=>clearTimeout(f))})});b&&k&&(h=h.catch(()=>e.scanImage(a,null,c,d,f)));return h=h.finally(()=>{g||e._postWorkerMessage(c,"close")})}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:d})}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static createQrEngine(a=
|
||||
e.WORKER_PATH){return("BarcodeDetector"in window?BarcodeDetector.getSupportedFormats():Promise.resolve([])).then(b=>-1!==b.indexOf("qr_code")?new BarcodeDetector({formats:["qr_code"]}):new Worker(a))}_onPlay(){this._updateSourceRect();this._scanFrame()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_updateSourceRect(){let a=Math.round(2/3*Math.min(this.$video.videoWidth,this.$video.videoHeight));this._sourceRect.width=this._sourceRect.height=a;this._sourceRect.x=(this.$video.videoWidth-
|
||||
a)/2;this._sourceRect.y=(this.$video.videoHeight-a)/2}_scanFrame(){if(!this._active||this.$video.paused||this.$video.ended)return!1;requestAnimationFrame(()=>{1>=this.$video.readyState?this._scanFrame():this._qrEnginePromise.then(a=>e.scanImage(this.$video,this._sourceRect,a,this.$canvas,!0)).then(this._onDecode,a=>{this._active&&this._onDecodeError(a)}).then(()=>this._scanFrame())})}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(a,b=!1){let c=[{width:{min:1024}},{width:{min:768}},
|
||||
{}];a&&(b&&(a={exact:a}),c.forEach(b=>b.facingMode=a));return this._getMatchingCameraStream(c)}_getMatchingCameraStream(a){return navigator.mediaDevices&&0!==a.length?navigator.mediaDevices.getUserMedia({video:a.shift()}).catch(()=>this._getMatchingCameraStream(a)):Promise.reject("Camera not found.")}_setFlash(a){return this.hasFlash().then(b=>b?this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:a}]}):Promise.reject("No flash available")).then(()=>this._flashOn=a)}_setVideoMirror(a){this.$video.style.transform=
|
||||
"scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b=null,c=null,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,k=b&&b.y?b.y:0,g=b&&b.width?b.width:a.width||a.videoWidth;b=b&&b.height?b.height:a.height||a.videoHeight;d||c.width===g&&c.height===b||(c.width=g,c.height=b);d=c.getContext("2d",{alpha:!1});d.imageSmoothingEnabled=!1;
|
||||
d.drawImage(a,f,k,g,b,0,0,c.width,c.height);return[c,d]}static _loadImage(a){if(a instanceof HTMLCanvasElement||a instanceof HTMLVideoElement||window.ImageBitmap&&a instanceof window.ImageBitmap||window.OffscreenCanvas&&a instanceof window.OffscreenCanvas)return Promise.resolve(a);if(a instanceof Image)return e._awaitImageLoad(a).then(()=>a);if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):
|
||||
a;return e._awaitImageLoad(b).then(()=>{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src);return b})}return Promise.reject("Unsupported image type.")}static _awaitImageLoad(a){return new Promise((b,c)=>{if(a.complete&&0!==a.naturalWidth)b();else{let d,f;d=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);b()};f=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);c("Image load error")};a.addEventListener("load",d);a.addEventListener("error",f)}})}static _postWorkerMessage(a,
|
||||
b,c){return Promise.resolve(a).then(a=>{a instanceof Worker&&a.postMessage({type:b,data:c})})}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e.WORKER_PATH="qr-scanner-worker.min.js";export default e
|
||||
//# sourceMappingURL=qr-scanner.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"qr-scanner.min.js","sources":["src/qr-scanner.js"],"sourcesContent":["export default class QrScanner {\n /* async */\n static hasCamera() {\n if (!navigator.mediaDevices) return Promise.resolve(false);\n // note that enumerateDevices can always be called and does not prompt the user for permission. However, device\n // labels are only readable if served via https and an active media stream exists or permanent permission is\n // given. That doesn't matter for us though as we don't require labels.\n return navigator.mediaDevices.enumerateDevices()\n .then(devices => devices.some(device => device.kind === 'videoinput'))\n .catch(() => false);\n }\n\n constructor(\n video,\n onDecode,\n canvasSizeOrOnDecodeError = this._onDecodeError.bind(this),\n canvasSize = QrScanner.DEFAULT_CANVAS_SIZE,\n preferredFacingMode = 'environment'\n ) {\n this.$video = video;\n this.$canvas = document.createElement('canvas');\n this._onDecode = onDecode;\n this._preferredFacingMode = preferredFacingMode;\n this._active = false;\n this._paused = false;\n this._flashOn = false;\n\n if (typeof canvasSizeOrOnDecodeError === 'number') {\n // legacy function signature where canvas size is the third argument\n canvasSize = canvasSizeOrOnDecodeError;\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n this._onDecodeError = canvasSizeOrOnDecodeError;\n }\n\n this.$canvas.width = canvasSize;\n this.$canvas.height = canvasSize;\n this._sourceRect = {\n x: 0,\n y: 0,\n width: canvasSize,\n height: canvasSize\n };\n\n this._updateSourceRect = this._updateSourceRect.bind(this);\n this._onPlay = this._onPlay.bind(this);\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\n\n // Allow inline playback on iPhone instead of requiring full screen playback,\n // see https://webkit.org/blog/6784/new-video-policies-for-ios/\n this.$video.playsInline = true;\n // Allow play() on iPhone without requiring a user gesture. Should not really be needed as camera stream\n // includes no audio, but just to be safe.\n this.$video.muted = true;\n this.$video.disablePictureInPicture = true;\n this.$video.addEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.addEventListener('play', this._onPlay);\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n\n this._qrWorker = new Worker(QrScanner.WORKER_PATH);\n }\n\n /* async */\n hasFlash() {\n if (!('ImageCapture' in window)) {\n return Promise.resolve(false);\n }\n\n const track = this.$video.srcObject ? this.$video.srcObject.getVideoTracks()[0] : null;\n if (!track) {\n return Promise.reject('Camera not started or not available');\n }\n\n const imageCapture = new ImageCapture(track);\n return imageCapture.getPhotoCapabilities()\n .then((result) => {\n return result.fillLightMode.includes('flash');\n })\n .catch((error) => {\n console.warn(error);\n return false;\n });\n }\n\n isFlashOn() {\n return this._flashOn;\n }\n\n /* async */\n toggleFlash() {\n return this._setFlash(!this._flashOn);\n }\n\n /* async */\n turnFlashOff() {\n return this._setFlash(false);\n }\n\n /* async */\n turnFlashOn() {\n return this._setFlash(true);\n }\n\n destroy() {\n this.$video.removeEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.removeEventListener('play', this._onPlay);\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n\n this.stop();\n this._qrWorker.postMessage({\n type: 'close'\n });\n }\n\n /* async */\n start() {\n if (this._active && !this._paused) {\n return Promise.resolve();\n }\n if (window.location.protocol !== 'https:') {\n // warn but try starting the camera anyways\n console.warn('The camera stream is only accessible if the page is transferred via https.');\n }\n this._active = true;\n this._paused = false;\n if (document.hidden) {\n // camera will be started as soon as tab is in foreground\n return Promise.resolve();\n }\n clearTimeout(this._offTimeout);\n this._offTimeout = null;\n if (this.$video.srcObject) {\n // camera stream already/still set\n this.$video.play();Line truncated
|
||||
{"version":3,"file":"qr-scanner.min.js","sources":["src/qr-scanner.js"],"sourcesContent":["export default class QrScanner {\n /* async */\n static hasCamera() {\n if (!navigator.mediaDevices) return Promise.resolve(false);\n // note that enumerateDevices can always be called and does not prompt the user for permission. However, device\n // labels are only readable if served via https and an active media stream exists or permanent permission is\n // given. That doesn't matter for us though as we don't require labels.\n return navigator.mediaDevices.enumerateDevices()\n .then(devices => devices.some(device => device.kind === 'videoinput'))\n .catch(() => false);\n }\n\n constructor(\n video,\n onDecode,\n canvasSizeOrOnDecodeError = this._onDecodeError.bind(this),\n canvasSize = QrScanner.DEFAULT_CANVAS_SIZE,\n preferredFacingMode = 'environment'\n ) {\n this.$video = video;\n this.$canvas = document.createElement('canvas');\n this._onDecode = onDecode;\n this._preferredFacingMode = preferredFacingMode;\n this._active = false;\n this._paused = false;\n this._flashOn = false;\n\n if (typeof canvasSizeOrOnDecodeError === 'number') {\n // legacy function signature where canvas size is the third argument\n canvasSize = canvasSizeOrOnDecodeError;\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n this._onDecodeError = canvasSizeOrOnDecodeError;\n }\n\n this.$canvas.width = canvasSize;\n this.$canvas.height = canvasSize;\n this._sourceRect = {\n x: 0,\n y: 0,\n width: canvasSize,\n height: canvasSize\n };\n\n this._updateSourceRect = this._updateSourceRect.bind(this);\n this._onPlay = this._onPlay.bind(this);\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\n\n // Allow inline playback on iPhone instead of requiring full screen playback,\n // see https://webkit.org/blog/6784/new-video-policies-for-ios/\n this.$video.playsInline = true;\n // Allow play() on iPhone without requiring a user gesture. Should not really be needed as camera stream\n // includes no audio, but just to be safe.\n this.$video.muted = true;\n this.$video.disablePictureInPicture = true;\n this.$video.addEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.addEventListener('play', this._onPlay);\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n\n this._qrEnginePromise = QrScanner.createQrEngine(QrScanner.WORKER_PATH);\n }\n\n /* async */\n hasFlash() {\n if (!('ImageCapture' in window)) {\n return Promise.resolve(false);\n }\n\n const track = this.$video.srcObject ? this.$video.srcObject.getVideoTracks()[0] : null;\n if (!track) {\n return Promise.reject('Camera not started or not available');\n }\n\n const imageCapture = new ImageCapture(track);\n return imageCapture.getPhotoCapabilities()\n .then((result) => {\n return result.fillLightMode.includes('flash');\n })\n .catch((error) => {\n console.warn(error);\n return false;\n });\n }\n\n isFlashOn() {\n return this._flashOn;\n }\n\n /* async */\n toggleFlash() {\n return this._setFlash(!this._flashOn);\n }\n\n /* async */\n turnFlashOff() {\n return this._setFlash(false);\n }\n\n /* async */\n turnFlashOn() {\n return this._setFlash(true);\n }\n\n destroy() {\n this.$video.removeEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.removeEventListener('play', this._onPlay);\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n\n this.stop();\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'close');\n }\n\n /* async */\n start() {\n if (this._active && !this._paused) {\n return Promise.resolve();\n }\n if (window.location.protocol !== 'https:') {\n // warn but try starting the camera anyways\n console.warn('The camera stream is only accessible if the page is transferred via https.');\n }\n this._active = true;\n this._paused = false;\n if (document.hidden) {\n // camera will be started as soon as tab is in foreground\n return Promise.resolve();\n }\n clearTimeout(this._offTimeout);\n this._offTimeout = null;\n if (this.$video.srcObject) {\n // camera stream already/still set\n this.Line truncated
|
||||
Vendored
+16
-14
@@ -1,15 +1,17 @@
|
||||
'use strict';(function(d,a){"object"===typeof exports&&"undefined"!==typeof module?module.exports=a():"function"===typeof define&&define.amd?define(a):(d=d||self,d.QrScanner=a())})(this,function(){class d{static hasCamera(){return navigator.mediaDevices?navigator.mediaDevices.enumerateDevices().then(a=>a.some(a=>"videoinput"===a.kind)).catch(()=>!1):Promise.resolve(!1)}constructor(a,b,c=this._onDecodeError.bind(this),e=d.DEFAULT_CANVAS_SIZE,f="environment"){this.$video=a;this.$canvas=document.createElement("canvas");
|
||||
this._onDecode=b;this._preferredFacingMode=f;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(e=c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;this.$canvas.width=e;this.$canvas.height=e;this._sourceRect={x:0,y:0,width:e,height:e};this._updateSourceRect=this._updateSourceRect.bind(this);this._onPlay=this._onPlay.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this.$video.playsInline=
|
||||
!0;this.$video.muted=!0;this.$video.disablePictureInPicture=!0;this.$video.addEventListener("loadedmetadata",this._updateSourceRect);this.$video.addEventListener("play",this._onPlay);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrWorker=new Worker(d.WORKER_PATH)}hasFlash(){if(!("ImageCapture"in window))return Promise.resolve(!1);let a=this.$video.srcObject?this.$video.srcObject.getVideoTracks()[0]:null;return a?(new ImageCapture(a)).getPhotoCapabilities().then(a=>a.fillLightMode.includes("flash")).catch(a=>
|
||||
{console.warn(a);return!1}):Promise.reject("Camera not started or not available")}isFlashOn(){return this._flashOn}toggleFlash(){return this._setFlash(!this._flashOn)}turnFlashOff(){return this._setFlash(!1)}turnFlashOn(){return this._setFlash(!0)}destroy(){this.$video.removeEventListener("loadedmetadata",this._updateSourceRect);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);this.stop();this._qrWorker.postMessage({type:"close"})}start(){if(this._active&&
|
||||
!this._paused)return Promise.resolve();"https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https.");this._active=!0;this._paused=!1;if(document.hidden)return Promise.resolve();clearTimeout(this._offTimeout);this._offTimeout=null;if(this.$video.srcObject)return this.$video.play(),Promise.resolve();let a=this._preferredFacingMode;return this._getCameraStream(a,!0).catch(()=>{a="environment"===a?"user":"environment";return this._getCameraStream()}).then(b=>
|
||||
{a=this._getFacingMode(b)||a;this.$video.srcObject=b;this.$video.play();this._setVideoMirror(a)}).catch(a=>{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(){this._paused=!0;this._active&&(this.$video.pause(),this._offTimeout||(this._offTimeout=setTimeout(()=>{let a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(let b of a)b.stop();this._offTimeout=this.$video.srcObject=null},300)))}static scanImage(a,b=null,c=null,e=null,f=!1,g=!1){let h=!1,l=new Promise((l,g)=>
|
||||
{c||(c=new Worker(d.WORKER_PATH),h=!0,c.postMessage({type:"inversionMode",data:"both"}));let n,m,k;m=a=>{"qrResult"===a.data.type&&(c.removeEventListener("message",m),c.removeEventListener("error",k),clearTimeout(n),null!==a.data.data?l(a.data.data):g(d.NO_QR_CODE_FOUND))};k=a=>{c.removeEventListener("message",m);c.removeEventListener("error",k);clearTimeout(n);g("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",m);c.addEventListener("error",k);n=setTimeout(()=>k("timeout"),
|
||||
1E4);d._loadImage(a).then(a=>{a=d._getImageData(a,b,e,f);c.postMessage({type:"decode",data:a},[a.data.buffer])}).catch(k)});b&&g&&(l=l.catch(()=>d.scanImage(a,null,c,e,f)));return l=l.finally(()=>{h&&c.postMessage({type:"close"})})}setGrayscaleWeights(a,b,c,e=!0){this._qrWorker.postMessage({type:"grayscaleWeights",data:{red:a,green:b,blue:c,useIntegerApproximation:e}})}setInversionMode(a){this._qrWorker.postMessage({type:"inversionMode",data:a})}_onPlay(){this._updateSourceRect();this._scanFrame()}_onVisibilityChange(){document.hidden?
|
||||
this.pause():this._active&&this.start()}_updateSourceRect(){let a=Math.round(2/3*Math.min(this.$video.videoWidth,this.$video.videoHeight));this._sourceRect.width=this._sourceRect.height=a;this._sourceRect.x=(this.$video.videoWidth-a)/2;this._sourceRect.y=(this.$video.videoHeight-a)/2}_scanFrame(){if(!this._active||this.$video.paused||this.$video.ended)return!1;requestAnimationFrame(()=>{1>=this.$video.readyState?this._scanFrame():d.scanImage(this.$video,this._sourceRect,this._qrWorker,this.$canvas,
|
||||
!0).then(this._onDecode,a=>{this._active&&this._onDecodeError(a)}).then(()=>this._scanFrame())})}_onDecodeError(a){a!==d.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(a,b=!1){let c=[{width:{min:1024}},{width:{min:768}},{}];a&&(b&&(a={exact:a}),c.forEach(b=>b.facingMode=a));return this._getMatchingCameraStream(c)}_getMatchingCameraStream(a){return navigator.mediaDevices&&0!==a.length?navigator.mediaDevices.getUserMedia({video:a.shift()}).catch(()=>this._getMatchingCameraStream(a)):Promise.reject("Camera not found.")}_setFlash(a){return this.hasFlash().then(b=>
|
||||
b?this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:a}]}):Promise.reject("No flash available")).then(()=>this._flashOn=a)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _getImageData(a,b=null,c=null,e=!1){c=c||document.createElement("canvas");let d=b&&b.x?b.x:0,g=b&&b.y?b.y:0,h=b&&
|
||||
b.width?b.width:a.width||a.videoWidth;b=b&&b.height?b.height:a.height||a.videoHeight;e||c.width===h&&c.height===b||(c.width=h,c.height=b);e=c.getContext("2d",{alpha:!1});e.imageSmoothingEnabled=!1;e.drawImage(a,d,g,h,b,0,0,c.width,c.height);return e.getImageData(0,0,c.width,c.height)}static _loadImage(a){if(a instanceof HTMLCanvasElement||a instanceof HTMLVideoElement||window.ImageBitmap&&a instanceof window.ImageBitmap||window.OffscreenCanvas&&a instanceof window.OffscreenCanvas)return Promise.resolve(a);
|
||||
if(a instanceof Image)return d._awaitImageLoad(a).then(()=>a);if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a;return d._awaitImageLoad(b).then(()=>{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src);return b})}return Promise.reject("Unsupported image type.")}static _awaitImageLoad(a){return new Promise((b,c)=>{if(a.complete&&0!==a.naturalWidth)b();else{let d,f;d=()=>
|
||||
{a.removeEventListener("load",d);a.removeEventListener("error",f);b()};f=()=>{a.removeEventListener("load",d);a.removeEventListener("error",f);c("Image load error")};a.addEventListener("load",d);a.addEventListener("error",f)}})}}d.DEFAULT_CANVAS_SIZE=400;d.NO_QR_CODE_FOUND="No QR code found";d.WORKER_PATH="qr-scanner-worker.min.js";return d})
|
||||
'use strict';(function(d,a){"object"===typeof exports&&"undefined"!==typeof module?module.exports=a():"function"===typeof define&&define.amd?define(a):(d=d||self,d.QrScanner=a())})(this,function(){class d{static hasCamera(){return navigator.mediaDevices?navigator.mediaDevices.enumerateDevices().then(a=>a.some(a=>"videoinput"===a.kind)).catch(()=>!1):Promise.resolve(!1)}constructor(a,b,c=this._onDecodeError.bind(this),e=d.DEFAULT_CANVAS_SIZE,g="environment"){this.$video=a;this.$canvas=document.createElement("canvas");
|
||||
this._onDecode=b;this._preferredFacingMode=g;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(e=c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;this.$canvas.width=e;this.$canvas.height=e;this._sourceRect={x:0,y:0,width:e,height:e};this._updateSourceRect=this._updateSourceRect.bind(this);this._onPlay=this._onPlay.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this.$video.playsInline=
|
||||
!0;this.$video.muted=!0;this.$video.disablePictureInPicture=!0;this.$video.addEventListener("loadedmetadata",this._updateSourceRect);this.$video.addEventListener("play",this._onPlay);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrEnginePromise=d.createQrEngine(d.WORKER_PATH)}hasFlash(){if(!("ImageCapture"in window))return Promise.resolve(!1);let a=this.$video.srcObject?this.$video.srcObject.getVideoTracks()[0]:null;return a?(new ImageCapture(a)).getPhotoCapabilities().then(a=>
|
||||
a.fillLightMode.includes("flash")).catch(a=>{console.warn(a);return!1}):Promise.reject("Camera not started or not available")}isFlashOn(){return this._flashOn}toggleFlash(){return this._setFlash(!this._flashOn)}turnFlashOff(){return this._setFlash(!1)}turnFlashOn(){return this._setFlash(!0)}destroy(){this.$video.removeEventListener("loadedmetadata",this._updateSourceRect);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);
|
||||
this.stop();d._postWorkerMessage(this._qrEnginePromise,"close")}start(){if(this._active&&!this._paused)return Promise.resolve();"https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https.");this._active=!0;this._paused=!1;if(document.hidden)return Promise.resolve();clearTimeout(this._offTimeout);this._offTimeout=null;if(this.$video.srcObject)return this.$video.play(),Promise.resolve();let a=this._preferredFacingMode;return this._getCameraStream(a,
|
||||
!0).catch(()=>{a="environment"===a?"user":"environment";return this._getCameraStream()}).then(b=>{a=this._getFacingMode(b)||a;this.$video.srcObject=b;this.$video.play();this._setVideoMirror(a)}).catch(a=>{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(){this._paused=!0;this._active&&(this.$video.pause(),this._offTimeout||(this._offTimeout=setTimeout(()=>{let a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(let b of a)b.stop();this._offTimeout=this.$video.srcObject=
|
||||
null},300)))}static scanImage(a,b=null,c=null,e=null,g=!1,k=!1){let f=c instanceof Worker,h=Promise.all([c||d.createQrEngine(),d._loadImage(a)]).then(([a,h])=>{c=a;let k;[e,k]=this._drawToCanvas(h,b,e,g);return c instanceof Worker?(f||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let g,l,f;l=e=>{"qrResult"===e.data.type&&(c.removeEventListener("message",l),c.removeEventListener("error",f),clearTimeout(g),null!==e.data.data?a(e.data.data):b(d.NO_QR_CODE_FOUND))};f=a=>{c.removeEventListener("message",
|
||||
l);c.removeEventListener("error",f);clearTimeout(g);b("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",l);c.addEventListener("error",f);g=setTimeout(()=>f("timeout"),1E4);let h=k.getImageData(0,0,e.width,e.height);c.postMessage({type:"decode",data:h},[h.data.buffer])})):new Promise((a,b)=>{let g=setTimeout(()=>b("Scanner error: timeout"),1E4);c.detect(e).then(c=>{c.length?a(c[0].rawValue):b(d.NO_QR_CODE_FOUND)}).catch(a=>b("Scanner error: "+(a.message||a))).finally(()=>
|
||||
clearTimeout(g))})});b&&k&&(h=h.catch(()=>d.scanImage(a,null,c,e,g)));return h=h.finally(()=>{f||d._postWorkerMessage(c,"close")})}setGrayscaleWeights(a,b,c,e=!0){d._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:e})}setInversionMode(a){d._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static createQrEngine(a=d.WORKER_PATH){return("BarcodeDetector"in window?BarcodeDetector.getSupportedFormats():Promise.resolve([])).then(b=>-1!==
|
||||
b.indexOf("qr_code")?new BarcodeDetector({formats:["qr_code"]}):new Worker(a))}_onPlay(){this._updateSourceRect();this._scanFrame()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_updateSourceRect(){let a=Math.round(2/3*Math.min(this.$video.videoWidth,this.$video.videoHeight));this._sourceRect.width=this._sourceRect.height=a;this._sourceRect.x=(this.$video.videoWidth-a)/2;this._sourceRect.y=(this.$video.videoHeight-a)/2}_scanFrame(){if(!this._active||this.$video.paused||
|
||||
this.$video.ended)return!1;requestAnimationFrame(()=>{1>=this.$video.readyState?this._scanFrame():this._qrEnginePromise.then(a=>d.scanImage(this.$video,this._sourceRect,a,this.$canvas,!0)).then(this._onDecode,a=>{this._active&&this._onDecodeError(a)}).then(()=>this._scanFrame())})}_onDecodeError(a){a!==d.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(a,b=!1){let c=[{width:{min:1024}},{width:{min:768}},{}];a&&(b&&(a={exact:a}),c.forEach(b=>b.facingMode=a));return this._getMatchingCameraStream(c)}_getMatchingCameraStream(a){return navigator.mediaDevices&&
|
||||
0!==a.length?navigator.mediaDevices.getUserMedia({video:a.shift()}).catch(()=>this._getMatchingCameraStream(a)):Promise.reject("Camera not found.")}_setFlash(a){return this.hasFlash().then(b=>b?this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:a}]}):Promise.reject("No flash available")).then(()=>this._flashOn=a)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?
|
||||
"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b=null,c=null,e=!1){c=c||document.createElement("canvas");let d=b&&b.x?b.x:0,k=b&&b.y?b.y:0,f=b&&b.width?b.width:a.width||a.videoWidth;b=b&&b.height?b.height:a.height||a.videoHeight;e||c.width===f&&c.height===b||(c.width=f,c.height=b);e=c.getContext("2d",{alpha:!1});e.imageSmoothingEnabled=!1;e.drawImage(a,d,k,f,b,0,0,c.width,c.height);return[c,e]}static _loadImage(a){if(a instanceof HTMLCanvasElement||a instanceof
|
||||
HTMLVideoElement||window.ImageBitmap&&a instanceof window.ImageBitmap||window.OffscreenCanvas&&a instanceof window.OffscreenCanvas)return Promise.resolve(a);if(a instanceof Image)return d._awaitImageLoad(a).then(()=>a);if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a;return d._awaitImageLoad(b).then(()=>{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src);return b})}return Promise.reject("Unsupported image type.")}static _awaitImageLoad(a){return new Promise((b,
|
||||
c)=>{if(a.complete&&0!==a.naturalWidth)b();else{let e,d;e=()=>{a.removeEventListener("load",e);a.removeEventListener("error",d);b()};d=()=>{a.removeEventListener("load",e);a.removeEventListener("error",d);c("Image load error")};a.addEventListener("load",e);a.addEventListener("error",d)}})}static _postWorkerMessage(a,b,c){return Promise.resolve(a).then(a=>{a instanceof Worker&&a.postMessage({type:b,data:c})})}}d.DEFAULT_CANVAS_SIZE=400;d.NO_QR_CODE_FOUND="No QR code found";d.WORKER_PATH="qr-scanner-worker.min.js";
|
||||
return d})
|
||||
//# sourceMappingURL=qr-scanner.umd.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"qr-scanner.umd.min.js","sources":["src/qr-scanner.js"],"sourcesContent":["export default class QrScanner {\n /* async */\n static hasCamera() {\n if (!navigator.mediaDevices) return Promise.resolve(false);\n // note that enumerateDevices can always be called and does not prompt the user for permission. However, device\n // labels are only readable if served via https and an active media stream exists or permanent permission is\n // given. That doesn't matter for us though as we don't require labels.\n return navigator.mediaDevices.enumerateDevices()\n .then(devices => devices.some(device => device.kind === 'videoinput'))\n .catch(() => false);\n }\n\n constructor(\n video,\n onDecode,\n canvasSizeOrOnDecodeError = this._onDecodeError.bind(this),\n canvasSize = QrScanner.DEFAULT_CANVAS_SIZE,\n preferredFacingMode = 'environment'\n ) {\n this.$video = video;\n this.$canvas = document.createElement('canvas');\n this._onDecode = onDecode;\n this._preferredFacingMode = preferredFacingMode;\n this._active = false;\n this._paused = false;\n this._flashOn = false;\n\n if (typeof canvasSizeOrOnDecodeError === 'number') {\n // legacy function signature where canvas size is the third argument\n canvasSize = canvasSizeOrOnDecodeError;\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n this._onDecodeError = canvasSizeOrOnDecodeError;\n }\n\n this.$canvas.width = canvasSize;\n this.$canvas.height = canvasSize;\n this._sourceRect = {\n x: 0,\n y: 0,\n width: canvasSize,\n height: canvasSize\n };\n\n this._updateSourceRect = this._updateSourceRect.bind(this);\n this._onPlay = this._onPlay.bind(this);\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\n\n // Allow inline playback on iPhone instead of requiring full screen playback,\n // see https://webkit.org/blog/6784/new-video-policies-for-ios/\n this.$video.playsInline = true;\n // Allow play() on iPhone without requiring a user gesture. Should not really be needed as camera stream\n // includes no audio, but just to be safe.\n this.$video.muted = true;\n this.$video.disablePictureInPicture = true;\n this.$video.addEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.addEventListener('play', this._onPlay);\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n\n this._qrWorker = new Worker(QrScanner.WORKER_PATH);\n }\n\n /* async */\n hasFlash() {\n if (!('ImageCapture' in window)) {\n return Promise.resolve(false);\n }\n\n const track = this.$video.srcObject ? this.$video.srcObject.getVideoTracks()[0] : null;\n if (!track) {\n return Promise.reject('Camera not started or not available');\n }\n\n const imageCapture = new ImageCapture(track);\n return imageCapture.getPhotoCapabilities()\n .then((result) => {\n return result.fillLightMode.includes('flash');\n })\n .catch((error) => {\n console.warn(error);\n return false;\n });\n }\n\n isFlashOn() {\n return this._flashOn;\n }\n\n /* async */\n toggleFlash() {\n return this._setFlash(!this._flashOn);\n }\n\n /* async */\n turnFlashOff() {\n return this._setFlash(false);\n }\n\n /* async */\n turnFlashOn() {\n return this._setFlash(true);\n }\n\n destroy() {\n this.$video.removeEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.removeEventListener('play', this._onPlay);\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n\n this.stop();\n this._qrWorker.postMessage({\n type: 'close'\n });\n }\n\n /* async */\n start() {\n if (this._active && !this._paused) {\n return Promise.resolve();\n }\n if (window.location.protocol !== 'https:') {\n // warn but try starting the camera anyways\n console.warn('The camera stream is only accessible if the page is transferred via https.');\n }\n this._active = true;\n this._paused = false;\n if (document.hidden) {\n // camera will be started as soon as tab is in foreground\n return Promise.resolve();\n }\n clearTimeout(this._offTimeout);\n this._offTimeout = null;\n if (this.$video.srcObject) {\n // camera stream already/still set\n this.$video.plaLine truncated
|
||||
{"version":3,"file":"qr-scanner.umd.min.js","sources":["src/qr-scanner.js"],"sourcesContent":["export default class QrScanner {\n /* async */\n static hasCamera() {\n if (!navigator.mediaDevices) return Promise.resolve(false);\n // note that enumerateDevices can always be called and does not prompt the user for permission. However, device\n // labels are only readable if served via https and an active media stream exists or permanent permission is\n // given. That doesn't matter for us though as we don't require labels.\n return navigator.mediaDevices.enumerateDevices()\n .then(devices => devices.some(device => device.kind === 'videoinput'))\n .catch(() => false);\n }\n\n constructor(\n video,\n onDecode,\n canvasSizeOrOnDecodeError = this._onDecodeError.bind(this),\n canvasSize = QrScanner.DEFAULT_CANVAS_SIZE,\n preferredFacingMode = 'environment'\n ) {\n this.$video = video;\n this.$canvas = document.createElement('canvas');\n this._onDecode = onDecode;\n this._preferredFacingMode = preferredFacingMode;\n this._active = false;\n this._paused = false;\n this._flashOn = false;\n\n if (typeof canvasSizeOrOnDecodeError === 'number') {\n // legacy function signature where canvas size is the third argument\n canvasSize = canvasSizeOrOnDecodeError;\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n this._onDecodeError = canvasSizeOrOnDecodeError;\n }\n\n this.$canvas.width = canvasSize;\n this.$canvas.height = canvasSize;\n this._sourceRect = {\n x: 0,\n y: 0,\n width: canvasSize,\n height: canvasSize\n };\n\n this._updateSourceRect = this._updateSourceRect.bind(this);\n this._onPlay = this._onPlay.bind(this);\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\n\n // Allow inline playback on iPhone instead of requiring full screen playback,\n // see https://webkit.org/blog/6784/new-video-policies-for-ios/\n this.$video.playsInline = true;\n // Allow play() on iPhone without requiring a user gesture. Should not really be needed as camera stream\n // includes no audio, but just to be safe.\n this.$video.muted = true;\n this.$video.disablePictureInPicture = true;\n this.$video.addEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.addEventListener('play', this._onPlay);\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n\n this._qrEnginePromise = QrScanner.createQrEngine(QrScanner.WORKER_PATH);\n }\n\n /* async */\n hasFlash() {\n if (!('ImageCapture' in window)) {\n return Promise.resolve(false);\n }\n\n const track = this.$video.srcObject ? this.$video.srcObject.getVideoTracks()[0] : null;\n if (!track) {\n return Promise.reject('Camera not started or not available');\n }\n\n const imageCapture = new ImageCapture(track);\n return imageCapture.getPhotoCapabilities()\n .then((result) => {\n return result.fillLightMode.includes('flash');\n })\n .catch((error) => {\n console.warn(error);\n return false;\n });\n }\n\n isFlashOn() {\n return this._flashOn;\n }\n\n /* async */\n toggleFlash() {\n return this._setFlash(!this._flashOn);\n }\n\n /* async */\n turnFlashOff() {\n return this._setFlash(false);\n }\n\n /* async */\n turnFlashOn() {\n return this._setFlash(true);\n }\n\n destroy() {\n this.$video.removeEventListener('loadedmetadata', this._updateSourceRect);\n this.$video.removeEventListener('play', this._onPlay);\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n\n this.stop();\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'close');\n }\n\n /* async */\n start() {\n if (this._active && !this._paused) {\n return Promise.resolve();\n }\n if (window.location.protocol !== 'https:') {\n // warn but try starting the camera anyways\n console.warn('The camera stream is only accessible if the page is transferred via https.');\n }\n this._active = true;\n this._paused = false;\n if (document.hidden) {\n // camera will be started as soon as tab is in foreground\n return Promise.resolve();\n }\n clearTimeout(this._offTimeout);\n this._offTimeout = null;\n if (this.$video.srcObject) {\n // camera stream already/still set\n tLine truncated
|
||||
+94
-58
@@ -58,7 +58,7 @@ export default class QrScanner {
|
||||
this.$video.addEventListener('play', this._onPlay);
|
||||
document.addEventListener('visibilitychange', this._onVisibilityChange);
|
||||
|
||||
this._qrWorker = new Worker(QrScanner.WORKER_PATH);
|
||||
this._qrEnginePromise = QrScanner.createQrEngine(QrScanner.WORKER_PATH);
|
||||
}
|
||||
|
||||
/* async */
|
||||
@@ -108,9 +108,7 @@ export default class QrScanner {
|
||||
document.removeEventListener('visibilitychange', this._onVisibilityChange);
|
||||
|
||||
this.stop();
|
||||
this._qrWorker.postMessage({
|
||||
type: 'close'
|
||||
});
|
||||
QrScanner._postWorkerMessage(this._qrEnginePromise, 'close');
|
||||
}
|
||||
|
||||
/* async */
|
||||
@@ -182,74 +180,103 @@ export default class QrScanner {
|
||||
}
|
||||
|
||||
/* async */
|
||||
static scanImage(imageOrFileOrUrl, sourceRect=null, worker=null, canvas=null, fixedCanvasSize=false,
|
||||
static scanImage(imageOrFileOrUrl, sourceRect=null, qrEngine=null, canvas=null, fixedCanvasSize=false,
|
||||
alsoTryWithoutSourceRect=false) {
|
||||
let createdNewWorker = false;
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
if (!worker) {
|
||||
worker = new Worker(QrScanner.WORKER_PATH);
|
||||
createdNewWorker = true;
|
||||
worker.postMessage({ type: 'inversionMode', data: 'both' }); // scan inverted color qr codes too
|
||||
const gotExternalWorker = qrEngine instanceof Worker;
|
||||
|
||||
let promise = Promise.all([
|
||||
qrEngine || QrScanner.createQrEngine(),
|
||||
QrScanner._loadImage(imageOrFileOrUrl),
|
||||
]).then(([engine, image]) => {
|
||||
qrEngine = engine;
|
||||
let canvasContext;
|
||||
[canvas, canvasContext] = this._drawToCanvas(image, sourceRect, canvas, fixedCanvasSize);
|
||||
|
||||
if (qrEngine instanceof Worker) {
|
||||
if (!gotExternalWorker) {
|
||||
// Enable scanning of inverted color qr codes. Not using _postWorkerMessage as it's async
|
||||
qrEngine.postMessage({ type: 'inversionMode', data: 'both' });
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeout, onMessage, onError;
|
||||
onMessage = event => {
|
||||
if (event.data.type !== 'qrResult') {
|
||||
return;
|
||||
}
|
||||
qrEngine.removeEventListener('message', onMessage);
|
||||
qrEngine.removeEventListener('error', onError);
|
||||
clearTimeout(timeout);
|
||||
if (event.data.data !== null) {
|
||||
resolve(event.data.data);
|
||||
} else {
|
||||
reject(QrScanner.NO_QR_CODE_FOUND);
|
||||
}
|
||||
};
|
||||
onError = (e) => {
|
||||
qrEngine.removeEventListener('message', onMessage);
|
||||
qrEngine.removeEventListener('error', onError);
|
||||
clearTimeout(timeout);
|
||||
const errorMessage = !e ? 'Unknown Error' : (e.message || e);
|
||||
reject('Scanner error: ' + errorMessage);
|
||||
};
|
||||
qrEngine.addEventListener('message', onMessage);
|
||||
qrEngine.addEventListener('error', onError);
|
||||
timeout = setTimeout(() => onError('timeout'), 10000);
|
||||
const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
|
||||
qrEngine.postMessage({
|
||||
type: 'decode',
|
||||
data: imageData
|
||||
}, [imageData.data.buffer]);
|
||||
});
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject('Scanner error: timeout'), 10000);
|
||||
qrEngine.detect(canvas).then(scanResults => {
|
||||
if (!scanResults.length) {
|
||||
reject(QrScanner.NO_QR_CODE_FOUND);
|
||||
} else {
|
||||
resolve(scanResults[0].rawValue);
|
||||
}
|
||||
}).catch((e) => reject('Scanner error: ' + (e.message || e))).finally(() => clearTimeout(timeout));
|
||||
});
|
||||
}
|
||||
let timeout, onMessage, onError;
|
||||
onMessage = event => {
|
||||
if (event.data.type !== 'qrResult') {
|
||||
return;
|
||||
}
|
||||
worker.removeEventListener('message', onMessage);
|
||||
worker.removeEventListener('error', onError);
|
||||
clearTimeout(timeout);
|
||||
if (event.data.data !== null) {
|
||||
resolve(event.data.data);
|
||||
} else {
|
||||
reject(QrScanner.NO_QR_CODE_FOUND);
|
||||
}
|
||||
};
|
||||
onError = (e) => {
|
||||
worker.removeEventListener('message', onMessage);
|
||||
worker.removeEventListener('error', onError);
|
||||
clearTimeout(timeout);
|
||||
const errorMessage = !e ? 'Unknown Error' : (e.message || e);
|
||||
reject('Scanner error: ' + errorMessage);
|
||||
};
|
||||
worker.addEventListener('message', onMessage);
|
||||
worker.addEventListener('error', onError);
|
||||
timeout = setTimeout(() => onError('timeout'), 10000);
|
||||
QrScanner._loadImage(imageOrFileOrUrl).then(image => {
|
||||
const imageData = QrScanner._getImageData(image, sourceRect, canvas, fixedCanvasSize);
|
||||
worker.postMessage({
|
||||
type: 'decode',
|
||||
data: imageData
|
||||
}, [imageData.data.buffer]);
|
||||
}).catch(onError);
|
||||
});
|
||||
|
||||
if (sourceRect && alsoTryWithoutSourceRect) {
|
||||
promise = promise.catch(() => QrScanner.scanImage(imageOrFileOrUrl, null, worker, canvas, fixedCanvasSize));
|
||||
promise = promise.catch(() => QrScanner.scanImage(imageOrFileOrUrl, null, qrEngine, canvas, fixedCanvasSize));
|
||||
}
|
||||
|
||||
promise = promise.finally(() => {
|
||||
if (!createdNewWorker) return;
|
||||
worker.postMessage({
|
||||
type: 'close'
|
||||
});
|
||||
if (gotExternalWorker) return;
|
||||
QrScanner._postWorkerMessage(qrEngine, 'close');
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
setGrayscaleWeights(red, green, blue, useIntegerApproximation = true) {
|
||||
this._qrWorker.postMessage({
|
||||
type: 'grayscaleWeights',
|
||||
data: { red, green, blue, useIntegerApproximation }
|
||||
});
|
||||
// Note that for the native BarcodeDecoder, this is a no-op. However, the native implementations work also
|
||||
// well with colored qr codes.
|
||||
QrScanner._postWorkerMessage(
|
||||
this._qrEnginePromise,
|
||||
'grayscaleWeights',
|
||||
{ red, green, blue, useIntegerApproximation }
|
||||
);
|
||||
}
|
||||
|
||||
setInversionMode(inversionMode) {
|
||||
this._qrWorker.postMessage({
|
||||
type: 'inversionMode',
|
||||
data: inversionMode
|
||||
});
|
||||
// Note that for the native BarcodeDecoder, this is a no-op. However, the native implementations scan normal
|
||||
// and inverted qr codes by default
|
||||
QrScanner._postWorkerMessage(this._qrEnginePromise, 'inversionMode', inversionMode);
|
||||
}
|
||||
|
||||
/* async */
|
||||
static createQrEngine(workerPath = QrScanner.WORKER_PATH) {
|
||||
return ('BarcodeDetector' in window ? BarcodeDetector.getSupportedFormats() : Promise.resolve([]))
|
||||
.then((supportedFormats) => supportedFormats.indexOf('qr_code') !== -1
|
||||
? new BarcodeDetector({ formats: ['qr_code'] })
|
||||
: new Worker(workerPath)
|
||||
);
|
||||
}
|
||||
|
||||
_onPlay() {
|
||||
@@ -285,7 +312,8 @@ export default class QrScanner {
|
||||
this._scanFrame();
|
||||
return;
|
||||
}
|
||||
QrScanner.scanImage(this.$video, this._sourceRect, this._qrWorker, this.$canvas, true)
|
||||
this._qrEnginePromise
|
||||
.then((qrEngine) => QrScanner.scanImage(this.$video, this._sourceRect, qrEngine, this.$canvas, true))
|
||||
.then(this._onDecode, (error) => {
|
||||
if (!this._active) return;
|
||||
this._onDecodeError(error);
|
||||
@@ -353,7 +381,7 @@ export default class QrScanner {
|
||||
: null; // unknown
|
||||
}
|
||||
|
||||
static _getImageData(image, sourceRect=null, canvas=null, fixedCanvasSize=false) {
|
||||
static _drawToCanvas(image, sourceRect=null, canvas=null, fixedCanvasSize=false) {
|
||||
canvas = canvas || document.createElement('canvas');
|
||||
const sourceRectX = sourceRect && sourceRect.x? sourceRect.x : 0;
|
||||
const sourceRectY = sourceRect && sourceRect.y? sourceRect.y : 0;
|
||||
@@ -366,7 +394,7 @@ export default class QrScanner {
|
||||
const context = canvas.getContext('2d', { alpha: false });
|
||||
context.imageSmoothingEnabled = false; // gives less blurry images
|
||||
context.drawImage(image, sourceRectX, sourceRectY, sourceRectWidth, sourceRectHeight, 0, 0, canvas.width, canvas.height);
|
||||
return context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
return [canvas, context];
|
||||
}
|
||||
|
||||
/* async */
|
||||
@@ -419,6 +447,14 @@ export default class QrScanner {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* async */
|
||||
static _postWorkerMessage(qrEngineOrQrEnginePromise, type, data) {
|
||||
return Promise.resolve(qrEngineOrQrEnginePromise).then((qrEngine) => {
|
||||
if (!(qrEngine instanceof Worker)) return;
|
||||
qrEngine.postMessage({ type, data });
|
||||
});
|
||||
}
|
||||
}
|
||||
QrScanner.DEFAULT_CANVAS_SIZE = 400;
|
||||
QrScanner.NO_QR_CODE_FOUND = 'No QR code found';
|
||||
|
||||
Vendored
+8
@@ -38,6 +38,14 @@ declare class QrScanner {
|
||||
fixedCanvasSize?: boolean,
|
||||
alsoTryWithoutSourceRect?: boolean
|
||||
): Promise<string>;
|
||||
static createQrEnginge(workerPath?: string): Promise<Worker | BarcodeDetector>;
|
||||
}
|
||||
|
||||
// simplified from https://wicg.github.io/shape-detection-api/#barcode-detection-api
|
||||
declare class BarcodeDetector {
|
||||
constructor(options?: { formats: string[] });
|
||||
static getSupportedFormats(): Promise<string[]>;
|
||||
detect(image: ImageBitmapSource): Promise<Array<{ rawValue: string }>>;
|
||||
}
|
||||
|
||||
// exported types
|
||||
|
||||
Reference in new issue
Block a user