diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 739f14a7..fe2111c7 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Real-time webcam-driven HTML5 QR code scanner. [Try the live demo](https://schmi *Note:* Chrome requires HTTPS when using the WebRTC API. Any pages using this library should be served over HTTPS. +*Note:* Some browsers (like Edge) require [WebRTC Adapter shim](https://github.com/webrtc/adapter). + ### NPM `npm install --save instascan` diff --git a/RELEASE.md b/RELEASE.md old mode 100644 new mode 100755 diff --git a/assets/qr.png b/assets/qr.png old mode 100644 new mode 100755 diff --git a/assets/setup.jpg b/assets/setup.jpg old mode 100644 new mode 100755 diff --git a/deploy/deploy.sh b/deploy/deploy.sh old mode 100644 new mode 100755 diff --git a/deploy/id_rsa.enc b/deploy/id_rsa.enc old mode 100644 new mode 100755 diff --git a/deploy/id_rsa.pub b/deploy/id_rsa.pub old mode 100644 new mode 100755 diff --git a/docs/app.js b/docs/app.js old mode 100644 new mode 100755 diff --git a/docs/favicon.png b/docs/favicon.png old mode 100644 new mode 100755 diff --git a/docs/index.html b/docs/index.html old mode 100644 new mode 100755 index b26e0dac..0a3dcd5d --- a/docs/index.html +++ b/docs/index.html @@ -3,7 +3,7 @@ Instascan – Demo - + diff --git a/docs/style.css b/docs/style.css old mode 100644 new mode 100755 diff --git a/export.js b/export.js old mode 100644 new mode 100755 index bfcb9cb9..23e43c59 --- a/export.js +++ b/export.js @@ -1 +1 @@ -window.Instascan = require('./index'); +window.Instascan = require('./src/index'); diff --git a/gulpfile.js b/gulpfile.js old mode 100644 new mode 100755 index 8a62cd55..854062f2 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,6 +5,29 @@ var source = require('vinyl-source-stream'); var buffer = require('vinyl-buffer'); var uglify = require('gulp-uglify'); var babelify = require('babelify'); +var babel = require('gulp-babel'); +var transform = require('gulp-transform'); + +var babelOptions = { + ignore: /zxing\.js$/i, + presets: ['env'], + plugins: ['transform-runtime'] +}; + +var build = function (file) { + return browserify(file, { noParse: [ require.resolve('./src/vendor/zxing') ] }) + .transform(babelify, babelOptions) + .bundle() + .pipe(source('instascan.js')); +} + +var mockImportsInZXing = function (content, file) { + if (/zxing\.js$/i.test(file.relative)) { + return content.replace(/require\([^)]+\)/g, '{}'); + } else { + return content; + } +}; gulp.task('default', ['build', 'watch']); @@ -13,28 +36,22 @@ gulp.task('watch', function () { gulp.watch('./*.js', ['build']); }); -function build(file) { - return browserify(file, { - noParse: [require.resolve('./src/zxing')] - }) - .transform(babelify, { - ignore: /zxing\.js$/i, - presets: ['es2015'], - plugins: ['syntax-async-functions', 'transform-regenerator'] - }) - .bundle() - .pipe(source('instascan.js')); -} +gulp.task('build-package', function () { + return gulp.src('./src/**/*.js') + .pipe(transform('utf-8', mockImportsInZXing)) + .pipe(babel(babelOptions)) + .pipe(gulp.dest('./lib/')); +}); -gulp.task('release', function () { +gulp.task('build', ['build-package'], function () { return build('./export.js') - .pipe(buffer()) - .pipe(uglify()) - .pipe(rename({ suffix: '.min' })) .pipe(gulp.dest('./dist/')); }); -gulp.task('build', function () { +gulp.task('release', ['build-package'], function () { return build('./export.js') + .pipe(buffer()) + .pipe(uglify()) + .pipe(rename({ suffix: '.min' })) .pipe(gulp.dest('./dist/')); }); diff --git a/index.js b/index.js old mode 100644 new mode 100755 diff --git a/lib/camera.js b/lib/camera.js new file mode 100755 index 00000000..5d7bc943 --- /dev/null +++ b/lib/camera.js @@ -0,0 +1,273 @@ +'use strict'; + +var _getIterator2 = require('babel-runtime/core-js/get-iterator'); + +var _getIterator3 = _interopRequireDefault(_getIterator2); + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); + +var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); + +var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); + +var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); + +var _inherits2 = require('babel-runtime/helpers/inherits'); + +var _inherits3 = _interopRequireDefault(_inherits2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function cameraName(label) { + var clean = label.replace(/\s*\([0-9a-f]+(:[0-9a-f]+)?\)\s*$/, ''); + return clean || label || null; +} + +var MediaError = function (_Error) { + (0, _inherits3.default)(MediaError, _Error); + + function MediaError(type) { + (0, _classCallCheck3.default)(this, MediaError); + + var _this = (0, _possibleConstructorReturn3.default)(this, (MediaError.__proto__ || (0, _getPrototypeOf2.default)(MediaError)).call(this, 'Cannot access video stream (' + type + ').')); + + _this.type = type; + return _this; + } + + return MediaError; +}(Error); + +var Camera = function () { + function Camera(id, name) { + (0, _classCallCheck3.default)(this, Camera); + + this.id = id; + this.name = name; + this._stream = null; + } + + (0, _createClass3.default)(Camera, [{ + key: 'start', + value: function () { + var _ref = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee2() { + var _this2 = this; + + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.next = 2; + return Camera._wrapErrors((0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + return _context.abrupt('return', navigator.mediaDevices.getUserMedia({ + audio: false, + video: { + deviceId: { + exact: _this2.id + }, + facingMode: "environment" + } + })); + + case 1: + case 'end': + return _context.stop(); + } + } + }, _callee, _this2); + }))); + + case 2: + this._stream = _context2.sent; + return _context2.abrupt('return', this._stream); + + case 4: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function start() { + return _ref.apply(this, arguments); + } + + return start; + }() + }, { + key: 'stop', + value: function stop() { + if (!this._stream) { + return; + } + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = (0, _getIterator3.default)(this._stream.getVideoTracks()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var stream = _step.value; + + stream.stop(); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + this._stream = null; + } + }], [{ + key: 'getCameras', + value: function () { + var _ref3 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee3() { + var devices; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + _context3.next = 2; + return this._ensureAccess(); + + case 2: + _context3.next = 4; + return navigator.mediaDevices.enumerateDevices(); + + case 4: + devices = _context3.sent; + return _context3.abrupt('return', devices.filter(function (d) { + return d.kind === 'videoinput'; + }).map(function (d) { + return new Camera(d.deviceId, cameraName(d.label)); + })); + + case 6: + case 'end': + return _context3.stop(); + } + } + }, _callee3, this); + })); + + function getCameras() { + return _ref3.apply(this, arguments); + } + + return getCameras; + }() + }, { + key: '_ensureAccess', + value: function () { + var _ref4 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee5() { + var _this3 = this; + + return _regenerator2.default.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + return _context5.abrupt('return', this._wrapErrors((0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee4() { + return _regenerator2.default.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + _context4.next = 2; + return navigator.mediaDevices.getUserMedia({ video: true }); + + case 2: + case 'end': + return _context4.stop(); + } + } + }, _callee4, _this3); + })))); + + case 1: + case 'end': + return _context5.stop(); + } + } + }, _callee5, this); + })); + + function _ensureAccess() { + return _ref4.apply(this, arguments); + } + + return _ensureAccess; + }() + }, { + key: '_wrapErrors', + value: function () { + var _ref6 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee6(fn) { + return _regenerator2.default.wrap(function _callee6$(_context6) { + while (1) { + switch (_context6.prev = _context6.next) { + case 0: + _context6.prev = 0; + return _context6.abrupt('return', fn()); + + case 4: + _context6.prev = 4; + _context6.t0 = _context6['catch'](0); + + if (!_context6.t0.name) { + _context6.next = 10; + break; + } + + throw new MediaError(_context6.t0.name); + + case 10: + throw _context6.t0; + + case 11: + case 'end': + return _context6.stop(); + } + } + }, _callee6, this, [[0, 4]]); + })); + + function _wrapErrors(_x) { + return _ref6.apply(this, arguments); + } + + return _wrapErrors; + }() + }]); + return Camera; +}(); + +module.exports = Camera; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100755 index 00000000..ad8950b3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,8 @@ +'use strict'; + +var Instascan = { + Scanner: require('./scanner'), + Camera: require('./camera') +}; + +module.exports = Instascan; \ No newline at end of file diff --git a/lib/scanner.js b/lib/scanner.js new file mode 100755 index 00000000..f1937936 --- /dev/null +++ b/lib/scanner.js @@ -0,0 +1,578 @@ +'use strict'; + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); + +var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); + +var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); + +var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); + +var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); + +var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); + +var _inherits2 = require('babel-runtime/helpers/inherits'); + +var _inherits3 = _interopRequireDefault(_inherits2); + +var _trunc = require('babel-runtime/core-js/math/trunc'); + +var _trunc2 = _interopRequireDefault(_trunc); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var EventEmitter = require('events'); +var ZXing = require('./vendor/zxing')(); +var Visibility = require('visibilityjs'); +var StateMachine = require('fsm-as-promised'); + +var ScanProvider = function () { + function ScanProvider(emitter, analyzer, captureImage, scanPeriod, refractoryPeriod) { + (0, _classCallCheck3.default)(this, ScanProvider); + + this.scanPeriod = scanPeriod; + this.captureImage = captureImage; + this.refractoryPeriod = refractoryPeriod; + this._emitter = emitter; + this._frameCount = 0; + this._analyzer = analyzer; + this._lastResult = null; + this._active = false; + } + + (0, _createClass3.default)(ScanProvider, [{ + key: 'start', + value: function start() { + var _this = this; + + this._active = true; + requestAnimationFrame(function () { + return _this._scan(); + }); + } + }, { + key: 'stop', + value: function stop() { + this._active = false; + } + }, { + key: 'scan', + value: function scan() { + return this._analyze(false); + } + }, { + key: '_analyze', + value: function _analyze(skipDups) { + var _this2 = this; + + var analysis = this._analyzer.analyze(); + if (!analysis) { + return null; + } + + var result = analysis.result, + canvas = analysis.canvas; + + if (!result) { + return null; + } + + if (skipDups && result === this._lastResult) { + return null; + } + + clearTimeout(this.refractoryTimeout); + this.refractoryTimeout = setTimeout(function () { + _this2._lastResult = null; + }, this.refractoryPeriod); + + var image = this.captureImage ? canvas.toDataURL('image/webp', 0.8) : null; + + this._lastResult = result; + + var payload = { content: result }; + if (image) { + payload.image = image; + } + + return payload; + } + }, { + key: '_scan', + value: function _scan() { + var _this3 = this; + + if (!this._active) { + return; + } + + requestAnimationFrame(function () { + return _this3._scan(); + }); + + if (++this._frameCount !== this.scanPeriod) { + return; + } else { + this._frameCount = 0; + } + + var result = this._analyze(true); + if (result) { + setTimeout(function () { + _this3._emitter.emit('scan', result.content, result.image || null); + }, 0); + } + } + }]); + return ScanProvider; +}(); + +var Analyzer = function () { + function Analyzer(video) { + (0, _classCallCheck3.default)(this, Analyzer); + + 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; + }); + } + + (0, _createClass3.default)(Analyzer, [{ + key: 'analyze', + value: function analyze() { + if (!this.video.videoWidth) { + return null; + } + + if (!this.imageBuffer) { + var videoWidth = this.video.videoWidth; + var 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); + + var data = this.canvasContext.getImageData(0, 0, this.sensorWidth, this.sensorHeight).data; + for (var i = 0, j = 0; i < data.length; i += 4, j++) { + var _ref = [data[i], data[i + 1], data[i + 2]], + r = _ref[0], + g = _ref[1], + b = _ref[2]; + + ZXing.HEAPU8[this.imageBuffer + j] = (0, _trunc2.default)((r + g + b) / 3); + } + + var err = ZXing._decode_qr(this.decodeCallback); + if (err) { + return null; + } + + var result = window.zxDecodeResult; + if (result != null) { + return { result: result, canvas: this.canvas }; + } + + return null; + } + }]); + return Analyzer; +}(); + +var Scanner = function (_EventEmitter) { + (0, _inherits3.default)(Scanner, _EventEmitter); + + function Scanner(opts) { + (0, _classCallCheck3.default)(this, Scanner); + + var _this4 = (0, _possibleConstructorReturn3.default)(this, (Scanner.__proto__ || (0, _getPrototypeOf2.default)(Scanner)).call(this)); + + _this4.video = _this4._configureVideo(opts); + _this4.mirror = opts.mirror !== false; + _this4.backgroundScan = opts.backgroundScan !== false; + _this4._continuous = opts.continuous !== false; + _this4._analyzer = new Analyzer(_this4.video); + _this4._camera = null; + + var captureImage = opts.captureImage || false; + var scanPeriod = opts.scanPeriod || 1; + var refractoryPeriod = opts.refractoryPeriod || 5 * 1000; + + _this4._scanner = new ScanProvider(_this4, _this4._analyzer, captureImage, scanPeriod, refractoryPeriod); + _this4._fsm = _this4._createStateMachine(); + + Visibility.change(function (e, state) { + if (state === 'visible') { + setTimeout(function () { + if (_this4._fsm.can('activate')) { + _this4._fsm.activate(); + } + }, 0); + } else { + if (!_this4.backgroundScan && _this4._fsm.can('deactivate')) { + _this4._fsm.deactivate(); + } + } + }); + + _this4.addListener('active', function () { + _this4.video.classList.remove('inactive'); + _this4.video.classList.add('active'); + }); + + _this4.addListener('inactive', function () { + _this4.video.classList.remove('active'); + _this4.video.classList.add('inactive'); + }); + + _this4.emit('inactive'); + return _this4; + } + + (0, _createClass3.default)(Scanner, [{ + key: 'scan', + value: function scan() { + return this._scanner.scan(); + } + }, { + key: 'start', + value: function () { + var _ref2 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee() { + var camera = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + if (!this._fsm.can('start')) { + _context.next = 5; + break; + } + + _context.next = 3; + return this._fsm.start(camera); + + case 3: + _context.next = 9; + break; + + case 5: + _context.next = 7; + return this._fsm.stop(); + + case 7: + _context.next = 9; + return this._fsm.start(camera); + + case 9: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + function start() { + return _ref2.apply(this, arguments); + } + + return start; + }() + }, { + key: 'stop', + value: function () { + var _ref3 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee2() { + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + if (!this._fsm.can('stop')) { + _context2.next = 3; + break; + } + + _context2.next = 3; + return this._fsm.stop(); + + case 3: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function stop() { + return _ref3.apply(this, arguments); + } + + return stop; + }() + }, { + key: '_enableScan', + value: function () { + var _ref4 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(camera) { + var stream; + return _regenerator2.default.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + this._camera = camera || this._camera; + + if (this._camera) { + _context3.next = 3; + break; + } + + throw new Error('Camera is not defined.'); + + case 3: + _context3.next = 5; + return this._camera.start(); + + case 5: + stream = _context3.sent; + + this.video.srcObject = stream; + + if (this._continuous) { + this._scanner.start(); + } + + case 8: + case 'end': + return _context3.stop(); + } + } + }, _callee3, this); + })); + + function _enableScan(_x2) { + return _ref4.apply(this, arguments); + } + + return _enableScan; + }() + }, { + key: '_disableScan', + value: function _disableScan() { + this.video.src = ''; + + if (this._scanner) { + this._scanner.stop(); + } + + if (this._camera) { + this._camera.stop(); + } + } + }, { + key: '_configureVideo', + value: function _configureVideo(opts) { + if (opts.video) { + if (opts.video.tagName !== 'VIDEO') { + throw new Error('Video must be a