Merge PR#112 (lafriks-fork:feat/camera_list) with fixes

This commit is contained in:
Daniel
2021-08-16 14:08:30 +02:00
8 changed files with 208 additions and 91 deletions
+19 -1
View File
@@ -140,7 +140,7 @@ As an optional third parameter an error handler to be invoked on decoding errors
As an optional fourth parameter a method can be provided that determines a region to which scanning should be restricted as a performance improvement. This region can optionally also be scaled down before performing the scan as an additional performance improvement. The region is specified as `x`, `y`, `width` and `height`; the dimensions for the downscaled region as `downScaledWidth` and `downScaledHeight`. Note that the aspect ratio between `width` and `height` and `downScaledWidth` and `downScaledHeight` should remain the same. By default, the scan region is restricted to a centered square of two thirds of the video width or height, whichever is smaller, and scaled down to a 400x400 square.
As an optional fifth parameter a preference for the camera to use can be specified. Allowed values are `'environment'` and `'user'`. The default is `'environment'`.
As an optional fifth parameter a preference for the camera to use can be specified. The preference can be either a device id as returned by `listCameras` or a facing mode specified as `'environment'` or `'user'`. The default is `'environment'`. Note that there is no guarantee that the preference can actually be fulfilled.
To use the default value for an optional parameter, omit it or use `undefined`.
@@ -195,6 +195,24 @@ This library provides a utility method for checking whether the device has a cam
QrScanner.hasCamera(); // async
```
### Getting the list of available Cameras
This library provides a utility method for getting a list of the device's cameras, defined via their `id` and `label`. This can be useful for letting a user choose a specific camera to use.
You can optionally request the camera's labels. Note that this however requires the user's permission to acess the cameras, which he will be asked for if not granted already. If not specifically requested, device labels are determined on a best effort basis, i.e. actual labels are returned if permissions were already granted and fallback labels otherwise.
```js
QrScanner.listCameras(); // async; without requesting camera labels
QrScanner.listCameras(true); // async; requesting camera labels, potentially asking the user for permission
```
### Specifying which camera to use
You can change the preferred camera to be used. The preference can be either a device id as returned by `listCameras` or a facing mode specified as `'environment'` or `'user'`. Note that there is no guarantee that the preference can actually be fulfilled.
```js
qrScanner.setCamera(facingModeOrDeviceId); // async
```
### Color Inverted Mode
The scanner by default scans for dark QR codes on a bright background. You can change this behavior to scan for bright QR codes on dark background or for both at the same time:
```js
+44 -18
View File
@@ -22,12 +22,6 @@
</style>
<h1>Scan from WebCam:</h1>
<div>
<b>Device has camera: </b>
<span id="cam-has-camera"></span>
<br>
<b>Device has flash: </b>
<span id="cam-has-flash"></span>
<br>
<video id="qr-video"></video>
<br>
<label>
@@ -35,9 +29,6 @@
Show scan region
</label>
</div>
<div>
<button id="flash-toggle">📸 Flash: <span id="flash-state">off</span></button>
</div>
<div>
<select id="inversion-mode-select">
<option value="original">Scan original (dark QR code on bright background)</option>
@@ -46,6 +37,22 @@
</select>
<br>
</div>
<b>Device has camera: </b>
<span id="cam-has-camera"></span>
<br>
<div>
<b>Preferred camera:</b>
<select id="cam-list">
<option value="environment" selected>Environment Facing (default)</option>
<option value="user">User Facing</option>
</select>
</div>
<b>Camera has flash: </b>
<span id="cam-has-flash"></span>
<div>
<button id="flash-toggle">📸 Flash: <span id="flash-state">off</span></button>
</div>
<br>
<b>Detected QR code: </b>
<span id="cam-qr-result">None</span>
<br>
@@ -67,6 +74,7 @@
const video = document.getElementById('qr-video');
const camHasCamera = document.getElementById('cam-has-camera');
const camList = document.getElementById('cam-list');
const camHasFlash = document.getElementById('cam-has-flash');
const flashToggle = document.getElementById('flash-toggle');
const flashState = document.getElementById('flash-state');
@@ -85,24 +93,34 @@
// ####### Web Cam Scanning #######
QrScanner.hasCamera().then(hasCamera => camHasCamera.textContent = hasCamera);
const scanner = new QrScanner(video, result => setResult(camQrResult, result), error => {
camQrResult.textContent = error;
camQrResult.style.color = 'inherit';
});
scanner.start().then(() => {
const updateFlashAvailability = () => {
scanner.hasFlash().then(hasFlash => {
camHasFlash.textContent = hasFlash;
if (hasFlash) {
flashToggle.style.display = 'inline-block';
flashToggle.addEventListener('click', () => {
scanner.toggleFlash().then(() => flashState.textContent = scanner.isFlashOn() ? 'on' : 'off');
});
}
flashToggle.style.display = hasFlash ? 'inline-block' : 'none';
});
};
scanner.start().then(() => {
updateFlashAvailability();
// List cameras after the scanner started to avoid listCamera's stream and the scanner's stream being requested
// at the same time which can result in listCamera's unconstrained stream also being offered to the scanner.
// Note that we can also start the scanner after listCameras, we just have it this way around in the demo to
// start the scanner earlier.
QrScanner.listCameras(true).then(cameras => cameras.forEach(camera => {
const option = document.createElement('option');
option.value = camera.id;
option.text = camera.label;
camList.add(option);
}));
});
QrScanner.hasCamera().then(hasCamera => camHasCamera.textContent = hasCamera);
// for debugging
window.scanner = scanner;
@@ -117,6 +135,14 @@
scanner.setInversionMode(event.target.value);
});
camList.addEventListener('change', event => {
scanner.setCamera(event.target.value).then(updateFlashAvailability);
});
flashToggle.addEventListener('click', () => {
scanner.toggleFlash().then(() => flashState.textContent = scanner.isFlashOn() ? 'on' : 'off');
});
document.getElementById('start-button').addEventListener('click', () => {
scanner.start();
});
+18 -17
View File
@@ -1,18 +1,19 @@
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,d=this._calculateScanRegion,g="environment"){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;this._legacyCanvasSize=e.DEFAULT_CANVAS_SIZE;this._preferredFacingMode=g;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(this._legacyCanvasSize=
c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;"number"===typeof d?(this._legacyCanvasSize=d,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._calculateScanRegion=d;this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);
a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let f=!1;a.hidden&&(a.hidden=!1,f=!0);document.body.contains(a)||(document.body.appendChild(a),f=!0);requestAnimationFrame(()=>{let b=window.getComputedStyle(a);"none"===b.display&&(a.style.setProperty("display","block","important"),f=!0);"visible"!==b.visibility&&(a.style.setProperty("visibility","visible","important"),f=!0);f&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity=
0,a.style.width=0,a.style.height=0)});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrEnginePromise=e.createQrEngine()}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._onLoadedMetaData);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,g=!1,f=!1){let h=c instanceof Worker,k=
Promise.all([c||e.createQrEngine(),e._loadImage(a)]).then(([a,f])=>{c=a;let k;[d,k]=this._drawToCanvas(f,b,d,g);return c instanceof Worker?(h||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let g,f,h;f=d=>{"qrResult"===d.data.type&&(c.removeEventListener("message",f),c.removeEventListener("error",h),clearTimeout(g),null!==d.data.data?a(d.data.data):b(e.NO_QR_CODE_FOUND))};h=a=>{c.removeEventListener("message",f);c.removeEventListener("error",h);clearTimeout(g);b("Scanner error: "+
(a?a.message||a:"Unknown Error"))};c.addEventListener("message",f);c.addEventListener("error",h);g=setTimeout(()=>h("timeout"),1E4);let l=k.getImageData(0,0,d.width,d.height);c.postMessage({type:"decode",data:l},[l.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&&f&&(k=k.catch(()=>e.scanImage(a,null,c,d,g)));
return k=k.finally(()=>{h||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._scanRegion=
this._calculateScanRegion(this.$video);this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video)}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_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._scanRegion,a,this.$canvas)).then(this._onDecode,a=>{this._active&&(-1!==(a.message||a).indexOf("service unavailable")&&(this._qrEnginePromise=e.createQrEngine()),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 g=b&&b.x?b.x:0,f=b&&b.y?b.y:0,h=b&&b.width?b.width:a.width||a.videoWidth,k=b&&b.height?b.height:a.height||a.videoHeight;d||(d=b&&b.downScaledWidth?b.downScaledWidth:h,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==d&&(c.width=
d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,g,f,h,k,0,0,c.width,c.height);return[c,b]}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,g;d=()=>{a.removeEventListener("load",d);a.removeEventListener("error",g);b()};g=()=>{a.removeEventListener("load",d);a.removeEventListener("error",
g);c("Image load error")};a.addEventListener("load",d);a.addEventListener("error",g)}})}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
class e{static hasCamera(){return e.listCameras(!1).then(a=>!!a.length).catch(()=>!1)}static listCameras(a=!1){if(!navigator.mediaDevices)return Promise.resolve([]);let b=null;return(a?navigator.mediaDevices.getUserMedia({audio:!1,video:!0}).then(a=>b=a).catch(()=>{}):Promise.resolve()).then(()=>navigator.mediaDevices.enumerateDevices()).then(a=>a.filter(a=>"videoinput"===a.kind).map((a,b)=>({id:a.deviceId,label:a.label||(0===b?"Default Camera":`Camera ${b+1}`)}))).finally(()=>{if(b)for(let a of b.getTracks())a.stop(),
b.removeTrack(a)})}constructor(a,b,c=this._onDecodeError,d=this._calculateScanRegion,f="environment"){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;this._legacyCanvasSize=e.DEFAULT_CANVAS_SIZE;this._preferredCamera=f;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(this._legacyCanvasSize=c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;"number"===typeof d?(this._legacyCanvasSize=
d,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._calculateScanRegion=d;this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let g=!1;a.hidden&&(a.hidden=!1,g=!0);document.body.contains(a)||(document.body.appendChild(a),g=
!0);requestAnimationFrame(()=>{let b=window.getComputedStyle(a);"none"===b.display&&(a.style.setProperty("display","block","important"),g=!0);"visible"!==b.visibility&&(a.style.setProperty("visibility","visible","important"),g=!0);g&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity=0,a.style.width=0,a.style.height=0)});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",
this._onVisibilityChange);this._qrEnginePromise=e.createQrEngine()}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._onLoadedMetaData);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;if(document.hidden)return Promise.resolve();this._paused=!1;if(this.$video.srcObject)return this.$video.play(),
Promise.resolve();let a="environment"===this._preferredCamera||"user"===this._preferredCamera?this._preferredCamera:null;return this._getCameraStream(this._preferredCamera,!0).catch(()=>{a="environment"===a?"user":"environment";return this._getCameraStream()}).then(b=>{a=this._getFacingMode(b)||a||"environment";this.$video.srcObject=b;this.$video.play();this._setVideoMirror(a)}).catch(a=>{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(a=!1){this._paused=!0;if(!this._active)return Promise.resolve(!0);
this.$video.pause();let b=()=>{const a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(const b of a)b.stop(),this.$video.srcObject.removeTrack(b);this.$video.srcObject=null};return a?(b(),Promise.resolve(!0)):(new Promise(a=>setTimeout(a,300))).then(()=>{if(!this._paused)return!1;b();return!0})}setCamera(a){if(a===this._preferredCamera)return Promise.resolve();this._preferredCamera=a;let b=this._paused;return this.pause(!0).then(a=>{if(a&&!b&&this._active)return this.start()})}static scanImage(a,
b=null,c=null,d=null,f=!1,g=!1){let h=c instanceof Worker,k=Promise.all([c||e.createQrEngine(),e._loadImage(a)]).then(([a,g])=>{c=a;let k;[d,k]=this._drawToCanvas(g,b,d,f);return c instanceof Worker?(h||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let f,g,h;g=d=>{"qrResult"===d.data.type&&(c.removeEventListener("message",g),c.removeEventListener("error",h),clearTimeout(f),null!==d.data.data?a(d.data.data):b(e.NO_QR_CODE_FOUND))};h=a=>{c.removeEventListener("message",g);c.removeEventListener("error",
h);clearTimeout(f);b("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",g);c.addEventListener("error",h);f=setTimeout(()=>h("timeout"),1E4);let l=k.getImageData(0,0,d.width,d.height);c.postMessage({type:"decode",data:l},[l.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&&g&&
(k=k.catch(()=>e.scanImage(a,null,c,d,f)));return k=k.finally(()=>{h||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._scanRegion=this._calculateScanRegion(this.$video);this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video)}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_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._scanRegion,a,this.$canvas)).then(this._onDecode,a=>{this._active&&(-1!==(a.message||a).indexOf("service unavailable")&&(this._qrEnginePromise=e.createQrEngine()),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}},{}];if(a){let d="environment"===a||"user"===a?"facingMode":"deviceId";b&&(a={exact:a});c.forEach(b=>b[d]=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,g=b&&b.y?b.y:0,h=b&&b.width?b.width:a.width||a.videoWidth,k=b&&b.height?b.height:a.height||a.videoHeight;
d||(d=b&&b.downScaledWidth?b.downScaledWidth:h,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,g,h,k,0,0,c.width,c.height);return[c,b]}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
File diff suppressed because one or more lines are too long
+19 -18
View File
@@ -1,19 +1,20 @@
'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,e=this._calculateScanRegion,k="environment"){this.$video=a;this.$canvas=document.createElement("canvas");
this._onDecode=b;this._legacyCanvasSize=d.DEFAULT_CANVAS_SIZE;this._preferredFacingMode=k;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(this._legacyCanvasSize=c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;"number"===typeof e?(this._legacyCanvasSize=e,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._calculateScanRegion=
e;this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let f=!1;a.hidden&&(a.hidden=!1,f=!0);document.body.contains(a)||(document.body.appendChild(a),f=!0);requestAnimationFrame(()=>{let b=window.getComputedStyle(a);"none"===b.display&&(a.style.setProperty("display","block","important"),f=!0);"visible"!==
b.visibility&&(a.style.setProperty("visibility","visible","important"),f=!0);f&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity=0,a.style.width=0,a.style.height=0)});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrEnginePromise=d.createQrEngine()}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._onLoadedMetaData);
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,k=!1,f=!1){let g=c instanceof Worker,h=Promise.all([c||d.createQrEngine(),d._loadImage(a)]).then(([a,f])=>{c=a;let h;[e,h]=this._drawToCanvas(f,b,e,k);return c instanceof Worker?(g||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let k,f,g;f=e=>{"qrResult"===e.data.type&&(c.removeEventListener("message",
f),c.removeEventListener("error",g),clearTimeout(k),null!==e.data.data?a(e.data.data):b(d.NO_QR_CODE_FOUND))};g=a=>{c.removeEventListener("message",f);c.removeEventListener("error",g);clearTimeout(k);b("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",f);c.addEventListener("error",g);k=setTimeout(()=>g("timeout"),1E4);let l=h.getImageData(0,0,e.width,e.height);c.postMessage({type:"decode",data:l},[l.data.buffer])})):new Promise((a,b)=>{let f=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(f))})});b&&f&&(h=h.catch(()=>d.scanImage(a,null,c,e,k)));return h=h.finally(()=>{g||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._scanRegion=this._calculateScanRegion(this.$video);this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video)}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,
a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_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._scanRegion,a,this.$canvas)).then(this._onDecode,a=>{this._active&&(-1!==(a.message||a).indexOf("service unavailable")&&
(this._qrEnginePromise=d.createQrEngine()),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,f=b&&b.y?b.y:0,g=b&&
b.width?b.width:a.width||a.videoWidth,h=b&&b.height?b.height:a.height||a.videoHeight;e||(e=b&&b.downScaledWidth?b.downScaledWidth:g,b=b&&b.downScaledHeight?b.downScaledHeight:h,c.width!==e&&(c.width=e),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,d,f,g,h,0,0,c.width,c.height);return[c,b]}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})
'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 d.listCameras(!1).then(a=>!!a.length).catch(()=>!1)}static listCameras(a=!1){if(!navigator.mediaDevices)return Promise.resolve([]);let b=null;return(a?navigator.mediaDevices.getUserMedia({audio:!1,video:!0}).then(a=>b=a).catch(()=>{}):Promise.resolve()).then(()=>navigator.mediaDevices.enumerateDevices()).then(a=>
a.filter(a=>"videoinput"===a.kind).map((a,b)=>({id:a.deviceId,label:a.label||(0===b?"Default Camera":`Camera ${b+1}`)}))).finally(()=>{if(b)for(let a of b.getTracks())a.stop(),b.removeTrack(a)})}constructor(a,b,c=this._onDecodeError,e=this._calculateScanRegion,h="environment"){this.$video=a;this.$canvas=document.createElement("canvas");this._onDecode=b;this._legacyCanvasSize=d.DEFAULT_CANVAS_SIZE;this._preferredCamera=h;this._flashOn=this._paused=this._active=!1;"number"===typeof c?(this._legacyCanvasSize=
c,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._onDecodeError=c;"number"===typeof e?(this._legacyCanvasSize=e,console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future")):this._calculateScanRegion=e;this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);
a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let f=!1;a.hidden&&(a.hidden=!1,f=!0);document.body.contains(a)||(document.body.appendChild(a),f=!0);requestAnimationFrame(()=>{let b=window.getComputedStyle(a);"none"===b.display&&(a.style.setProperty("display","block","important"),f=!0);"visible"!==b.visibility&&(a.style.setProperty("visibility","visible","important"),f=!0);f&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity=
0,a.style.width=0,a.style.height=0)});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);this._qrEnginePromise=d.createQrEngine()}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._onLoadedMetaData);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;if(document.hidden)return Promise.resolve();this._paused=!1;if(this.$video.srcObject)return this.$video.play(),Promise.resolve();let a="environment"===this._preferredCamera||"user"===this._preferredCamera?this._preferredCamera:null;return this._getCameraStream(this._preferredCamera,!0).catch(()=>{a="environment"===a?
"user":"environment";return this._getCameraStream()}).then(b=>{a=this._getFacingMode(b)||a||"environment";this.$video.srcObject=b;this.$video.play();this._setVideoMirror(a)}).catch(a=>{this._active=!1;throw a;})}stop(){this.pause();this._active=!1}pause(a=!1){this._paused=!0;if(!this._active)return Promise.resolve(!0);this.$video.pause();let b=()=>{const a=this.$video.srcObject?this.$video.srcObject.getTracks():[];for(const b of a)b.stop(),this.$video.srcObject.removeTrack(b);this.$video.srcObject=
null};return a?(b(),Promise.resolve(!0)):(new Promise(a=>setTimeout(a,300))).then(()=>{if(!this._paused)return!1;b();return!0})}setCamera(a){if(a===this._preferredCamera)return Promise.resolve();this._preferredCamera=a;let b=this._paused;return this.pause(!0).then(a=>{if(a&&!b&&this._active)return this.start()})}static scanImage(a,b=null,c=null,e=null,h=!1,f=!1){let g=c instanceof Worker,k=Promise.all([c||d.createQrEngine(),d._loadImage(a)]).then(([a,f])=>{c=a;let k;[e,k]=this._drawToCanvas(f,b,e,
h);return c instanceof Worker?(g||c.postMessage({type:"inversionMode",data:"both"}),new Promise((a,b)=>{let h,f,g;f=e=>{"qrResult"===e.data.type&&(c.removeEventListener("message",f),c.removeEventListener("error",g),clearTimeout(h),null!==e.data.data?a(e.data.data):b(d.NO_QR_CODE_FOUND))};g=a=>{c.removeEventListener("message",f);c.removeEventListener("error",g);clearTimeout(h);b("Scanner error: "+(a?a.message||a:"Unknown Error"))};c.addEventListener("message",f);c.addEventListener("error",g);h=setTimeout(()=>
g("timeout"),1E4);let l=k.getImageData(0,0,e.width,e.height);c.postMessage({type:"decode",data:l},[l.data.buffer])})):new Promise((a,b)=>{let h=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(h))})});b&&f&&(k=k.catch(()=>d.scanImage(a,null,c,e,h)));return k=k.finally(()=>{g||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._scanRegion=this._calculateScanRegion(this.$video);this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video)}_onVisibilityChange(){document.hidden?
this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_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._scanRegion,
a,this.$canvas)).then(this._onDecode,a=>{this._active&&(-1!==(a.message||a).indexOf("service unavailable")&&(this._qrEnginePromise=d.createQrEngine()),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}},{}];if(a){let e="environment"===a||"user"===a?"facingMode":"deviceId";b&&(a={exact:a});c.forEach(b=>b[e]=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,f=b&&b.y?b.y:0,g=b&&b.width?b.width:a.width||a.videoWidth,k=b&&b.height?b.height:a.height||a.videoHeight;e||(e=b&&b.downScaledWidth?b.downScaledWidth:g,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==e&&(c.width=e),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,d,f,g,k,0,0,c.width,
c.height);return[c,b]}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
File diff suppressed because one or more lines are too long
+93 -32
View File
@@ -1,27 +1,55 @@
export default class QrScanner {
/* async */
static hasCamera() {
if (!navigator.mediaDevices) return Promise.resolve(false);
// note that enumerateDevices can always be called and does not prompt the user for permission. However, device
// labels are only readable if served via https and an active media stream exists or permanent permission is
// given. That doesn't matter for us though as we don't require labels.
return navigator.mediaDevices.enumerateDevices()
.then(devices => devices.some(device => device.kind === 'videoinput'))
return QrScanner.listCameras(false)
.then(cameras => !!cameras.length)
.catch(() => false);
}
/* async */
static listCameras(requestLabels = false) {
if (!navigator.mediaDevices) return Promise.resolve([]);
// Note that enumerateDevices can always be called and does not prompt the user for permission.
// However, enumerateDevices only includes device labels if served via https and an active media stream exists
// or permission to access the camera was given. Therefore, ask for camera permission by opening a stream, if
// labels were requested.
let openedStream = null;
return (requestLabels
? navigator.mediaDevices.getUserMedia({ audio: false, video: true })
.then(stream => openedStream = stream)
// Fail gracefully, especially if the device has no camera or on mobile when the camera is already in
// use and some browsers disallow a second stream.
.catch(() => {})
: Promise.resolve()
)
.then(() => navigator.mediaDevices.enumerateDevices())
.then(devices => devices.filter(device => device.kind === 'videoinput').map((device, i) => ({
id: device.deviceId,
label: device.label || (i === 0 ? 'Default Camera' : `Camera ${i + 1}`),
})))
.finally(() => {
// close the stream we just opened for getting camera access for listing the device labels
if (!openedStream) return;
for (const track of openedStream.getTracks()) {
track.stop();
openedStream.removeTrack(track);
}
});
}
constructor(
video,
onDecode,
canvasSizeOrOnDecodeError = this._onDecodeError,
canvasSizeOrCalculateScanRegion = this._calculateScanRegion,
preferredFacingMode = 'environment'
preferredCamera = 'environment'
) {
this.$video = video;
this.$canvas = document.createElement('canvas');
this._onDecode = onDecode;
this._legacyCanvasSize = QrScanner.DEFAULT_CANVAS_SIZE;
this._preferredFacingMode = preferredFacingMode;
this._preferredCamera = preferredCamera;
this._active = false;
this._paused = false;
this._flashOn = false;
@@ -156,33 +184,38 @@ export default class QrScanner {
console.warn('The camera stream is only accessible if the page is transferred via https.');
}
this._active = true;
this._paused = false;
if (document.hidden) {
// camera will be started as soon as tab is in foreground
return Promise.resolve();
}
clearTimeout(this._offTimeout);
this._offTimeout = null;
this._paused = false;
if (this.$video.srcObject) {
// camera stream already/still set
this.$video.play();
return Promise.resolve();
}
let facingMode = this._preferredFacingMode;
return this._getCameraStream(facingMode, true)
let facingModeGuess = this._preferredCamera === 'environment' || this._preferredCamera === 'user'
? this._preferredCamera
: null;
return this._getCameraStream(this._preferredCamera, true)
.catch(() => {
// We (probably) don't have a camera of the requested facing mode
facingMode = facingMode === 'environment' ? 'user' : 'environment';
return this._getCameraStream(); // throws if camera is not accessible (e.g. due to not https)
// If _preferredCamera was a facing mode, we (probably) don't have a camera of the requested facing mode
// and switch our facing mode guess.
facingModeGuess = facingModeGuess === 'environment'
? 'user' // switch as the requested facing mode was not available
: 'environment'; // switch, or also assume 'environment' as default if facingModeGuess was null
// Retry unconstrained. Throws if camera is not accessible (e.g. due to not https)
return this._getCameraStream();
})
.then(stream => {
// Try to determine the facing mode from the stream, otherwise use our guess. Note that the guess is not
// always accurate as Safari returns cameras of different facing mode, even for exact constraints.
facingMode = this._getFacingMode(stream) || facingMode;
// Try to determine the facing mode from the stream, otherwise use our guess or 'environment' as
// default. Note that the guess is not always accurate as Safari returns cameras of different facing
// mode, even for exact facingMode constraints.
facingModeGuess = this._getFacingMode(stream) || facingModeGuess || 'environment';
this.$video.srcObject = stream;
this.$video.play();
this._setVideoMirror(facingMode);
this._setVideoMirror(facingModeGuess);
})
.catch(e => {
this._active = false;
@@ -195,23 +228,47 @@ export default class QrScanner {
this._active = false;
}
pause() {
pause(stopStreamImmediately = false) {
this._paused = true;
if (!this._active) {
return;
return Promise.resolve(true);
}
this.$video.pause();
if (this._offTimeout) {
return;
}
this._offTimeout = setTimeout(() => {
const stopStream = () => {
const tracks = this.$video.srcObject ? this.$video.srcObject.getTracks() : [];
for (const track of tracks) {
track.stop(); // note that this will also automatically turn the flashlight off
this.$video.srcObject.removeTrack(track);
}
this.$video.srcObject = null;
this._offTimeout = null;
}, 300);
};
if (stopStreamImmediately) {
stopStream();
return Promise.resolve(true);
}
return new Promise((resolve) => setTimeout(resolve, 300))
.then(() => {
if (!this._paused) return false;
stopStream();
return true;
});
}
/* async */
setCamera(facingModeOrDeviceId) {
if (facingModeOrDeviceId === this._preferredCamera) return Promise.resolve();
this._preferredCamera = facingModeOrDeviceId;
// Restart the scanner with the new camera which will also update the video mirror and the scan region. Note
// that we always pause the stream and not only if !this._paused as even if this._paused === true, the stream
// might still be running, as it's by default only stopped after a delay of 300ms.
const wasPaused = this._paused;
return this.pause(true).then((paused) => {
if (!paused || wasPaused || !this._active) return;
return this.start();
});
}
/* async */
@@ -379,19 +436,23 @@ export default class QrScanner {
console.log(error);
}
_getCameraStream(facingMode, exact = false) {
_getCameraStream(preferredCamera, exact = false) {
const constraintsToTry = [{
width: { min: 1024 }
}, {
width: { min: 768 }
}, {}];
if (facingMode) {
if (preferredCamera) {
const preferenceType = preferredCamera === 'environment' || preferredCamera === 'user'
? 'facingMode'
: 'deviceId';
if (exact) {
facingMode = { exact: facingMode };
preferredCamera = { exact: preferredCamera };
}
constraintsToTry.forEach(constraint => constraint.facingMode = facingMode);
constraintsToTry.forEach(constraint => constraint[preferenceType] = preferredCamera);
}
return this._getMatchingCameraStream(constraintsToTry);
}
+13 -3
View File
@@ -9,12 +9,14 @@ declare class QrScanner {
static hasCamera(): Promise<boolean>;
static listCameras(requestLabels: boolean): Promise<Array<QrScanner.Camera>>;
constructor(
video: HTMLVideoElement,
onDecode: (result: string) => void,
onDecodeError?: (error: string) => void,
calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion,
preferredFacingMode?: 'environment' | 'user',
preferredCamera?: 'environment' | 'user' | QrScanner.DeviceId,
);
/** @deprecated */
constructor(
@@ -22,7 +24,7 @@ declare class QrScanner {
onDecode: (result: string) => void,
onDecodeError?: (error: string) => void,
canvasSize?: number,
preferredFacingMode?: 'environment' | 'user',
preferredCamera?: 'environment' | 'user' | QrScanner.DeviceId,
);
/** @deprecated */
constructor(video: HTMLVideoElement, onDecode: (result: string) => void, canvasSize?: number);
@@ -35,7 +37,8 @@ declare class QrScanner {
destroy(): void;
start(): Promise<void>;
stop(): void;
pause(): void;
pause(stopStreamImmediately?: boolean): Promise<boolean>;
setCamera(facingModeOrDeviceId: 'environment' | 'user' | QrScanner.DeviceId): Promise<void>;
setGrayscaleWeights(red: number, green: number, blue: number, useIntegerApproximation?: boolean): void;
setInversionMode(inversionMode: QrScanner.InversionMode): void;
static scanImage(
@@ -67,6 +70,13 @@ declare namespace QrScanner {
downScaledHeight?: number;
}
export type DeviceId = string;
export interface Camera {
id: DeviceId;
label: string;
}
export type InversionMode = 'original' | 'invert' | 'both';
}