Improvements and fixes for continous scanning error handling

- Make change backwards compatible with previous consructor signature
- Fix demo to interpret errors as proper results
- Update types
- Do not report errors when the scanner is inactive
- Update readme
This commit is contained in:
Daniel
2020-07-07 23:45:49 +02:00
parent 0961d4e102
commit 947fb7ebcf
6 changed files with 56 additions and 19 deletions
+4 -2
View File
@@ -105,9 +105,11 @@ Create a `<video>` element where the web cam video stream should get rendered:
#### 2. Create a QrScanner Instance
```js
const qrScanner = new QrScanner(videoElem, result => console.log('decoded qr code:', result), error => console.log(error || 'No QR code found.'));
const qrScanner = new QrScanner(videoElem, result => console.log('decoded qr code:', result));
```
As an optional third parameter a specific resolution that should be worked on can be specified. The default is 400.
As an optional third parameter an error handler to be invoked on decoding errors can be specified. The default is `QrScanner._onDecodeError`.
As an optional fourth parameter a specific resolution that should be worked on can be specified. The default is 400.
#### 3. Start scanning
```js
+4 -1
View File
@@ -73,7 +73,10 @@
QrScanner.hasCamera().then(hasCamera => camHasCamera.textContent = hasCamera);
const scanner = new QrScanner(video, result => setResult(camQrResult, result), error => setResult(camQrResult, error || 'No QR code found.'));
const scanner = new QrScanner(video, result => setResult(camQrResult, result), error => {
camQrResult.textContent = error;
camQrResult.style.color = 'inherit';
});
scanner.start();
document.getElementById('inversion-mode-select').addEventListener('change', event => {
+12 -11
View File
@@ -1,12 +1,13 @@
class e{static hasCamera(){return!!navigator.mediaDevices&&navigator.mediaDevices.enumerateDevices().then((a)=>a.some((a)=>"videoinput"===a.kind)).catch(()=>!1)}constructor(a,b,c,d=e.DEFAULT_CANVAS_SIZE){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;this._onDecodeError=c;this._paused=this._active=!1;this.$canvas.width=d;this.$canvas.height=d;this._sourceRect={x:0,y:0,width:d,height:d};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("play",this._onPlay);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrWorker=new Worker(e.WORKER_PATH)}destroy(){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="environment";return this._getCameraStream("environment",!0).catch(()=>{a="user";return this._getCameraStream()}).then((b)=>{this._isUserFacing(b)&&(a="user");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("QR code not 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"),3E3);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,this._onDecodeError).then(()=>
this._scanFrame())})}_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.")}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_isUserFacing(a){return(a=a.getVideoTracks()[0])?
/front|user|face/i.test(a.label):!1}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 URL||"string"===typeof a){let b=new Image;b.src=a instanceof File?URL.createObjectURL(a):a;return e._awaitImageLoad(b).then(()=>{a instanceof File&&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.WORKER_PATH="qr-scanner-worker.min.js";export default e;
class e{static hasCamera(){return!!navigator.mediaDevices&&navigator.mediaDevices.enumerateDevices().then((a)=>a.some((a)=>"videoinput"===a.kind)).catch(()=>!1)}constructor(a,b,c=this._onDecodeError.bind(this),d=e.DEFAULT_CANVAS_SIZE){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;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._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("play",this._onPlay);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrWorker=new Worker(e.WORKER_PATH)}destroy(){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="environment";return this._getCameraStream("environment",
!0).catch(()=>{a="user";return this._getCameraStream()}).then((b)=>{this._isUserFacing(b)&&(a="user");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"),3E3);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.")}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_isUserFacing(a){return(a=a.getVideoTracks()[0])?/front|user|face/i.test(a.label):!1}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 URL||"string"===typeof a){let b=new Image;b.src=a instanceof
File?URL.createObjectURL(a):a;return e._awaitImageLoad(b).then(()=>{a instanceof File&&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;
//# sourceMappingURL=qr-scanner.min.js.map
File diff suppressed because one or more lines are too long
+27 -4
View File
@@ -9,14 +9,27 @@ export default class QrScanner {
.catch(() => false);
}
constructor(video, onDecode, onDecodeError, canvasSize = QrScanner.DEFAULT_CANVAS_SIZE) {
constructor(
video,
onDecode,
canvasSizeOrOnDecodeError = this._onDecodeError.bind(this),
canvasSize = QrScanner.DEFAULT_CANVAS_SIZE
) {
this.$video = video;
this.$canvas = document.createElement('canvas');
this._onDecode = onDecode;
this._onDecodeError = onDecodeError;
this._active = false;
this._paused = false;
if (typeof canvasSizeOrOnDecodeError === 'number') {
// legacy function signature where canvas size is the third argument
canvasSize = canvasSizeOrOnDecodeError;
console.warn('You\'re using a deprecated version of the QrScanner constructor which will be removed in '
+ 'the future');
} else {
this._onDecodeError = canvasSizeOrOnDecodeError;
}
this.$canvas.width = canvasSize;
this.$canvas.height = canvasSize;
this._sourceRect = {
@@ -143,7 +156,7 @@ export default class QrScanner {
if (event.data.data !== null) {
resolve(event.data.data);
} else {
reject('QR code not found.');
reject(QrScanner.NO_QR_CODE_FOUND);
}
};
onError = (e) => {
@@ -227,11 +240,20 @@ export default class QrScanner {
return;
}
QrScanner.scanImage(this.$video, this._sourceRect, this._qrWorker, this.$canvas, true)
.then(this._onDecode, this._onDecodeError)
.then(this._onDecode, (error) => {
if (!this._active) return;
this._onDecodeError(error);
})
.then(() => this._scanFrame());
});
}
_onDecodeError(error) {
// default error handler; can be overwritten in the constructor
if (error === QrScanner.NO_QR_CODE_FOUND) return;
console.log(error);
}
_getCameraStream(facingMode, exact = false) {
const constraintsToTry = [{
width: { min: 1024 }
@@ -338,4 +360,5 @@ export default class QrScanner {
}
}
QrScanner.DEFAULT_CANVAS_SIZE = 400;
QrScanner.NO_QR_CODE_FOUND = 'No QR code found';
QrScanner.WORKER_PATH = 'qr-scanner-worker.min.js';
+8
View File
@@ -4,10 +4,18 @@
declare class QrScanner {
static DEFAULT_CANVAS_SIZE: number;
static NO_QR_CODE_FOUND: string;
static WORKER_PATH: string;
static hasCamera(): Promise<boolean>;
constructor(
video: HTMLVideoElement,
onDecode: (result: string) => void,
onDecodeError?: (error: string) => void,
canvasSize?: number,
);
/** @deprecated */
constructor(video: HTMLVideoElement, onDecode: (result: string) => void, canvasSize?: number);
destroy(): void;