'use strict';
(function(name, myClass) {
let instance = new myClass();
// This is only for unit test.
if (typeof module !== 'undefined') {
module['exports'] = instance;
} else {
window[name] = instance;
instance.createCanvas();
}
return instance;
}) ('yeah', function(global) {
let tracking = global.tracking || {};
const MAX_TRACKING_FAST_THRESHOLD = 100;
const MAX_SENSITIVITY = 100;
const MIN_SENSITIVITY = 0;
const DEFAULT_SENSITIVITY = 60;
const MIN_CAPTURE_INTERVAL = 250;
const DEFAULT_CAPTURE_INTERVAL = 1000;
const DELAY_TO_DECENTRALIZE_CPU_USAGE = 200;
const CAPTURE_OFFSET_TOP = 0;
const CAPTURE_OFFSET_LEFT = 0;
const CANVAS_HEIGHT_FOR_CALCULATION = 128;
const DEFAULT_DELAY_FOR_VIDEO_INIT = 1000;
const DEFAULT_MARKER_SIZE = 4;
const DEFAULT_OPACITY = 1;
const OVER_WRAPPED_OPACITY = 0.5;
const HIDDEN_OPACITY = 0;
const MIN_DATA_TO_CALC_YEAH = 2;
const MIN_SENSITIVITY_AUTO_ADJUSTMENT = 2;
const MAX_MODERATE_CORNER_COUNT = 400;
const MIN_MODERATE_CORNER_COUNT = 250;
const MAX_SENSITIVITY_ADJUST_DENOMINATOR = 500;
const MIN_SENSITIVITY_ADJUST_DENOMINATOR = 100;
const MAX_MATCH_RATE = 100;
const MAX_YEAH_SCORE = 100;
const MIN_YEAH_SCORE = 0;
const INVERSE_BASE = 1;
/**
* Get flag for video playing status
* http://www.w3schools.com/TagS/ref_av_dom.asp
*
* @private
* @method isVideoPaused
* @param {Object} videoElm Video element
* @return {Boolean} Flag for video playing status (stop: true, playing: false)
*/
function isVideoPaused(videoElm) {
return (videoElm !== undefined) ? videoElm.paused : false;
}
/**
* Promise to make delay
*
* @private
* @method delayTimer
* @param {Number} delay Delay time for resolve timer (ms)
* @return {Object} Promise object to make delay like setTimeout
*/
function delayTimer(delay) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
/**
* Promise to wrap processing
*
* @private
* @method pEmit
* @param {Function} func Function to be wrapped by Promise
* @return {Object} Promise object for some processing
*/
function pEmit(func) {
return new Promise((resolve, reject) => {
try {
let result = func();
resolve(result);
} catch (error) {
reject(error);
}
});
}
/**
* Fill marker on tracked corners and matched points
*
* @private
* @method fillTrackedPointsOnCanvas
* @param {Object} canvasContext Canvas element
* @param {Object} trackedData Tracked data points
* @param {Number} scaleInverted Inverted scale
* @param {Object} options Options of Yeah class
*/
function fillTrackedPointsOnCanvas(canvasContext, trackedData, scaleInverted, options) {
if (!options.isShowCapturePanel) {
return;
}
canvasContext.fillStyle = '#f00';
let loopCnt = trackedData.cornerList.length;
// eslint-disable-next-line
while ((loopCnt-=2)>=0) {
canvasContext.fillRect(
trackedData.cornerList[loopCnt] * scaleInverted,
// eslint-disable-next-line
trackedData.cornerList[loopCnt + 1] * scaleInverted,
options.markerSize, options.markerSize
);
}
canvasContext.fillStyle = '#0f0';
loopCnt = trackedData.matchList.length;
let match;
// eslint-disable-next-line
while ((loopCnt-=2)>=0) { // Skip drawing the half of them.
match = trackedData.matchList[loopCnt].keypoint1;
canvasContext.fillRect(
match[0] * scaleInverted, match[1] * scaleInverted,
options.markerSize, options.markerSize
);
}
}
/**
* A library to convert video to its excitement score.
*
* @class Yeah
*/
class Yeah {
/**
* constructor of yeah.js class
*
* @constructor
*/
constructor() {
this.videoElm;
this.userCanvas;
this.userCtx;
this.calcCanvas;
this.calcCtx;
this.options = {
captureInterval: DEFAULT_CAPTURE_INTERVAL,
sensitivity: DEFAULT_SENSITIVITY,
isShowCapturePanel: false,
isAutoAdjustSensitivity: true,
markerSize: DEFAULT_MARKER_SIZE
};
this.startTime;
this.scale;
this.scaleInverted;
this.customYeahCalculator;
this.lastCornerList;
this.lastDescriptorList;
this.tsDataList = new Array();
this.captureIntervalTimer;
}
/**
* Create canvas
*
* @method createCanvas
*/
createCanvas() {
if (this.userCanvas === undefined) {
this.userCanvas = document.createElement('canvas');
this.userCtx = this.userCanvas.getContext('2d');
this.calcCanvas = document.createElement('canvas');
this.calcCtx = this.calcCanvas.getContext('2d');
}
}
/**
* Set options
*
* @method SetOptions
* @param {Object} options Options to be set
*/
setOptions(options) {
if (options.captureInterval !== undefined) {
this.setCaptureInterval(options.captureInterval);
}
if (options.sensitivity !== undefined) {
this.setSensitivity(options.sensitivity);
}
if (options.isShowCapturePanel !== undefined) {
this.setIsShowCapturePanel(options.isShowCapturePanel);
}
if (options.isAutoAdjustSensitivity !== undefined) {
this.setIsAutoAdjustSensitivity(options.isAutoAdjustSensitivity);
}
if (options.markerSize !== undefined) {
this.setMarkerSize(options.markerSize);
}
}
/**
* Get options
*
* @method getOptions
* @return {Object} Options set to Yeah class
*/
getOptions() {
return this.options;
}
/**
* Set video element
*
* @method setVideoElement
* @param {Object} videoElm A video element as a capture target
*/
setVideoElement(videoElm) {
if (this.videoElm === undefined) {
this.videoElm = videoElm;
this.videoElm.parentNode.insertBefore(this.userCanvas, this.videoElm.nextSibling);
}
}
/**
* Set video src
*
* @method setVideoSrc
* @param {String} src Video source url
*/
setVideoSrc(src) {
this.videoElm.src = src;
this.clearLastCaptureInfo();
}
/**
* Get capture interval
*
* @method getCaptureInterval
* @return {Number} Capture interval
*/
getCaptureInterval() {
return this.options.captureInterval;
}
/**
* Set capture interval
*
* @method setCaptureInterval
* @param {Number} captureInterval Capture interval to be set
*/
setCaptureInterval(captureInterval) {
this.options.captureInterval = Math.max(MIN_CAPTURE_INTERVAL, captureInterval);
}
/**
* Get sensitivity
*
* @method getSensitivity
* @return {Number} Sensitivity
*/
getSensitivity() {
return this.options.sensitivity;
}
/**
* Update sensitivity
*
* @method setSensitivity
* @param {Number} sensitivity Sensitivity to be set
*/
setSensitivity(sensitivity) {
sensitivity = Math.min(MAX_SENSITIVITY, sensitivity);
sensitivity = Math.max(MIN_SENSITIVITY, sensitivity);
this.options.sensitivity = sensitivity;
}
/**
* Get flag for showing capture panel
*
* @method isShowCapturePanel
* @return {Boolean} Flag for show capture panel
*/
isShowCapturePanel() {
return this.options.isShowCapturePanel;
}
/**
* Set flag for showing capture panel
*
* @method setIsShowCapturePanel
* @param {Boolean} bool Flag to be set
*/
setIsShowCapturePanel(bool) {
this.options.isShowCapturePanel = bool;
this.videoElm.style.opacity = bool ? OVER_WRAPPED_OPACITY : DEFAULT_OPACITY;
}
/**
* Get flag for auto sensitivity adjustment
*
* @method isAutoAdjustSensitivity
* @return {Boolean} Flag for auto sensitivity adjustment
*/
isAutoAdjustSensitivity() {
return this.options.isAutoAdjustSensitivity;
}
/**
* Set flag for auto sensitivity adjustment
*
* @method setIsAutoAdjustSensitivity
* @param {Boolean} bool Flag to be set
*/
setIsAutoAdjustSensitivity(bool) {
this.options.isAutoAdjustSensitivity = bool;
}
/**
* Get marker size
*
* @method getMarkerSize
* @return {Number} Marker size
*/
getMarkerSize() {
return this.options.markerSize;
}
/**
* Set marker size
*
* @method setMarkerSize
* @param {Number} markerSize Marker size to be set
*/
setMarkerSize(markerSize) {
this.options.markerSize = markerSize;
}
/**
* play video and initialize canvas
*
* @method playVideo
* @param {String} src Video src
* @param {Number} delay Delay for copying height and width from video element
* @return {Object} Promise object
*/
playVideo(src, delay) {
this.setVideoSrc(src);
delay = delay || DEFAULT_DELAY_FOR_VIDEO_INIT;
this.videoElm.style.opacity = HIDDEN_OPACITY;
this.userCanvas.style.opacity = HIDDEN_OPACITY;
return delayTimer(delay)
.then(() => {
this.userCanvas.width = this.videoElm.clientWidth;
this.userCanvas.height = this.videoElm.clientHeight;
this.scale = CANVAS_HEIGHT_FOR_CALCULATION / this.userCanvas.height;
this.scaleInverted = INVERSE_BASE / this.scale;
this.calcCanvas.width = this.userCanvas.width * this.scale;
this.calcCanvas.height = this.userCanvas.height * this.scale;
this.videoElm.style.opacity = this.isShowCapturePanel() ? OVER_WRAPPED_OPACITY : DEFAULT_OPACITY;
this.userCanvas.style.opacity = DEFAULT_OPACITY;
});
}
/**
* Stop capturing video element
*
* @method stopCaptureVideo
*/
stopCaptureVideo() {
if (this.captureIntervalTimer) {
clearInterval(this.captureIntervalTimer);
}
}
/**
* Start capturing video element to canvas
*
* @method startCaptureVideo
* @param {Function} successCallback Callback after getting capture data
* @param {Function} failureCallback Callback for getting error for each capture loop
*/
startCaptureVideo(successCallback, failureCallback) {
this.stopCaptureVideo();
this.captureIntervalTimer = setInterval(() => {
if (!isVideoPaused(this.videoElm)) {
if (this.isShowCapturePanel()) {
this.userCtx.drawImage(this.videoElm, CAPTURE_OFFSET_LEFT, CAPTURE_OFFSET_TOP, this.videoElm.clientWidth, this.videoElm.clientHeight);
}
this.calcCtx.drawImage(this.videoElm, CAPTURE_OFFSET_LEFT, CAPTURE_OFFSET_TOP, this.calcCanvas.width, this.calcCanvas.height);
} else {
this.clearLastCaptureInfo();
}
delayTimer(DELAY_TO_DECENTRALIZE_CPU_USAGE)
.then(() => this.findFeatures())
.then((data) => pEmit(() => {
data.time = data.now - this.startTime;
data.yeah = null;
if (!isVideoPaused(this.videoElm)) {
this.setSensitivity(this.calcAdjustedSensitivity(data.cornerList.length, this.options));
data.yeah = this.calcYeah(data.matchRate, this.tsDataList);
fillTrackedPointsOnCanvas(this.userCtx, data, this.scaleInverted, this.options);
this.tsDataList.push(data);
if (this.tsDataList.length > MIN_DATA_TO_CALC_YEAH) {
this.tsDataList.shift();
}
}
data.sensitivity = this.getSensitivity();
return data;
})).then((data) => {
successCallback(data);
}).catch((error) => {
if (failureCallback !== undefined) {
failureCallback(error);
} else {
throw new Error('yeah.js: caught an error in captureIntervalTimer.');
}
});
}, this.getCaptureInterval());
}
/**
* Set custom yeah calculator
*
* @method setCustomYeahCalculator
* @param {Function} func Customized yeah calculator
*/
setCustomYeahCalculator(func) {
this.customYeahCalculator = func;
}
/**
* Calculate yeah score from time series data set
*
* @method calcYeah
* @param {Number} currentMatchRate Current match rate score
* @param {Array} tsDataList List of corner detection and yeah score history data
* @return {Number} Yeah score
*/
calcYeah(currentMatchRate, tsDataList) {
if (this.customYeahCalculator !== undefined) {
return this.customYeahCalculator(currentMatchRate, tsDataList);
}
let yeahScore;
let tsDataCnt = tsDataList.length;
if (tsDataCnt >= MIN_DATA_TO_CALC_YEAH) {
/* eslint-disable */
yeahScore = (Math.abs(currentMatchRate - tsDataList[tsDataCnt - 1].matchRate) * 2
+ Math.abs(tsDataList[tsDataCnt - 1].matchRate - tsDataList[tsDataCnt - 2].matchRate)) / 3;
/* eslint-enable */
yeahScore = Math.min(MAX_YEAH_SCORE, yeahScore);
yeahScore = Math.max(MIN_YEAH_SCORE, yeahScore);
} else {
yeahScore = null;
}
return yeahScore;
}
/**
* Clear last capture info
*
* @method clearLastCaptureInfo
*/
clearLastCaptureInfo() {
this.tsDataList = new Array();
this.lastCornerList = undefined;
this.lastDescriptorList = undefined;
}
/**
* Find features from captured image on canvas
*
* @method findFeatures
* @return {Object} Promise object to find features
*/
findFeatures() {
return pEmit(() => {
let now = (new Date()).getTime();
if (this.startTime === undefined) {
this.startTime = now;
}
let matchRate = null;
if (isVideoPaused(this.videoElm)) {
return {
now: now,
cornerList: [],
matchList: [],
matchRate: matchRate
};
}
tracking.Fast.THRESHOLD = MAX_TRACKING_FAST_THRESHOLD - this.getSensitivity();
let imageData = this.calcCtx.getImageData(
CAPTURE_OFFSET_LEFT, CAPTURE_OFFSET_TOP,
this.calcCtx.canvas.width, this.calcCtx.canvas.height
);
let gray = tracking.Image.grayscale(imageData.data, imageData.width, imageData.height);
let cornerList = tracking.Fast.findCorners(gray, imageData.width, imageData.height);
let matchList = new Array();
let descriptorList = tracking.Brief.getDescriptors(gray, imageData.width, cornerList);
if (cornerList.length && this.lastDescriptorList && this.lastCornerList) {
// tracking.Brief.reciprocalMatch is more accurate but takes higher cpu cost.
matchList = tracking.Brief.match(this.lastCornerList, this.lastDescriptorList, cornerList, descriptorList);
matchRate = Math.min(MAX_MATCH_RATE, matchList.length / cornerList.length * MAX_MATCH_RATE);
}
this.lastDescriptorList = descriptorList;
this.lastCornerList = cornerList;
return {
now: now,
cornerList: cornerList,
matchList: matchList,
matchRate: matchRate
};
});
}
/**
* Calculate adjusted sensitivity with found corner size
*
* @method calcAdjustedSensitivity
* @param {Number} cornerCnt Found corner size
* @param {Object} options Options of Yeah class
* @return {Number} Calculated sensitivity
*/
calcAdjustedSensitivity(cornerCnt, options) {
let adjustedSensitivity = options.sensitivity;
if (!options.isAutoAdjustSensitivity) {
return adjustedSensitivity;
}
if (cornerCnt > MAX_MODERATE_CORNER_COUNT && options.sensitivity > MIN_SENSITIVITY) {
adjustedSensitivity = adjustedSensitivity - Math.max(
MIN_SENSITIVITY_AUTO_ADJUSTMENT,
Math.ceil((cornerCnt - MAX_MODERATE_CORNER_COUNT) / MAX_SENSITIVITY_ADJUST_DENOMINATOR)
);
adjustedSensitivity = Math.max(MIN_SENSITIVITY, adjustedSensitivity);
} else if (cornerCnt < MIN_MODERATE_CORNER_COUNT && options.sensitivity < MAX_SENSITIVITY) {
adjustedSensitivity = adjustedSensitivity + Math.max(
MIN_SENSITIVITY_AUTO_ADJUSTMENT,
Math.ceil((MIN_MODERATE_CORNER_COUNT - cornerCnt) / MIN_SENSITIVITY_ADJUST_DENOMINATOR)
);
adjustedSensitivity = Math.min(MAX_SENSITIVITY, adjustedSensitivity);
}
return adjustedSensitivity;
}
}
return Yeah;
} (this.self || global));