UNPKG

instascan

Version:

Webcam-driven QR code scanner.

374 lines (309 loc) 8.83 kB
const EventEmitter = require('events'); const ZXing = require('./zxing')(); const Visibility = require('visibilityjs'); const StateMachine = require('fsm-as-promised'); class ScanProvider { constructor(emitter, analyzer, captureImage, scanPeriod, refractoryPeriod) { this.scanPeriod = scanPeriod; this.captureImage = captureImage; this.refractoryPeriod = refractoryPeriod; this._emitter = emitter; this._frameCount = 0; this._analyzer = analyzer; this._lastResult = null; this._active = false; } start() { this._active = true; requestAnimationFrame(() => this._scan()); } stop() { this._active = false; } scan() { return this._analyze(false); } _analyze(skipDups) { let analysis = this._analyzer.analyze(); if (!analysis) { return null; } let { result, canvas } = analysis; if (!result) { return null; } if (skipDups && result === this._lastResult) { return null; } clearTimeout(this.refractoryTimeout); this.refractoryTimeout = setTimeout(() => { this._lastResult = null; }, this.refractoryPeriod); let image = this.captureImage ? canvas.toDataURL('image/webp', 0.8) : null; this._lastResult = result; let payload = { content: result }; if (image) { payload.image = image; } return payload; } _scan() { if (!this._active) { return; } requestAnimationFrame(() => this._scan()); if (++this._frameCount !== this.scanPeriod) { return; } else { this._frameCount = 0; } let result = this._analyze(true); if (result) { setTimeout(() => { this._emitter.emit('scan', result.content, result.image || null); }, 0); } } } class Analyzer { constructor(video) { this.video = video; this.imageBuffer = null; this.sensorLeft = null; this.sensorTop = null; this.sensorWidth = null; this.sensorHeight = null; this.canvas = document.createElement('canvas'); this.canvas.style.display = 'none'; this.canvasContext = null; this.decodeCallback = ZXing.Runtime.addFunction(function (ptr, len, resultIndex, resultCount) { var result = new Uint8Array(ZXing.HEAPU8.buffer, ptr, len); var str = String.fromCharCode.apply(null, result); if (resultIndex === 0) { window.zxDecodeResult = ''; } window.zxDecodeResult += str; }); } analyze() { if (!this.video.videoWidth) { return null; } if (!this.imageBuffer) { let videoWidth = this.video.videoWidth; let videoHeight = this.video.videoHeight; this.sensorWidth = videoWidth; this.sensorHeight = videoHeight; this.sensorLeft = Math.floor((videoWidth / 2) - (this.sensorWidth / 2)); this.sensorTop = Math.floor((videoHeight / 2) - (this.sensorHeight / 2)); this.canvas.width = this.sensorWidth; this.canvas.height = this.sensorHeight; this.canvasContext = this.canvas.getContext('2d'); this.imageBuffer = ZXing._resize(this.sensorWidth, this.sensorHeight); return null; } this.canvasContext.drawImage( this.video, this.sensorLeft, this.sensorTop, this.sensorWidth, this.sensorHeight ); let data = this.canvasContext.getImageData(0, 0, this.sensorWidth, this.sensorHeight).data; for (var i = 0, j = 0; i < data.length; i += 4, j++) { let [r, g, b] = [data[i], data[i + 1], data[i + 2]]; ZXing.HEAPU8[this.imageBuffer + j] = Math.trunc((r + g + b) / 3); } let err = ZXing._decode_qr(this.decodeCallback); if (err) { return null; } let result = window.zxDecodeResult; if (result != null) { return { result: result, canvas: this.canvas }; } return null; } } class Scanner extends EventEmitter { constructor(opts) { super(); this.video = this._configureVideo(opts); this.mirror = (opts.mirror !== false); this.backgroundScan = opts.backgroundScan || false; this._continuous = (opts.continuous !== false); this._analyzer = new Analyzer(this.video); this._camera = null; let captureImage = opts.captureImage || false; let scanPeriod = opts.scanPeriod || 1; let refractoryPeriod = opts.refractoryPeriod || (5 * 1000); this._scanner = new ScanProvider(this, this._analyzer, captureImage, scanPeriod, refractoryPeriod); this._fsm = this._createStateMachine(); Visibility.change((e, state) => { if (state === 'visible') { setTimeout(() => { if (this._fsm.can('activate')) { this._fsm.activate(); } }, 0); } else { if (!this.backgroundScan && this._fsm.can('deactivate')) { this._fsm.deactivate(); } } }); this.addListener('active', () => { this.video.classList.remove('inactive'); this.video.classList.add('active'); }); this.addListener('inactive', () => { this.video.classList.remove('active'); this.video.classList.add('inactive'); }); this.emit('inactive'); } scan() { return this._scanner.scan(); } async start(camera = null) { if (this._fsm.can('start')) { await this._fsm.start(camera); } else { await this._fsm.stop(); await this._fsm.start(camera); } } async stop() { if (this._fsm.can('stop')) { await this._fsm.stop(); } } set captureImage(capture) { this._scanner.captureImage = capture; } get captureImage() { return this._scanner.captureImage; } set scanPeriod(period) { this._scanner.scanPeriod = period; } get scanPeriod() { return this._scanner.scanPeriod; } set refractoryPeriod(period) { this._scanner.refractoryPeriod = period; } get refractoryPeriod() { return this._scanner.refractoryPeriod; } set continuous(continuous) { this._continuous = continuous; if (continuous && this._fsm.current === 'active') { this._scanner.start(); } else { this._scanner.stop(); } } get continuous() { return this._continuous; } set mirror(mirror) { this._mirror = mirror; if (mirror) { this.video.style.MozTransform = 'scaleX(-1)'; this.video.style.webkitTransform = 'scaleX(-1)'; this.video.style.OTransform = 'scaleX(-1)'; this.video.style.msFilter = 'FlipH'; this.video.style.filter = 'FlipH'; this.video.style.transform = 'scaleX(-1)'; } else { this.video.style.MozTransform = null; this.video.style.webkitTransform = null; this.video.style.OTransform = null; this.video.style.msFilter = null; this.video.style.filter = null; this.video.style.transform = null; } } get mirror() { return this._mirror; } async _enableScan(camera) { this._camera = camera || this._camera; if (!this._camera) { throw new Error('Camera is not defined.'); } let streamUrl = await this._camera.start(); this.video.src = streamUrl; if (this._continuous) { this._scanner.start(); } } _disableScan() { this.video.src = ''; if (this._scanner) { this._scanner.stop(); } if (this._camera) { this._camera.stop(); } } _configureVideo(opts) { if (opts.video) { if (opts.video.tagName !== 'VIDEO') { throw new Error('Video must be a <video> element.'); } } var video = opts.video || document.createElement('video'); video.setAttribute('autoplay', 'autoplay'); return video; } _createStateMachine() { return StateMachine.create({ initial: 'stopped', events: [ { name: 'start', from: 'stopped', to: 'started' }, { name: 'stop', from: ['started', 'active', 'inactive'], to: 'stopped' }, { name: 'activate', from: ['started', 'inactive'], to: ['active', 'inactive'], condition: function (options) { if (Visibility.state() === 'visible' || this.backgroundScan) { return 'active'; } else { return 'inactive'; } } }, { name: 'deactivate', from: ['started', 'active'], to: 'inactive' } ], callbacks: { onenteractive: async (options) => { await this._enableScan(options.args[0]); this.emit('active'); }, onleaveactive: () => { this._disableScan(); this.emit('inactive'); }, onenteredstarted: async (options) => { await this._fsm.activate(options.args[0]); } } }); } } module.exports = Scanner;