UNPKG

@theopenweb/js-sensor

Version:

JavaScript sensor library for native input sensors.

856 lines (742 loc) 24.6 kB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ /** * @typedef {import('../types/index').SensorListener} SensorListener * @typedef {import('../types/index').WatchOptions} WatchOptions */ /** * Collection of browser based sensor events. * Sensors should be continuous events. * Therefor each should be setup in similar way using an event listener. * Objects of events, arguments and implementations may differ. */ class BrowserSensorWatcher { constructor() { /** * All sensor handles must return promise and take no arguments. * start: required. args: (optional options object) should resolve an array with arguments to be passed to stop. * stop: required. args: (returnedData from start) * check: optional. */ this.sensorHandles = { /** * Watch GPS position * data: https://developer.mozilla.org/en-US/docs/Web/API/Position * * @see https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition * @return {SensorListener} */ watchPosition: { /** * @param {WatchOptions} options */ start: (options) => { let eventListener = options.events.data; return new Promise((resolve, reject) => { let id = navigator.geolocation.watchPosition( eventListener, (err) => { return reject(console.error('Failed watchPosition', err)); } ); resolve([id]); }); }, /** * @param {number} id */ stop: (id) => { navigator.geolocation.clearWatch(id); return Promise.resolve(); } }, /** * Gets camera and audio and records * data: * 1. stream: returns stream once(FASTEST) * 2. image: returns each image frame({video, canvas, context, stream}) * 3. details: returns {video, canvas, context, stream} * * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia * @return {SensorListener} */ getUserMedia: { /** * @param {WatchOptions} options */ start: (options) => { return navigator.mediaDevices.getUserMedia({ audio: true, video: true }) .then((stream) => { const mode = options.mode || 'stream';//String for existing handle OR function for custom. /** * @param {MediaStream} stream */ const _streamToVideo = (stream) => { let url = window.URL.createObjectURL(stream); let video = document.createElement('video'); video.autoplay = true; video.src = url; return video; }; /** * Returns stream as data once. * For simple handling. * @param {MediaStream} stream * @param {Object} options */ const streamGrabber = (stream, options) => { let eventListener = options.events.data; eventListener(stream); } /** * @param {MediaStream} stream * @param {Object} options */ const detailsGrabber = (stream, options) => { let eventListener = options.events.data; let video = _streamToVideo(stream); const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); const state = { canvas: canvas, context: ctx, video: video, stream: stream }; eventListener(state); } /** * Returns each frame * @param {MediaStream} stream * @param {Object} options */ const createImageGrabber = (stream, options) => { let eventListener = options.events.data; let video = _streamToVideo(stream); //Initialization const canvas = document.createElement('canvas'); const ctx = /** @type {CanvasRenderingContext2D} */ (notFalsy(canvas.getContext('2d'))); const state = { canvas: canvas, context: ctx, video: video, stream: stream }; const onFrame = () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0); eventListener(state); }; video.addEventListener('frame', onFrame); return { stop: () => { video.removeEventListener('frame', onFrame); }, state: state }; }; const handleMap = { stream: streamGrabber, details: detailsGrabber, image: createImageGrabber }; const handle = typeof mode === 'function' ? mode : handleMap[mode]; const commonObject = { handler: handle() }; return Promise.resolve([stream, commonObject]); }); }, /** * @param {MediaStream} stream * @param {{ handler: { stop?: () => void } }} commonObject */ stop: (stream, commonObject) => { // Stop object if (commonObject.handler && commonObject.handler.stop) { commonObject.handler.stop(); } // Stop stream let tracks = stream.getTracks(); tracks.forEach((track) => { track.stop(); }); return Promise.resolve(); } }, /** * Device orientation(3d coordinates) * @see https://developer.mozilla.org/en-US/docs/Web/Events/deviceorientation * @return {SensorListener} */ deviceOrientation: this._getWindowEventListenerObject('deviceorientation'), /** * Device light(lux) * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceLightEvent * @return {SensorListener} */ deviceLight: this._getWindowEventListenerObject('devicelight'), /** * Device proximity(cm) * @see https://developer.mozilla.org/en-US/docs/Web/Events/deviceproximity * @return {SensorListener} */ deviceProximity: this._getWindowEventListenerObject('deviceproximity'), /** * Device motion(acceleration + rotation. Can use for measuring path taken.) * @see https://developer.mozilla.org/en-US/docs/Web/Events/devicemotion * @return {SensorListener} */ deviceMotion: this._getWindowEventListenerObject('devicemotion'), /** * Test use only. * For testing when sensors might not be available. * Not implemented in watchAll. * @return {SensorListener} */ test: this._getTestEventListenerObject()//Useful for both automated testing and learning on personal computer. }; } /** * Common object for handling sensors * start: Starts watching sensor * stop: Stops watching sensor * check: Checks for support * @return {SensorListener} */ SensorListener() { return { start: () => { }, stop: () => { }, check: () => { return true; } }; } /** * Common SensorListener for window events such as devicemotion. * * @param {String} eventName * @return {SensorListener} */ _getWindowEventListenerObject(eventName) { return { /** * @param {WatchOptions} options */ start: function (options) { let eventListener = options.events.data; return new Promise((resolve, reject) => { window.addEventListener(eventName, eventListener); resolve([eventName, eventListener]); }); }, /** * @param {string} eventName * @param {function} eventListener */ stop: function (eventName, eventListener) { return new Promise((resolve, reject) => { window.removeEventListener(eventName, eventListener); resolve(); }); }, check: () => { return true; } }; } /** * Gets test SensorListener that can be used without access to sensors. * Used for testing on displaying when sensors not available. * * @return {SensorListener} */ _getTestEventListenerObject() { return { /** * @param {WatchOptions} options */ start: function (options) { let eventListener = options.events.data; return new Promise((resolve, reject) => { let id = window.setInterval(() => { eventListener('test data'); }, 500); resolve([id]); }); }, /** * @param {number} id clearTimeout id */ stop: function (id) { return new Promise((resolve, reject) => { window.clearTimeout(id); resolve(); }); }, check: function () { return true; } } } } module.exports = BrowserSensorWatcher; /** * @param {*} value TODO: Pass type through */ function notFalsy(value) { if (!value) { throw new Error(`Expected ${value} to not be falsy`) } return value } },{}],2:[function(require,module,exports){ const SensorHandler = require('./sensor-handler'); /** * Wrapper for main class * Add additional methods here not to do with sensors if required. */ class JsSensor extends SensorHandler { constructor() { super(); } } if(typeof window !== 'undefined'){ window.JsSensor = JsSensor; } if(typeof module !== 'undefined'){ module.exports = JsSensor; } },{"./sensor-handler":3}],3:[function(require,module,exports){ const BrowserSensorWatcher = require('./browser-sensor-watcher'); /** * @type {BrowserSensorWatcher} */ let rawSensorWatcher; /** * @typedef {import('../types/index').SensorState} SensorState * @typedef {import('../types/index').SensorEvents} SensorEvents * @typedef {import('../types/index').WatchOptions} WatchOptions * @typedef {import('../types/index').SensorStateSettings} SensorStateSettings */ /** * Main class for handling sensors. * This class allows get, watch, etc. of sensors. */ class SensorHandler { constructor(env = 'browser') { // Override with updateSensorListeners if (env === 'browser') { rawSensorWatcher = this.rawSensorWatcher = new BrowserSensorWatcher(); } /** * SensorState mapped by sensor name * @type {Object<string, SensorState>} */ this.sensors = { // }; /** * Sensor counts stored accross start and stop watching to make sure async starting and stopping doesn't cause bugs. * @type {Object<string, number>} */ this.sensorCreationCounts = { // }; } /** * Get sensor once. Should stop watching sensor if newly made. * * @param {string} sensorName * @return {Promise<SensorState>} */ get(sensorName) { let self = this; return new Promise((resolve) => { let sensorCreationCount = self._getSensorCreationCount() + 1; /** * @param {SensorState} sensorState */ let handle = (sensorState) => { //resolve(sensorState); //Only stop if no new watches happened. if (!(self._getSensorCreationCount() > sensorCreationCount)) { self.stop(sensorName) .then(() => { resolve(sensorState); }); } else { resolve(sensorState); } }; self.watch(sensorName, { events: { data: handle } }); }); } /** * Watch sensor from RawSensorWatcher. Ignores duplicates. * options: { * events: { * data: function(data){} * } * } * * @param {string} sensorName * @param {Partial<WatchOptions>} options optional options * @return {Promise<SensorState>} Returns: promise resolving SensorState */ watch(sensorName, options = {}) { const self = this; // data required(use console log if nothing) if (!options.events) { options.events = {}; } if (!options.events.data) { options.events.data = this._getDefaultDataEvent(sensorName); } return new Promise((resolve, reject) => { /** * @param {SensorState} sensorState */ const onResolve = (sensorState) => { self._incSensorCreationCount(sensorName); resolve(sensorState); }; if (self.sensors[sensorName]) { return onResolve(self.sensors[sensorName]); } if (rawSensorWatcher.sensorHandles[sensorName]) { rawSensorWatcher.sensorHandles[sensorName].start(options) .then(function (startReturnData) { let sensorState = self._setSensorState(sensorName, { options: options, startReturnData: startReturnData, stop: rawSensorWatcher.sensorHandles[sensorName].stop // Set to state because may implement multiple watching later. }); return onResolve(sensorState); }) .catch(reject); } else { return reject(new Error('No raw sensor: ' + sensorName)); } }); } /** * Watches all available sensors. * * @return {Promise<SensorState[]>} Resolves array of SensorStates. However allows for failing returning null. */ watchAll() { let possibleSensors = rawSensorWatcher.sensorHandles; let promises = []; for (let key in possibleSensors) { if (key === 'test') { continue; } let p = new Promise((resolve) => { this.watch(key) .then(resolve) .catch(() => { resolve(null); }); }); promises.push(p); } return Promise.all(promises); } /** * Stops watching sensor. * * @param {string} sensorName * @return {Promise<SensorState>} */ stop(sensorName) { let self = this; return new Promise((resolve, reject) => { let sensor = self.sensors[sensorName]; if (!sensor) { reject(new Error('No sensor: ' + sensorName)); } if (sensor.stop) { sensor.stop(...sensor.startReturnData) .then(() => { delete self.sensors[sensorName]; console.log('sensor stop success: ' + sensorName); resolve(sensor); }) .catch(reject); } else { reject(new Error('No sensor stop handle: ' + sensorName)); } }); } /** * Stops watching all. */ stopAll() { /** * @type {Promise<SensorState>[]} */ const promises = []; for (let key in this.sensors) { promises.push(this.stop(key)); } return Promise.all(promises); } /** * Adds sensor event. Multiple at same time ok. * * @param {string} sensorName * @param {string} eventName * @param {function} handle * @return {boolean} success/failure */ addSensorEvent(sensorName, eventName, handle) { //Require started let sensor = this.sensors[sensorName]; if (!sensor) { return false; } let events = sensor.events; if (!events[eventName]) { events[eventName] = []; } events[eventName].push(handle); return true; } /** * Removes sensor event. Only removes handle specified. * * @param {string} sensorName * @param {string} eventName * @param {function} handle * @return {boolean} success/failure */ removeSensorEvent(sensorName, eventName, handle) { //No sensor or event let sensor = this.sensors[sensorName]; if (!sensor || !sensor.events[eventName]) { return false; } let event = sensor.events[eventName]; let index = event.indexOf(handle); if (index >= 0) { event.splice(index, 1); return true; } else { return false; } } /** * Update registered sensor listeners. * Pass handle with one argument. * MUST return. * Dynamic updating during watching may result in bugs. * * @param {Function} callback handle taking object map of registered SensorListeners */ updateSensorListeners(callback) { this.rawSensorWatcher = callback(this.rawSensorWatcher); } /** * Gets usable sensor names * * Returns: sensor names */ getMappedSensorNames() { return Object.keys(this.rawSensorWatcher.sensorHandles); } /** * Current state of sensor * @return {SensorState} */ _SensorState() { return { isSensorState: true, name: '', stop: null,//Copied over from RawSensorWatcher object for better management. startReturnData: null, options: {}, events: { data: null } }; } /** * Sets sensor state. Requires settings.events. * * @param {string} sensorName * @param {SensorStateSettings} settings */ _setSensorState(sensorName, settings = {}) { console.log('_setSensorState: ', sensorName, settings); /** * @type {SensorState|undefined} */ let sensorState; if (this.sensors[sensorName]) { sensorState = this.sensors[sensorName]; } else { sensorState = this._SensorState(); sensorState.name = sensorName; this.sensors[sensorName] = sensorState; for (let key in settings) { sensorState[key] = settings[key]; } //Events let options = settings.options; if (options.events) { for (let key in options.events) { this.addSensorEvent(sensorName, key, options.events[key]); } } console.log('added sensor state: ', sensorState); } return sensorState; } /** * Gets number of times sensor created * * @param {string} sensorName * @return {number} sensor creation count */ _getSensorCreationCount(sensorName = '') { return (!!this.sensorCreationCounts[sensorName]) ? this.sensorCreationCounts[sensorName] : 0; } /** * Increment sensor creation count * * @param {string} sensorName * @return {number} sensor creation count */ _incSensorCreationCount(sensorName) { if (!Number.isInteger(this.sensorCreationCounts[sensorName])) { this.sensorCreationCounts[sensorName] = 0; } return ++this.sensorCreationCounts[sensorName]; } /** * Data event used when no event to handle sensor data is provided. * Stringifies data and logs. * * @param {string} sensorName */ _getDefaultDataEvent(sensorName) { /** * @param {any} data */ return (data) => { if (data && typeof data === 'object') { // let oldData = data; /** * @type {any[]} */ let cache = []; data = JSON.stringify(data, function (key, val) { if (val && typeof val === 'object') { //Ignore duplicates if (cache.indexOf(val) >= 0) { return; } cache.push(val); } return val; }); cache = []; // deref } console.log('sensor: ' + sensorName, String(new Date()), data); } } } module.exports = SensorHandler; },{"./browser-sensor-watcher":1}],4:[function(require,module,exports){ const BrowserSensorWatcher = require('../class/browser-sensor-watcher'); const browserSensorWatcher = new BrowserSensorWatcher(); describe('BrowserSensorWatcher', ()=>{ it('_getWindowEventListenerObject', ()=>{ let object = browserSensorWatcher._getWindowEventListenerObject('devicemotion'); chai.expect(object).to.be.an('object'); chai.expect(object).to.have.property('start'); chai.expect(object).to.have.property('stop'); chai.expect(object).to.have.property('check'); }); }); },{"../class/browser-sensor-watcher":1}],5:[function(require,module,exports){ const JsSensor = require('../class/js-sensor'); const jsSensor = new JsSensor(); describe('JsSensor', ()=>{ it('construction', ()=>{ chai.expect(jsSensor).to.be.an('object'); }); }); },{"../class/js-sensor":2}],6:[function(require,module,exports){ const SensorHandler = require('../class/sensor-handler'); const sensorHandler = new SensorHandler(); console.log(sensorHandler); /** * Tests require fully capable device + manual movement to complete. */ describe('SensorHandler', function(){ beforeEach(()=>{ return sensorHandler.stopAll(); }); this.timeout(5000);//Devices may take a while. it('getMappedSensorNames', ()=>{ let names = sensorHandler.getMappedSensorNames(); chai.expect(names).to.be.an('array'); }); it('get', ()=>{ return sensorHandler.get('test') .then((data)=>{ chai.expect(data).to.equal('test data'); return Promise.resolve(); }); }); it('watch', ()=>{ return sensorHandler.watch('test') .then((sensorState)=>{ chai.expect(sensorState).to.be.an('object'); chai.expect(sensorState.isSensorState).to.equal(true); return Promise.resolve(); }); }); it('watchAll', ()=>{ return sensorHandler.watchAll() .then((promises)=>{ chai.expect(promises).to.be.an('array'); return Promise.resolve(); }); }); it('stop', ()=>{ return sensorHandler.watch('test') .then(()=>{return sensorHandler.stop('test');}) .then(()=>{ return Promise.resolve(); }); }); it('stopAll', ()=>{ return sensorHandler.stopAll() .then((promises)=>{ chai.expect(promises).to.be.an('array'); return Promise.resolve(); }); }); it('addSensorEvent', ()=>{ return sensorHandler.watch('test') .then(()=>{ let handle = function(){}; sensorHandler.addSensorEvent('test', 'data', handle); chai.expect(sensorHandler.sensors['test'].events['data'].indexOf(handle)).to.be.greaterThan(-1); return Promise.resolve(); }) .then(()=>{ return sensorHandler.stop('test'); }); }); it('removeSensorEvent', ()=>{ return sensorHandler.watch('test') .then(()=>{ let handle = function(){}; sensorHandler.addSensorEvent('test', 'data', handle); chai.expect(sensorHandler.sensors['test'].events['data'].indexOf(handle)).to.be.greaterThan(-1); sensorHandler.removeSensorEvent('test', 'data', handle); chai.expect(sensorHandler.sensors['test'].events['data'].indexOf(handle)).to.equal(-1); }) .then(()=>{ return sensorHandler.stop('test'); }); }); it('_SensorState', ()=>{ chai.expect(sensorHandler._SensorState()).to.be.an('object'); }); }); },{"../class/sensor-handler":3}]},{},[5,4,6]);