From eae4904e7acdd4fd6635811d19905e744dd0ad93 Mon Sep 17 00:00:00 2001 From: Christoph Oberhofer Date: Wed, 5 Apr 2017 21:14:54 +0200 Subject: [PATCH] Extracted CameraSource; Added applyConfig to change config during runtime --- example/live_w_locator.js | 23 +++- src/input/CameraSource.js | 208 ++++++++++++++++++++++++++++++++++++ src/input/Source.js | 215 +------------------------------------- src/quagga.js | 52 ++++----- src/scanner.js | 6 ++ 5 files changed, 256 insertions(+), 248 deletions(-) create mode 100644 src/input/CameraSource.js diff --git a/example/live_w_locator.js b/example/live_w_locator.js index 8ba5580..06683ce 100644 --- a/example/live_w_locator.js +++ b/example/live_w_locator.js @@ -100,10 +100,14 @@ $(function() { this._accessByPath(this.state, path, value); console.log(JSON.stringify(this.state)); - this.detachListeners(); - this.scanner.stop(); - this.scanner.removeEventListener(); - App.init(); + + this.scanner.applyConfig({ + constraints: this.state.inputStream.constraints, + locator: this.state.locator, + decoder: this.state.decoder, + numOfWorkers: this.state.numOfWorkers, + }) + .catch(e => console.error(e)); }, inputMapper: { inputStream: { @@ -111,6 +115,17 @@ $(function() { if (/^(\d+)x(\d+)$/.test(value)) { var values = value.split('x'); return { + landscape: { + width: {ideal: parseInt(values[0])}, + height: {ideal: parseInt(values[1])}, + zoom: 1.5, + }, + portrait: { + width: {ideal: parseInt(values[0])}, + height: {ideal: parseInt(values[0])}, + zoom: 1.5, + aspectRatio: 1, + }, width: {ideal: parseInt(values[0])}, height: {ideal: parseInt(values[1])} }; diff --git a/src/input/CameraSource.js b/src/input/CameraSource.js new file mode 100644 index 0000000..c5d7ff8 --- /dev/null +++ b/src/input/CameraSource.js @@ -0,0 +1,208 @@ +import {clone} from 'lodash'; +import {determineOrientation, PORTRAIT} from '../common/device'; +import CameraAccess from './camera_access'; +import {getViewport} from '../common/utils'; + +const ConstraintPresets = [ + { + width: 720, + height: 1280, + }, { + width: 540, + height: 960, + }, { + width: 600, + height: 800, + }, { + width: 480, + height: 640, + }, { + width: 1280, + height: 720, + }, { + width: 960, + height: 540, + }, { + width: 800, + height: 600, + }, { + width: 640, + height: 480, + }, { + width: 1280, + height: 1280, + }, { + width: 1080, + height: 1080, + }, { + width: 960, + height: 960, + }, { + width: 800, + height: 800, + }, { + width: 640, + height: 640, + }, +].map((preset) => Object.assign({}, preset, {aspectRatio: preset.width / preset.height})); + +function getFilter(aspectRatio) { + if (aspectRatio === 1) { + return pre => pre.aspectRatio === aspectRatio; + } else if (aspectRatio > 1) { + return pre => pre.aspectRatio > 1; + } + return pre => pre.aspectRatio < 1; +} + +function resolveMinWidthToAdvanced({aspectRatio, minPixels}) { + return [...ConstraintPresets] + .filter(getFilter(aspectRatio)) + .map((pre) => { + return { + error: Math.abs((pre.width * pre.height) - minPixels), + pre, + }; + }) + .sort(({error: errorA}, {error: errorB}) => { + if (errorB > errorA) { + return -1; + } + if (errorB < errorA) { + return 1; + } + return 0; + }) + .map(({pre}) => pre); +} + +function getOrCreateVideo(target) { + const $viewport = getViewport(target); + if ($viewport) { + let $video = $viewport.querySelector("video"); + if (!$video) { + $video = document.createElement("video"); + $viewport.appendChild($video); + } + return $video; + } + return document.createElement("video"); +} + +function constraintToNumber(constraint) { + if (!constraint) { + return null; + } + if (typeof constraint === 'number') { + return constraint; + } + const {ideal, exact, min, max} = constraint; + if (typeof exact !== 'undefined') { + return exact; + } + if (typeof ideal !== 'undefined') { + return ideal; + } + if (typeof min !== 'undefined') { + return min; + } + if (typeof max !== 'undefined') { + return max; + } + + return null; +} + +function adjustWithZoom(videoConstraints) { + const constraints = clone(videoConstraints); + const orientation = determineOrientation(); + + let zoom = constraintToNumber(constraints.zoom) || 1, + width = constraintToNumber(constraints.width), + height = constraintToNumber(constraints.height), + aspectRatio = constraintToNumber(constraints.aspectRatio) || (width / height); + + if (constraints[orientation]) { + zoom = constraintToNumber(constraints[orientation].zoom) || zoom; + width = constraintToNumber(constraints[orientation].width) || width; + height = constraintToNumber(constraints[orientation].height) || height; + aspectRatio = constraintToNumber(constraints[orientation].aspectRatio) || (width / height); + } + + if (zoom > 1) { + width = Math.floor(width * zoom); + height = Math.floor(height * zoom); + } + + delete constraints.zoom; + delete constraints.orientation; + delete constraints.landscape; + delete constraints.portrait; + + const advanced = resolveMinWidthToAdvanced({minPixels: (width * height), aspectRatio}); + return { + zoom, + video: Object.assign({}, constraints, { + width: {ideal: advanced[0].width}, + height: {ideal: advanced[0].height}, + aspectRatio: {exact: advanced[0].aspectRatio || aspectRatio}, + advanced, + }), + }; +} + +export function fromCamera(constraints, {target} = {}) { + let {video: videoConstraints, zoom} = adjustWithZoom(constraints); + + const video = getOrCreateVideo(target); + return CameraAccess.request(video, videoConstraints) + .then((mediastream) => { + let track = mediastream.getVideoTracks()[0]; + return { + type: "CAMERA", + getDimensions() { + const viewport = { + x: 0, + y: 0, + width: video.videoWidth, + height: video.videoHeight, + }; + if (zoom > 1) { + viewport.width = Math.floor(video.videoWidth / zoom); + viewport.height = Math.floor(video.videoHeight / zoom); + viewport.x = Math.floor((video.videoWidth - viewport.width) / 2); + viewport.y = Math.floor((video.videoHeight - viewport.height) / 2); + } + + return { + viewport, + canvas: { + width: viewport.width, // AR + height: viewport.height, // AR + }, + }; + }, + getConstraints: function() { + return videoConstraints; + }, + getDrawable: function() { + return video; + }, + applyConstraints: function(newConstraints) { + track.stop(); + constraints = newConstraints; + const adjustment = adjustWithZoom(constraints); + videoConstraints = adjustment.video; + zoom = adjustment.zoom; + return CameraAccess.request(video, videoConstraints) + .then((stream) => { + mediastream = stream; + track = mediastream.getVideoTracks()[0]; + }); + }, + getLabel: function() { + return track.label; + } + }; + }); +} diff --git a/src/input/Source.js b/src/input/Source.js index c65ff33..7efb38e 100644 --- a/src/input/Source.js +++ b/src/input/Source.js @@ -1,218 +1,5 @@ -import {clone} from 'lodash'; -import {determineOrientation, PORTRAIT, LANDSCAPE, SQUARE} from '../common/device'; -import CameraAccess from './camera_access'; -import {getViewport} from '../common/utils'; - export * from './ImageSource'; - -const ConstraintPresets = [ - { - width: 720, - height: 1280, - }, { - width: 540, - height: 960, - }, { - width: 600, - height: 800, - }, { - width: 480, - height: 640, - }, { - width: 1280, - height: 720, - }, { - width: 960, - height: 540, - }, { - width: 800, - height: 600, - }, { - width: 640, - height: 480, - }, { - width: 1280, - height: 1280, - }, { - width: 1080, - height: 1080, - }, { - width: 960, - height: 960, - }, { - width: 800, - height: 800, - }, { - width: 640, - height: 640, - }, -].map((preset) => Object.assign({}, preset, {aspectRatio: preset.width / preset.height})); - -function getFilter(aspectRatio) { - if (aspectRatio === 1) { - return pre => pre.aspectRatio === aspectRatio; - } else if (aspectRatio > 1) { - return pre => pre.aspectRatio > 1; - } - return pre => pre.aspectRatio < 1; -} - -function resolveMinWidthToAdvanced({aspectRatio, minPixels}) { - return [...ConstraintPresets] - .filter(getFilter(aspectRatio)) - .map((pre) => { - return { - error: Math.abs((pre.width * pre.height) - minPixels), - pre, - }; - }) - .sort(({error: errorA}, {error: errorB}) => { - if (errorB > errorA) { - return -1; - } - if (errorB < errorA) { - return 1; - } - return 0; - }) - .map(({pre}) => pre); -} - -function getOrCreateVideo(target) { - const $viewport = getViewport(target); - if ($viewport) { - let $video = $viewport.querySelector("video"); - if (!$video) { - $video = document.createElement("video"); - $viewport.appendChild($video); - } - return $video; - } - return document.createElement("video"); -} - -function constraintToNumber(constraint) { - if (!constraint) { - return null; - } - if (typeof constraint === 'number') { - return constraint; - } - const {ideal, exact, min, max} = constraint; - if (typeof exact !== 'undefined') { - return exact; - } - if (typeof ideal !== 'undefined') { - return ideal; - } - if (typeof min !== 'undefined') { - return min; - } - if (typeof max !== 'undefined') { - return max; - } - - return null; -} - -function adjustWithZoom(videoConstraints) { - const constraints = clone(videoConstraints); - const orientation = determineOrientation(); - - let zoom = constraintToNumber(constraints.zoom) || 1, - width = constraintToNumber(constraints.width), - height = constraintToNumber(constraints.height), - aspectRatio = constraintToNumber(constraints.aspectRatio) || (width / height); - - if (constraints[orientation]) { - zoom = constraintToNumber(constraints[orientation].zoom) || zoom; - width = constraintToNumber(constraints[orientation].width) || width; - height = constraintToNumber(constraints[orientation].height) || height; - aspectRatio = constraintToNumber(constraints[orientation].aspectRatio) || (width / height); - } - - if (zoom > 1) { - width = Math.floor(width * zoom); - height = Math.floor(height * zoom); - } - - delete constraints.zoom; - delete constraints.orientation; - delete constraints.landscape; - delete constraints.portrait; - - const advanced = resolveMinWidthToAdvanced({minPixels: (width * height), aspectRatio}); - return { - zoom, - video: Object.assign({}, constraints, { - width: {ideal: advanced[0].width}, - height: {ideal: advanced[0].height}, - aspectRatio: {exact: advanced[0].aspectRatio || aspectRatio}, - advanced, - }), - }; -} - -export function fromCamera(constraints, {target} = {}) { - var {video: videoConstraints, zoom} = adjustWithZoom(constraints); - - const video = getOrCreateVideo(target); - return CameraAccess.request(video, videoConstraints) - .then(function(mediastream) { - const track = mediastream.getVideoTracks()[0]; - return { - type: "CAMERA", - getDimensions() { - const viewport = { - x: 0, - y: 0, - width: video.videoWidth, - height: video.videoHeight, - }; - if (zoom > 1) { - viewport.width = Math.floor(video.videoWidth / zoom); - viewport.height = Math.floor(video.videoHeight / zoom); - viewport.x = Math.floor((video.videoWidth - viewport.width) / 2); - viewport.y = Math.floor((video.videoHeight - viewport.height) / 2); - } - - return { - viewport, - canvas: { - width: viewport.width, // AR - height: viewport.height, // AR - }, - }; - }, - getConstraints: function() { - return videoConstraints; - }, - getDrawable: function() { - return video; - }, - applyConstraints: function(constraints) { - track.stop(); - videoConstraints = Object.assign({}, constraints); - if (determineOrientation() === PORTRAIT) { - constraints = Object.assign({}, constraints, { - width: constraints.height, - height: constraints.width, - }); - } - console.log(videoConstraints, constraints); - if (constraints.zoom && constraints.zoom.exact > 1) { - constraints.width.ideal = Math.floor(constraints.width.ideal * constraints.zoom.exact); - constraints.height.ideal = Math.floor(constraints.height.ideal * constraints.zoom.exact); - delete constraints.zoom; - } - return CameraAccess.request(video, videoConstraints); - }, - getLabel: function() { - return track.label; - } - }; - }); -} +export * from './CameraSource'; export function fromCanvas(input) { var $canvas = null; diff --git a/src/quagga.js b/src/quagga.js index 86cc705..cd8e889 100644 --- a/src/quagga.js +++ b/src/quagga.js @@ -4,12 +4,19 @@ import ImageWrapper from './common/image_wrapper'; import ImageDebug from './common/image_debug'; import ResultCollector from './analytics/result_collector'; import Config from './config/config'; -import {merge} from 'lodash'; +import {merge, isEqual} from 'lodash'; import CameraAccess from './input/camera_access'; import * as PixelCapture from './input/PixelCapture'; import * as Source from './input/Source'; import {PORTRAIT, LANDSCAPE, SQUARE} from './common/device'; +function hasConfigChanged(currentConfig, newConfig, prop) { + if (!prop) { + return !isEqual(currentConfig, newConfig); + } + return newConfig[prop] && !isEqual(currentConfig[prop], newConfig[prop]); +} + function fromConfig(pixelCapturer, config) { const scanner = createScanner(pixelCapturer); const source = pixelCapturer.getSource(); @@ -98,16 +105,21 @@ function fromConfig(pixelCapturer, config) { return pixelCapturer.getCanvas(); }, applyConfig(newConfig) { - // during runtime? - // running scanners must know that! - // apply constraints to source, only if changed - - const normalizedConfig = merge({}, Config, newConfig); - this.stop(); + const normalizedConfig = merge({}, Config, currentConfig, newConfig); + let promise = Promise.resolve(); + if (hasConfigChanged(currentConfig, normalizedConfig, "constraints")) { + console.log("constraints changed!", currentConfig.constraints, normalizedConfig.constraints); + promise = promise.then(() => { + scanner.pause(); + return source.applyConstraints(normalizedConfig.constraints); + }); + } + if (hasConfigChanged(currentConfig, normalizedConfig)) { + console.log("config changed!"); + promise = promise.then(() => scanner.applyConfig(normalizedConfig)); + } currentConfig = normalizedConfig; - - return source.applyConstraints(currentConfig.constraints) - .then(() => this.start()); + return promise; }, getSource() { return pixelCapturer.getSource(); @@ -120,11 +132,6 @@ function fromSource(config, source) { return fromConfig(pixelCapturer, config); } -function setConfig(configuration = {}, key, config = {}) { - var mergedConfig = merge({}, configuration, {[key]: config}); - return createApi(mergedConfig); -} - function createApi() { return { fromImage(image, options) { @@ -142,21 +149,6 @@ function createApi() { fromSource(src, inputConfig) { return fromSource(configuration, src, inputConfig); }, - fromConfig(conf) { - return fromConfig(merge({}, configuration, conf)); - }, - decoder(conf) { - return setConfig(configuration, "decoder", conf); - }, - locator(conf) { - return setConfig(configuration, "locator", conf); - }, - throttle(timeInMs) { - return setConfig(configuration, "frequency", 1000 / parseInt(timeInMs)); - }, - config(conf) { - return createApi(merge({}, configuration, conf)); - }, CameraAccess, ImageWrapper, ImageDebug, diff --git a/src/scanner.js b/src/scanner.js index dd1ca06..fac474f 100644 --- a/src/scanner.js +++ b/src/scanner.js @@ -442,6 +442,12 @@ function createScanner(pixelCapturer) { CameraAccess.release(); } }, + applyConfig(newConfig) { + _stopped = true; + adjustWorkerPool(0); + _config = merge({}, Config, _config, newConfig); + return setup(_config).then(start); + }, pause: function() { _stopped = true; },