forked from nimiq/qr-scanner
-
Notifications
You must be signed in to change notification settings - Fork 0
/
qr-scanner.legacy.min.js.map
1 lines (1 loc) · 52.3 KB
/
qr-scanner.legacy.min.js.map
1
{"version":3,"file":"qr-scanner.legacy.min.js","sources":["src/qr-scanner.ts"],"sourcesContent":["class QrScanner {\n static readonly DEFAULT_CANVAS_SIZE = 400;\n static readonly NO_QR_CODE_FOUND = 'No QR code found';\n private static _disableBarcodeDetector = false;\n private static _workerMessageId = 0;\n\n /** @deprecated */\n static set WORKER_PATH(workerPath: string) {\n console.warn('Setting QrScanner.WORKER_PATH is not required and not supported anymore. '\n + 'Have a look at the README for new setup instructions.');\n }\n\n static async hasCamera(): Promise<boolean> {\n try {\n return !!(await QrScanner.listCameras(false)).length;\n } catch (e) {\n return false;\n }\n }\n\n static async listCameras(requestLabels = false): Promise<Array<QrScanner.Camera>> {\n if (!navigator.mediaDevices) return [];\n\n const enumerateCameras = async (): Promise<Array<MediaDeviceInfo>> =>\n (await navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === 'videoinput');\n\n // Note that enumerateDevices can always be called and does not prompt the user for permission.\n // However, enumerateDevices only includes device labels if served via https and an active media stream exists\n // or permission to access the camera was given. Therefore, if we're not getting labels but labels are requested\n // ask for camera permission by opening a stream.\n let openedStream: MediaStream | undefined;\n try {\n if (requestLabels && (await enumerateCameras()).every((camera) => !camera.label)) {\n openedStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });\n }\n } catch (e) {\n // Fail gracefully, especially if the device has no camera or on mobile when the camera is already in use\n // and some browsers disallow a second stream.\n }\n\n try {\n return (await enumerateCameras()).map((camera, i) => ({\n id: camera.deviceId,\n label: camera.label || (i === 0 ? 'Default Camera' : `Camera ${i + 1}`),\n }));\n } finally {\n // close the stream we just opened for getting camera access for listing the device labels\n if (openedStream) {\n console.warn('Call listCameras after successfully starting a QR scanner to avoid creating '\n + 'a temporary video stream');\n QrScanner._stopVideoStream(openedStream);\n }\n }\n }\n\n readonly $video: HTMLVideoElement;\n readonly $canvas: HTMLCanvasElement;\n readonly $overlay?: HTMLDivElement;\n private readonly $codeOutlineHighlight?: SVGSVGElement;\n private readonly _onDecode?: (result: QrScanner.ScanResult) => void;\n private readonly _legacyOnDecode?: (result: string) => void;\n private readonly _legacyCanvasSize: number = QrScanner.DEFAULT_CANVAS_SIZE;\n private _preferredCamera: QrScanner.FacingMode | QrScanner.DeviceId = 'environment';\n private readonly _maxScansPerSecond: number = 25;\n private _lastScanTimestamp: number = -1;\n private _scanRegion: QrScanner.ScanRegion;\n private _codeOutlineHighlightRemovalTimeout?: number;\n private _qrEnginePromise: Promise<Worker | BarcodeDetector>\n private _active: boolean = false;\n private _paused: boolean = false;\n private _flashOn: boolean = false;\n private _destroyed: boolean = false;\n\n constructor(\n video: HTMLVideoElement,\n onDecode: (result: QrScanner.ScanResult) => void,\n options: {\n onDecodeError?: (error: Error | string) => void,\n calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion,\n preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,\n maxScansPerSecond?: number;\n highlightScanRegion?: boolean,\n highlightCodeOutline?: boolean,\n overlay?: HTMLDivElement,\n /** just a temporary flag until we switch entirely to the new api */\n returnDetailedScanResult?: true,\n },\n );\n /** @deprecated */\n constructor(\n video: HTMLVideoElement,\n onDecode: (result: string) => void,\n onDecodeError?: (error: Error | string) => void,\n calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion,\n preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,\n );\n /** @deprecated */\n constructor(\n video: HTMLVideoElement,\n onDecode: (result: string) => void,\n onDecodeError?: (error: Error | string) => void,\n canvasSize?: number,\n preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,\n );\n /** @deprecated */\n constructor(video: HTMLVideoElement, onDecode: (result: string) => void, canvasSize?: number);\n constructor(\n video: HTMLVideoElement,\n onDecode: ((result: QrScanner.ScanResult) => void) | ((result: string) => void),\n canvasSizeOrOnDecodeErrorOrOptions?: number | ((error: Error | string) => void) | {\n onDecodeError?: (error: Error | string) => void,\n calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion,\n preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,\n maxScansPerSecond?: number;\n highlightScanRegion?: boolean,\n highlightCodeOutline?: boolean,\n overlay?: HTMLDivElement,\n /** just a temporary flag until we switch entirely to the new api */\n returnDetailedScanResult?: true,\n },\n canvasSizeOrCalculateScanRegion?: number | ((video: HTMLVideoElement) => QrScanner.ScanRegion),\n preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,\n ) {\n this.$video = video;\n this.$canvas = document.createElement('canvas');\n\n if (canvasSizeOrOnDecodeErrorOrOptions && typeof canvasSizeOrOnDecodeErrorOrOptions === 'object') {\n // we got an options object using the new api\n this._onDecode = onDecode as QrScanner['_onDecode'];\n } else {\n if (canvasSizeOrOnDecodeErrorOrOptions || canvasSizeOrCalculateScanRegion || preferredCamera) {\n console.warn('You\\'re using a deprecated version of the QrScanner constructor which will be removed in '\n + 'the future');\n } else {\n // Only video and onDecode were specified and we can't distinguish between new or old api usage. For\n // backwards compatibility we have to assume the old api for now. The options object is marked as non-\n // optional in the parameter list above to make clear that ScanResult instead of string is only passed\n // if an options object was provided. However, in the future once legacy support is removed, the options\n // object should become optional.\n console.warn('Note that the type of the scan result passed to onDecode will change in the future. '\n + 'To already switch to the new api today, you can pass returnDetailedScanResult: true.');\n }\n this._legacyOnDecode = onDecode as QrScanner['_legacyOnDecode'];\n }\n\n const options = typeof canvasSizeOrOnDecodeErrorOrOptions === 'object'\n ? canvasSizeOrOnDecodeErrorOrOptions\n : {};\n this._onDecodeError = options.onDecodeError || (typeof canvasSizeOrOnDecodeErrorOrOptions === 'function'\n ? canvasSizeOrOnDecodeErrorOrOptions\n : this._onDecodeError);\n this._calculateScanRegion = options.calculateScanRegion || (typeof canvasSizeOrCalculateScanRegion==='function'\n ? canvasSizeOrCalculateScanRegion\n : this._calculateScanRegion);\n this._preferredCamera = options.preferredCamera || preferredCamera || this._preferredCamera;\n this._legacyCanvasSize = typeof canvasSizeOrOnDecodeErrorOrOptions === 'number'\n ? canvasSizeOrOnDecodeErrorOrOptions\n : typeof canvasSizeOrCalculateScanRegion === 'number'\n ? canvasSizeOrCalculateScanRegion\n : this._legacyCanvasSize;\n this._maxScansPerSecond = options.maxScansPerSecond || this._maxScansPerSecond;\n\n this._onPlay = this._onPlay.bind(this);\n this._onLoadedMetaData = this._onLoadedMetaData.bind(this);\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\n this._updateOverlay = this._updateOverlay.bind(this);\n\n // @ts-ignore\n video.disablePictureInPicture = true;\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 // @ts-ignore\n 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 video.muted = true;\n\n // Avoid Safari stopping the video stream on a hidden video.\n // See https://github.com/cozmo/jsQR/issues/185\n let shouldHideVideo = false;\n if (video.hidden) {\n video.hidden = false;\n shouldHideVideo = true;\n }\n if (!document.body.contains(video)) {\n document.body.appendChild(video);\n shouldHideVideo = true;\n }\n const videoContainer = video.parentElement!;\n\n if (options.highlightScanRegion || options.highlightCodeOutline) {\n const gotExternalOverlay = !!options.overlay;\n this.$overlay = options.overlay || document.createElement('div');\n const overlayStyle = this.$overlay.style;\n overlayStyle.position = 'absolute';\n overlayStyle.display = 'none';\n overlayStyle.pointerEvents = 'none';\n this.$overlay.classList.add('scan-region-highlight');\n if (!gotExternalOverlay && options.highlightScanRegion) {\n // default style; can be overwritten via css, e.g. by changing the svg's stroke color, hiding the\n // .scan-region-highlight-svg, setting a border, outline, background, etc.\n this.$overlay.innerHTML = '<svg class=\"scan-region-highlight-svg\" viewBox=\"0 0 238 238\" '\n + 'preserveAspectRatio=\"none\" style=\"position:absolute;width:100%;height:100%;left:0;top:0;'\n + 'fill:none;stroke:#e9b213;stroke-width:4;stroke-linecap:round;stroke-linejoin:round\">'\n + '<path d=\"M31 2H10a8 8 0 0 0-8 8v21M207 2h21a8 8 0 0 1 8 8v21m0 176v21a8 8 0 0 1-8 8h-21m-176 '\n + '0H10a8 8 0 0 1-8-8v-21\"/></svg>';\n try {\n this.$overlay.firstElementChild!.animate({ transform: ['scale(.98)', 'scale(1.01)'] }, {\n duration: 400,\n iterations: Infinity,\n direction: 'alternate',\n easing: 'ease-in-out',\n });\n } catch (e) {}\n videoContainer.insertBefore(this.$overlay, this.$video.nextSibling);\n }\n if (options.highlightCodeOutline) {\n // default style; can be overwritten via css\n this.$overlay.insertAdjacentHTML(\n 'beforeend',\n '<svg class=\"code-outline-highlight\" preserveAspectRatio=\"none\" style=\"display:none;width:100%;'\n + 'height:100%;fill:none;stroke:#e9b213;stroke-width:5;stroke-dasharray:25;'\n + 'stroke-linecap:round;stroke-linejoin:round\"><polygon/></svg>',\n );\n this.$codeOutlineHighlight = this.$overlay.lastElementChild as SVGSVGElement;\n }\n }\n this._scanRegion = this._calculateScanRegion(video);\n\n requestAnimationFrame(() => {\n // Checking in requestAnimationFrame which should avoid a potential additional re-flow for getComputedStyle.\n const videoStyle = window.getComputedStyle(video);\n if (videoStyle.display === 'none') {\n video.style.setProperty('display', 'block', 'important');\n shouldHideVideo = true;\n }\n if (videoStyle.visibility !== 'visible') {\n video.style.setProperty('visibility', 'visible', 'important');\n shouldHideVideo = true;\n }\n if (shouldHideVideo) {\n // Hide the video in a way that doesn't cause Safari to stop the playback.\n console.warn('QrScanner has overwritten the video hiding style to avoid Safari stopping the playback.');\n video.style.opacity = '0';\n video.style.width = '0';\n video.style.height = '0';\n if (this.$overlay && this.$overlay.parentElement) {\n this.$overlay.parentElement.removeChild(this.$overlay);\n }\n // @ts-ignore\n delete this.$overlay!;\n // @ts-ignore\n delete this.$codeOutlineHighlight!;\n }\n\n if (this.$overlay) {\n this._updateOverlay();\n }\n });\n\n video.addEventListener('play', this._onPlay);\n video.addEventListener('loadedmetadata', this._onLoadedMetaData);\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n window.addEventListener('resize', this._updateOverlay);\n\n this._qrEnginePromise = QrScanner.createQrEngine();\n }\n\n async hasFlash(): Promise<boolean> {\n let stream: MediaStream | undefined;\n try {\n if (this.$video.srcObject) {\n if (!(this.$video.srcObject instanceof MediaStream)) return false; // srcObject is not a camera stream\n stream = this.$video.srcObject;\n } else {\n stream = (await this._getCameraStream()).stream;\n }\n return 'torch' in stream.getVideoTracks()[0].getSettings();\n } catch (e) {\n return false;\n } finally {\n // close the stream we just opened for detecting whether it supports flash\n if (stream && stream !== this.$video.srcObject) {\n console.warn('Call hasFlash after successfully starting the scanner to avoid creating '\n + 'a temporary video stream');\n QrScanner._stopVideoStream(stream);\n }\n }\n }\n\n isFlashOn(): boolean {\n return this._flashOn;\n }\n\n async toggleFlash(): Promise<void> {\n if (this._flashOn) {\n await this.turnFlashOff();\n } else {\n await this.turnFlashOn();\n }\n }\n\n async turnFlashOn(): Promise<void> {\n\n this._flashOn = true;\n return await (this.$video.srcObject as MediaStream).getVideoTracks()[0].applyConstraints({\n // @ts-ignore: constraint 'torch' is unknown to ts\n advanced: [{ torch: true }],\n });\n \n }\n\n async turnFlashOff(): Promise<void> {\n\n this._flashOn = false;\n return await (this.$video.srcObject as MediaStream).getVideoTracks()[0].applyConstraints({\n // @ts-ignore: constraint 'torch' is unknown to ts\n advanced: [{ torch: false }],\n });\n }\n\n destroy(): void {\n this.$video.removeEventListener('loadedmetadata', this._onLoadedMetaData);\n this.$video.removeEventListener('play', this._onPlay);\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n window.removeEventListener('resize', this._updateOverlay);\n\n this._destroyed = true;\n this._flashOn = false;\n this.stop(); // sets this._paused = true and this._active = false\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'close');\n }\n\n async start(): Promise<void> {\n if (this._destroyed) throw new Error('The QR scanner can not be started as it had been destroyed.');\n if (this._active && !this._paused) return;\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\n this._active = true;\n if (document.hidden) return; // camera will be started as soon as tab is in foreground\n this._paused = false;\n if (this.$video.srcObject) {\n // camera stream already/still set\n await this.$video.play();\n return;\n }\n\n try {\n const { stream, facingMode } = await this._getCameraStream();\n if (!this._active || this._paused) {\n // was stopped in the meantime\n QrScanner._stopVideoStream(stream);\n return;\n }\n this._setVideoMirror(facingMode);\n this.$video.srcObject = stream;\n await this.$video.play();\n\n // Restart the flash if it was previously on\n if (this._flashOn) {\n this._flashOn = false; // force turnFlashOn to restart the flash\n this.turnFlashOn().catch(() => {});\n }\n } catch (e) {\n if (this._paused) return;\n this._active = false;\n throw e;\n }\n }\n\n stop(): void {\n this.pause();\n this._active = false;\n }\n\n async pause(stopStreamImmediately = false): Promise<boolean> {\n this._paused = true;\n if (!this._active) return true;\n this.$video.pause();\n\n if (this.$overlay) {\n this.$overlay.style.display = 'none';\n }\n\n const stopStream = () => {\n if (this.$video.srcObject instanceof MediaStream) {\n // revoke srcObject only if it's a stream which was likely set by us\n QrScanner._stopVideoStream(this.$video.srcObject);\n this.$video.srcObject = null;\n }\n };\n\n if (stopStreamImmediately) {\n stopStream();\n return true;\n }\n\n await new Promise((resolve) => setTimeout(resolve, 300));\n if (!this._paused) return false;\n stopStream();\n return true;\n }\n\n async setCamera(facingModeOrDeviceId: QrScanner.FacingMode | QrScanner.DeviceId): Promise<void> {\n if (facingModeOrDeviceId === this._preferredCamera) return;\n this._preferredCamera = facingModeOrDeviceId;\n // Restart the scanner with the new camera which will also update the video mirror and the scan region.\n await this._restartVideoStream();\n }\n\n static async scanImage(\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement | File | Blob | URL | String,\n options: {\n scanRegion?: QrScanner.ScanRegion | null,\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\n canvas?: HTMLCanvasElement | null,\n disallowCanvasResizing?: boolean,\n alsoTryWithoutScanRegion?: boolean,\n /** just a temporary flag until we switch entirely to the new api */\n returnDetailedScanResult?: true,\n },\n ): Promise<QrScanner.ScanResult>;\n /** @deprecated */\n static async scanImage(\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement | File | Blob | URL | String,\n scanRegion?: QrScanner.ScanRegion | null,\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\n canvas?: HTMLCanvasElement | null,\n disallowCanvasResizing?: boolean,\n alsoTryWithoutScanRegion?: boolean,\n ): Promise<string>;\n static async scanImage(\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement | File | Blob | URL | String,\n scanRegionOrOptions?: QrScanner.ScanRegion | {\n scanRegion?: QrScanner.ScanRegion | null,\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\n canvas?: HTMLCanvasElement | null,\n disallowCanvasResizing?: boolean,\n alsoTryWithoutScanRegion?: boolean,\n /** just a temporary flag until we switch entirely to the new api */\n returnDetailedScanResult?: true,\n } | null,\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\n canvas?: HTMLCanvasElement | null,\n disallowCanvasResizing: boolean = false,\n alsoTryWithoutScanRegion: boolean = false,\n ): Promise<string | QrScanner.ScanResult> {\n let scanRegion: QrScanner.ScanRegion | null | undefined;\n let returnDetailedScanResult = false;\n if (scanRegionOrOptions && (\n 'scanRegion' in scanRegionOrOptions\n || 'qrEngine' in scanRegionOrOptions\n || 'canvas' in scanRegionOrOptions\n || 'disallowCanvasResizing' in scanRegionOrOptions\n || 'alsoTryWithoutScanRegion' in scanRegionOrOptions\n || 'returnDetailedScanResult' in scanRegionOrOptions\n )) {\n // we got an options object using the new api\n scanRegion = scanRegionOrOptions.scanRegion;\n qrEngine = scanRegionOrOptions.qrEngine;\n canvas = scanRegionOrOptions.canvas;\n disallowCanvasResizing = scanRegionOrOptions.disallowCanvasResizing || false;\n alsoTryWithoutScanRegion = scanRegionOrOptions.alsoTryWithoutScanRegion || false;\n returnDetailedScanResult = true;\n } else if (scanRegionOrOptions || qrEngine || canvas || disallowCanvasResizing || alsoTryWithoutScanRegion) {\n console.warn('You\\'re using a deprecated api for scanImage which will be removed in the future.');\n } else {\n // Only imageOrFileOrBlobOrUrl was specified and we can't distinguish between new or old api usage. For\n // backwards compatibility we have to assume the old api for now. The options object is marked as non-\n // optional in the parameter list above to make clear that ScanResult instead of string is only returned if\n // an options object was provided. However, in the future once legacy support is removed, the options object\n // should become optional.\n console.warn('Note that the return type of scanImage will change in the future. To already switch to the '\n + 'new api today, you can pass returnDetailedScanResult: true.');\n }\n\n const gotExternalEngine = !!qrEngine;\n\n try {\n let image: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement;\n let canvasContext: CanvasRenderingContext2D;\n\n if('then' in qrEngine!) {\n\n qrEngine = await qrEngine.then(value=> {\n return value;\n });\n }\n \n qrEngine = await qrEngine === null ? QrScanner.createQrEngine() : qrEngine;\n image = await QrScanner._loadImage(imageOrFileOrBlobOrUrl);\n\n\n\n [canvas, canvasContext] = QrScanner._drawToCanvas(image, scanRegion, canvas, disallowCanvasResizing);\n let detailedScanResult: QrScanner.ScanResult;\n\n if (qrEngine instanceof Worker) {\n const qrEngineWorker = qrEngine; // for ts to know that it's still a worker later in the event listeners\n if (!gotExternalEngine) {\n // Enable scanning of inverted color qr codes.\n QrScanner._postWorkerMessageSync(qrEngineWorker, 'inversionMode', 'both');\n }\n detailedScanResult = await new Promise((resolve, reject) => {\n let timeout: number;\n let onMessage: (event: MessageEvent) => void;\n let onError: (error: ErrorEvent | string) => void;\n let expectedResponseId = -1;\n onMessage = (event: MessageEvent) => {\n if (event.data.id !== expectedResponseId) {\n return;\n }\n qrEngineWorker.removeEventListener('message', onMessage);\n qrEngineWorker.removeEventListener('error', onError);\n clearTimeout(timeout);\n if (event.data.data !== null) {\n resolve({\n data: event.data.data,\n cornerPoints: QrScanner._convertPoints(event.data.cornerPoints, scanRegion),\n });\n } else {\n reject(QrScanner.NO_QR_CODE_FOUND);\n }\n };\n onError = (error: ErrorEvent | string) => {\n qrEngineWorker.removeEventListener('message', onMessage);\n qrEngineWorker.removeEventListener('error', onError);\n clearTimeout(timeout);\n const errorMessage = !error ? 'Unknown Error' : ((error as ErrorEvent).message || error);\n reject('Scanner error: ' + errorMessage);\n };\n qrEngineWorker.addEventListener('message', onMessage);\n qrEngineWorker.addEventListener('error', onError);\n timeout = setTimeout(() => onError('timeout'), 10000);\n const imageData = canvasContext.getImageData(0, 0, canvas!.width, canvas!.height);\n expectedResponseId = QrScanner._postWorkerMessageSync(\n qrEngineWorker,\n 'decode',\n imageData,\n [imageData.data.buffer],\n );\n });\n } else {\n detailedScanResult = await Promise.race([\n new Promise<QrScanner.ScanResult>((resolve, reject) => window.setTimeout(\n () => reject('Scanner error: timeout'),\n 10000,\n )),\n (async (): Promise<QrScanner.ScanResult> => {\n try {\n\n let scanResult;\n if('detect' in qrEngine!){\n\n [scanResult] = await qrEngine.detect(canvas!);\n\n }\n \n if (!scanResult) throw QrScanner.NO_QR_CODE_FOUND;\n return {\n data: scanResult.rawValue,\n cornerPoints: QrScanner._convertPoints(scanResult.cornerPoints, scanRegion),\n };\n } catch (e) {\n const errorMessage = (e as Error).message || e as string;\n if (/not implemented|service unavailable/.test(errorMessage)) {\n // Not implemented can apparently for some reason happen even though getSupportedFormats\n // in createQrScanner reported that it's supported, see issue #98.\n // Service unavailable can happen after some time when the BarcodeDetector crashed and\n // can theoretically be recovered from by creating a new BarcodeDetector. However, in\n // newer browsers this issue does not seem to be present anymore and therefore we do not\n // apply this optimization anymore but just set _disableBarcodeDetector in both cases.\n // Also note that if we got an external qrEngine that crashed, we should possibly notify\n // the caller about it, but we also don't do this here, as it's such an unlikely case.\n QrScanner._disableBarcodeDetector = true;\n // retry without passing the broken BarcodeScanner instance\n return QrScanner.scanImage(imageOrFileOrBlobOrUrl, {\n scanRegion,\n canvas,\n disallowCanvasResizing,\n alsoTryWithoutScanRegion,\n });\n }\n throw `Scanner error: ${errorMessage}`;\n }\n })(),\n ]);\n }\n return returnDetailedScanResult ? detailedScanResult : detailedScanResult.data;\n } catch (e) {\n if (!scanRegion || !alsoTryWithoutScanRegion) throw e;\n const detailedScanResult = await QrScanner.scanImage(\n imageOrFileOrBlobOrUrl,\n { qrEngine, canvas, disallowCanvasResizing },\n );\n return returnDetailedScanResult ? detailedScanResult : detailedScanResult.data;\n } finally {\n if (!gotExternalEngine) {\n QrScanner._postWorkerMessage(qrEngine!, 'close');\n }\n }\n }\n\n setGrayscaleWeights(red: number, green: number, blue: number, useIntegerApproximation: boolean = true): void {\n // Note that for the native BarcodeDecoder or if the worker was destroyed, this is a no-op. However, the native\n // implementations work also well with colored qr codes.\n QrScanner._postWorkerMessage(\n this._qrEnginePromise,\n 'grayscaleWeights',\n { red, green, blue, useIntegerApproximation }\n );\n }\n\n setInversionMode(inversionMode: QrScanner.InversionMode): void {\n // Note that for the native BarcodeDecoder or if the worker was destroyed, this is a no-op. However, the native\n // implementations scan normal and inverted qr codes by default\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'inversionMode', inversionMode);\n }\n\n static async createQrEngine(): Promise<Worker | BarcodeDetector>;\n /** @deprecated */\n static async createQrEngine(workerPath: string): Promise<Worker | BarcodeDetector>;\n static async createQrEngine(workerPath?: string): Promise<Worker | BarcodeDetector> {\n if (workerPath) {\n console.warn('Specifying a worker path is not required and not supported anymore.');\n }\n const useNativeBarcodeDetector = !QrScanner._disableBarcodeDetector\n && ('BarcodeDetector' in window && BarcodeDetector.getSupportedFormats\n ? (await BarcodeDetector.getSupportedFormats()).includes('qr_code')\n : false);\n return useNativeBarcodeDetector\n ? new BarcodeDetector({ formats: ['qr_code'] })\n // @ts-ignore no types defined\n : (import('./qr-scanner-worker.min.js') as Promise<{ createWorker: () => Worker }>)\n .then((module) => module.createWorker());\n }\n\n private _onPlay(): void {\n this._scanRegion = this._calculateScanRegion(this.$video);\n this._updateOverlay();\n if (this.$overlay) {\n this.$overlay.style.display = '';\n }\n this._scanFrame();\n }\n\n private _onLoadedMetaData(): void {\n this._scanRegion = this._calculateScanRegion(this.$video);\n this._updateOverlay();\n }\n\n private _onVisibilityChange(): void {\n if (document.hidden) {\n this.pause();\n } else if (this._active) {\n this.start();\n }\n }\n\n private _calculateScanRegion(video: HTMLVideoElement): QrScanner.ScanRegion {\n // Default scan region calculation. Note that this can be overwritten in the constructor.\n const smallestDimension = Math.min(video.videoWidth, video.videoHeight);\n const scanRegionSize = Math.round(2 / 3 * smallestDimension);\n return {\n x: Math.round((video.videoWidth - scanRegionSize) / 2),\n y: Math.round((video.videoHeight - scanRegionSize) / 2),\n width: scanRegionSize,\n height: scanRegionSize,\n downScaledWidth: this._legacyCanvasSize,\n downScaledHeight: this._legacyCanvasSize,\n };\n }\n\n private _updateOverlay(): void {\n requestAnimationFrame(() => {\n // Running in requestAnimationFrame which should avoid a potential additional re-flow for getComputedStyle\n // and offsetWidth, offsetHeight, offsetLeft, offsetTop.\n if (!this.$overlay) return;\n const video = this.$video;\n const videoWidth = video.videoWidth;\n const videoHeight = video.videoHeight;\n const elementWidth = video.offsetWidth;\n const elementHeight = video.offsetHeight;\n const elementX = video.offsetLeft;\n const elementY = video.offsetTop;\n\n const videoStyle = window.getComputedStyle(video);\n const videoObjectFit = videoStyle.objectFit;\n const videoAspectRatio = videoWidth / videoHeight;\n const elementAspectRatio = elementWidth / elementHeight;\n let videoScaledWidth: number;\n let videoScaledHeight: number;\n switch (videoObjectFit) {\n case 'none':\n videoScaledWidth = videoWidth;\n videoScaledHeight = videoHeight;\n break;\n case 'fill':\n videoScaledWidth = elementWidth;\n videoScaledHeight = elementHeight;\n break;\n default: // 'cover', 'contains', 'scale-down'\n if (videoObjectFit === 'cover'\n ? videoAspectRatio > elementAspectRatio\n : videoAspectRatio < elementAspectRatio) {\n // The scaled height is the element height\n // - for 'cover' if the video aspect ratio is wider than the element aspect ratio\n // (scaled height matches element height and scaled width overflows element width)\n // - for 'contains'/'scale-down' if element aspect ratio is wider than the video aspect ratio\n // (scaled height matched element height and element width overflows scaled width)\n videoScaledHeight = elementHeight;\n videoScaledWidth = videoScaledHeight * videoAspectRatio;\n } else {\n videoScaledWidth = elementWidth;\n videoScaledHeight = videoScaledWidth / videoAspectRatio;\n }\n if (videoObjectFit === 'scale-down') {\n // for 'scale-down' the dimensions are the minimum of 'contains' and 'none'\n videoScaledWidth = Math.min(videoScaledWidth, videoWidth);\n videoScaledHeight = Math.min(videoScaledHeight, videoHeight);\n }\n }\n\n // getComputedStyle is so nice to convert keywords (left, center, right, top, bottom) to percent and makes\n // sure to set the default of 50% if only one or no component was provided, therefore we can be sure that\n // both components are set. Additionally, it converts units other than px (e.g. rem) to px.\n const [videoX, videoY] = videoStyle.objectPosition.split(' ').map((length, i) => {\n const lengthValue = parseFloat(length);\n return length.endsWith('%')\n ? (!i ? elementWidth - videoScaledWidth : elementHeight - videoScaledHeight) * lengthValue / 100\n : lengthValue;\n });\n\n const regionWidth = this._scanRegion.width || videoWidth;\n const regionHeight = this._scanRegion.height || videoHeight;\n const regionX = this._scanRegion.x || 0;\n const regionY = this._scanRegion.y || 0;\n\n const overlayStyle = this.$overlay.style;\n overlayStyle.width = `${regionWidth / videoWidth * videoScaledWidth}px`;\n overlayStyle.height = `${regionHeight / videoHeight * videoScaledHeight}px`;\n overlayStyle.top = `${elementY + videoY + regionY / videoHeight * videoScaledHeight}px`;\n const isVideoMirrored = /scaleX\\(-1\\)/.test(video.style.transform!);\n overlayStyle.left = `${elementX\n + (isVideoMirrored ? elementWidth - videoX - videoScaledWidth : videoX)\n + (isVideoMirrored ? videoWidth - regionX - regionWidth : regionX) / videoWidth * videoScaledWidth}px`;\n // apply same mirror as on video\n overlayStyle.transform = video.style.transform;\n });\n }\n\n private static _convertPoints(\n points: QrScanner.Point[],\n scanRegion?: QrScanner.ScanRegion | null,\n ): QrScanner.Point[] {\n if (!scanRegion) return points;\n const offsetX = scanRegion.x || 0;\n const offsetY = scanRegion.y || 0;\n const scaleFactorX = scanRegion.width && scanRegion.downScaledWidth\n ? scanRegion.width / scanRegion.downScaledWidth\n : 1;\n const scaleFactorY = scanRegion.height && scanRegion.downScaledHeight\n ? scanRegion.height / scanRegion.downScaledHeight\n : 1;\n for (const point of points) {\n point.x = point.x * scaleFactorX + offsetX;\n point.y = point.y * scaleFactorY + offsetY;\n }\n return points;\n }\n\n private _scanFrame(): void {\n if (!this._active || this.$video.paused || this.$video.ended) return;\n // If requestVideoFrameCallback is available use that to avoid unnecessary scans on the same frame as the\n // camera's framerate can be lower than the screen refresh rate and this._maxScansPerSecond, especially in dark\n // settings where the exposure time is longer. Both, requestVideoFrameCallback and requestAnimationFrame are not\n // being fired if the tab is in the background, which is what we want.\n const requestFrame = 'requestVideoFrameCallback' in this.$video\n // @ts-ignore\n ? this.$video.requestVideoFrameCallback.bind(this.$video)\n : requestAnimationFrame;\n requestFrame(async () => {\n if (this.$video.readyState <= 1) {\n // Skip scans until the video is ready as drawImage() only works correctly on a video with readyState\n // > 1, see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage#Notes.\n // This also avoids false positives for videos paused after a successful scan which remains visible on\n // the canvas until the video is started again and ready.\n this._scanFrame();\n return;\n }\n\n const timeSinceLastScan = Date.now() - this._lastScanTimestamp;\n const minimumTimeBetweenScans = 1000 / this._maxScansPerSecond;\n if (timeSinceLastScan < minimumTimeBetweenScans) {\n await new Promise((resolve) => setTimeout(resolve, minimumTimeBetweenScans - timeSinceLastScan));\n }\n // console.log('Scan rate:', Math.round(1000 / (Date.now() - this._lastScanTimestamp)));\n this._lastScanTimestamp = Date.now();\n\n let result: QrScanner.ScanResult | undefined;\n try {\n result = await QrScanner.scanImage(this.$video, {\n scanRegion: this._scanRegion,\n qrEngine: this._qrEnginePromise,\n canvas: this.$canvas,\n });\n } catch (error) {\n if (!this._active) return;\n this._onDecodeError(error as Error | string);\n }\n\n if (QrScanner._disableBarcodeDetector && !(await this._qrEnginePromise instanceof Worker)) {\n // replace the disabled BarcodeDetector\n this._qrEnginePromise = QrScanner.createQrEngine();\n }\n\n if (result) {\n if (this._onDecode) {\n this._onDecode(result);\n } else if (this._legacyOnDecode) {\n this._legacyOnDecode(result.data);\n }\n\n if (this.$codeOutlineHighlight) {\n clearTimeout(this._codeOutlineHighlightRemovalTimeout);\n this._codeOutlineHighlightRemovalTimeout = undefined;\n this.$codeOutlineHighlight.setAttribute(\n 'viewBox',\n `${this._scanRegion.x || 0} `\n + `${this._scanRegion.y || 0} `\n + `${this._scanRegion.width || this.$video.videoWidth} `\n + `${this._scanRegion.height || this.$video.videoHeight}`,\n );\n const polygon = this.$codeOutlineHighlight.firstElementChild!;\n polygon.setAttribute('points', result.cornerPoints.map(({x, y}) => `${x},${y}`).join(' '));\n this.$codeOutlineHighlight.style.display = '';\n }\n } else if (this.$codeOutlineHighlight && !this._codeOutlineHighlightRemovalTimeout) {\n // hide after timeout to make it flash less when on some frames the QR code is detected and on some not\n this._codeOutlineHighlightRemovalTimeout = setTimeout(\n () => this.$codeOutlineHighlight!.style.display = 'none',\n 100,\n );\n }\n\n this._scanFrame();\n });\n }\n\n private _onDecodeError(error: Error | string): void {\n // default error handler; can be overwritten in the constructor\n if (error === QrScanner.NO_QR_CODE_FOUND) return;\n console.log(error);\n }\n\n private async _getCameraStream(): Promise<{ stream: MediaStream, facingMode: QrScanner.FacingMode }> {\n if (!navigator.mediaDevices) throw 'Camera not found.';\n\n const preferenceType = /^(environment|user)$/.test(this._preferredCamera)\n ? 'facingMode'\n : 'deviceId';\n const constraintsWithoutCamera: Array<MediaTrackConstraints> = [{\n width: { min: 1024 }\n }, {\n width: { min: 768 }\n }, {}];\n const constraintsWithCamera = constraintsWithoutCamera.map((constraint) => Object.assign({}, constraint, {\n [preferenceType]: { exact: this._preferredCamera },\n }));\n\n for (const constraints of [...constraintsWithCamera, ...constraintsWithoutCamera]) {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ video: constraints, audio: false });\n // Try to determine the facing mode from the stream, otherwise use a guess or 'environment' as\n // default. Note that the guess is not always accurate as Safari returns cameras of different facing\n // mode, even for exact facingMode constraints.\n const facingMode = this._getFacingMode(stream)\n || (constraints.facingMode\n ? this._preferredCamera as QrScanner.FacingMode // a facing mode we were able to fulfill\n : (this._preferredCamera === 'environment'\n ? 'user' // switch as _preferredCamera was environment but we are not able to fulfill it\n : 'environment' // switch from unfulfilled user facingMode or default to environment\n )\n );\n return { stream, facingMode };\n } catch (e) {}\n }\n\n throw 'Camera not found.';\n }\n\n private async _restartVideoStream(): Promise<void> {\n // Note that we always pause the stream and not only if !this._paused as even if this._paused === true, the\n // stream might still be running, as it's by default only stopped after a delay of 300ms.\n const wasPaused = this._paused;\n const paused = await this.pause(true);\n if (!paused || wasPaused || !this._active) return;\n await this.start();\n }\n\n private static _stopVideoStream(stream : MediaStream): void {\n for (const track of stream.getTracks()) {\n track.stop(); // note that this will also automatically turn the flashlight off\n stream.removeTrack(track);\n }\n }\n\n private _setVideoMirror(facingMode: QrScanner.FacingMode): void {\n // in user facing mode mirror the video to make it easier for the user to position the QR code\n const scaleFactor = facingMode === 'user'? -1 : 1;\n this.$video.style.transform = 'scaleX(' + scaleFactor + ')';\n }\n\n private _getFacingMode(videoStream: MediaStream): QrScanner.FacingMode | null {\n const videoTrack = videoStream.getVideoTracks()[0];\n if (!videoTrack) return null; // unknown\n // inspired by https://github.com/JodusNodus/react-qr-reader/blob/master/src/getDeviceId.js#L13\n return /rear|back|environment/i.test(videoTrack.label)\n ? 'environment'\n : /front|user|face/i.test(videoTrack.label)\n ? 'user'\n : null; // unknown\n }\n\n private static _drawToCanvas(\n image: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement,\n scanRegion?: QrScanner.ScanRegion | null,\n canvas?: HTMLCanvasElement | null,\n disallowCanvasResizing= false,\n ): [HTMLCanvasElement, CanvasRenderingContext2D] {\n canvas = canvas || document.createElement('canvas');\n const scanRegionX = scanRegion && scanRegion.x ? scanRegion.x : 0;\n const scanRegionY = scanRegion && scanRegion.y ? scanRegion.y : 0;\n const scanRegionWidth = scanRegion && scanRegion.width\n ? scanRegion.width\n : (image as HTMLVideoElement).videoWidth || image.width as number;\n const scanRegionHeight = scanRegion && scanRegion.height\n ? scanRegion.height\n : (image as HTMLVideoElement).videoHeight || image.height as number;\n\n if (!disallowCanvasResizing) {\n const canvasWidth = scanRegion && scanRegion.downScaledWidth\n ? scanRegion.downScaledWidth\n : scanRegionWidth;\n const canvasHeight = scanRegion && scanRegion.downScaledHeight\n ? scanRegion.downScaledHeight\n : scanRegionHeight;\n // Setting the canvas width or height clears the canvas, even if the values didn't change, therefore only\n // set them if they actually changed.\n if (canvas.width !== canvasWidth) {\n canvas.width = canvasWidth;\n }\n if (canvas.height !== canvasHeight) {\n canvas.height = canvasHeight;\n }\n }\n\n const context = canvas.getContext('2d', { alpha: false })!;\n context.imageSmoothingEnabled = false; // gives less blurry images\n context.drawImage(\n image,\n scanRegionX, scanRegionY, scanRegionWidth, scanRegionHeight,\n 0, 0, canvas.width, canvas.height,\n );\n return [canvas, context];\n }\n\n private static async _loadImage(\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement | File | Blob | URL | String,\n ): Promise<HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\n | SVGImageElement > {\n if (imageOrFileOrBlobOrUrl instanceof Image) {\n await QrScanner._awaitImageLoad(imageOrFileOrBlobOrUrl);\n return imageOrFileOrBlobOrUrl;\n } else if (imageOrFileOrBlobOrUrl instanceof HTMLVideoElement\n || imageOrFileOrBlobOrUrl instanceof HTMLCanvasElement\n || imageOrFileOrBlobOrUrl instanceof SVGImageElement\n || 'OffscreenCanvas' in window && imageOrFileOrBlobOrUrl instanceof OffscreenCanvas\n || 'ImageBitmap' in window && imageOrFileOrBlobOrUrl instanceof ImageBitmap) {\n return imageOrFileOrBlobOrUrl;\n } else if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob\n || imageOrFileOrBlobOrUrl instanceof URL || typeof imageOrFileOrBlobOrUrl === 'string') {\n const image = new Image();\n if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob) {\n image.src = URL.createObjectURL(imageOrFileOrBlobOrUrl);\n } else {\n image.src = imageOrFileOrBlobOrUrl.toString();\n }\n try {\n await QrScanner._awaitImageLoad(image);\n return image;\n } finally {\n if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob) {\n URL.revokeObjectURL(image.src);\n }\n }\n } else {\n throw 'Unsupported image type.';\n }\n }\n\n private static async _awaitImageLoad(image: HTMLImageElement): Promise<void> {\n if (image.complete && image.naturalWidth !== 0) return; // already loaded\n await new Promise<void>((resolve, reject) => {\n const listener = (event: ErrorEvent | Event) => {\n image.removeEventListener('load', listener);\n image.removeEventListener('error', listener);\n if (event instanceof ErrorEvent) {\n reject('Image load error');\n } else {\n resolve();\n }\n };\n image.addEventListener('load', listener);\n image.addEventListener('error', listener);\n });\n }\n\n private static async _postWorkerMessage(\n qrEngineOrQrEnginePromise: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector>,\n type: string,\n data?: any,\n transfer?: Transferable[],\n ): Promise<number> {\n return QrScanner._postWorkerMessageSync(await qrEngineOrQrEnginePromise, type, data, transfer);\n }\n\n // sync version of _postWorkerMessage without performance overhead of async functions\n private static _postWorkerMessageSync(\n qrEngine: Worker | BarcodeDetector,\n type: string,\n data?: any,\n transfer?: Transferable[],\n ): number {\n if (!(qrEngine instanceof Worker)) return -1;\n const id = QrScanner._workerMessageId++;\n qrEngine.postMessage({\n id,\n type,\n data,\n }, transfer);\n return id;\n }\n}\n\ndeclare namespace QrScanner {\n export interface ScanRegion {\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n downScaledWidth?: number;\n downScaledHeight?: number;\n }\n\n export type FacingMode = 'environment' | 'user';\n export type DeviceId = string;\n\n export interface Camera {\n id: DeviceId;\n label: string;\n }\n\n export type InversionMode = 'original' | 'invert' | 'both';\n\n export interface Point {\n x: number;\n y: number;\n }\n\n export interface ScanResult {\n data: string;\n // In clockwise order, starting at top left, but this might not be guaranteed in the future.\n cornerPoints: QrScanner.Point[];\n }\n}\n\n// simplified from https://wicg.github.io/shape-detection-api/#barcode-detection-api\ndeclare class BarcodeDetector {\n constructor(options?: { formats: string[] });\n static getSupportedFormats(): Promise<string[]>;\n detect(image: ImageBitmapSource): Promise<Array<{ rawValue: string, cornerPoints: QrScanner.Point[] }>>;\n}\n\nexport default QrScanner;\n"],"names":[],"mappings":"aAkIY,IAAA,QAAA,OAAA,EAAsC,UAGrC,CAAA,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}