<commit_before>(function (enyo, scope) {
	/**
	 * The onChange event fires when the user changes the value of the toggle button, but not when the value is
	 * changed programmatically.
	 *
	 * @event mochi.ToggleButton#onChange
	 * @type {Object}
	 * @property {Boolean} value - The on/off value of the toggle button.
	 * @public
	 */

	/**
	 * `mochi.ToggleButton` extends {@link enyo.Control} and is a control that looks like a switch with labels for two
	 * states. Each time a `mochi.ToggleButton` is tapped, it switches its value and fires an onChange event.
	 *
	 *	 ```
	 *	 {kind: 'mochi.ToggleButton', onChange: 'buttonToggle'}
	 *
	 *	 buttonToggle: function (inSender, inEvent) {
 *				this.log('Toggled to value ' + inEvent.value);
 *			}
	 * 	```
	 *
	 * To find out the value of the button, use getValue:
	 *
	 *	```
	 *	 queryToggleValue: function () {
 *				return this.$.toggleButton.getValue();
 *			}
	 *	```
	 *
	 * The color of the toggle button can be customized by applying a background color:
	 *
	 * 	```
	 * 	{kind: 'mochi.ToggleButton', style: 'background-color: #35A8EE;'}
	 * 	```
	 *
	 * @class mochi.ToggleButton
	 * @extends enyo.Control
	 * @ui
	 * @public
	 */

	enyo.kind(
		/** @lends mochi.ToggleButton.prototype */ {

		/**
		 * @private
		 */
		name: 'mochi.ToggleButton',

		/**
		 * @private
		 */
		kind: 'enyo.Control',

		/**
		 * @private
		 */
		classes: 'mochi-toggle-button',

		/**
		 * @private
		 */
		published: {
			/**
			 * Boolean indicating whether toggle button is currently the active item in a group
			 *
			 * @type {Boolean}
			 * @default false
			 * @public
			 */
			active: false,

			/**
			 * Boolean indicating whether toggle button is currently in the 'on' state
			 *
			 * @type {Boolean}
			 * @default false
			 * @public
			 */
			value: false,

			/**
			 * Boolean indicating whether toggle button is disabled and does not generate tap events
			 *
			 * @type {Boolean}
			 * @default false
			 * @public
			 */
			disabled: false,

			/**
			 * Boolean indicating whether toggle transitions should be animated
			 *
			 * @type {Boolean}
			 * @default false
			 * @public
			 */
			canAnimate: true,

			/**
			 * CSS background-color property indicating active state
			 *
			 * @type {String}
			 * @default '#ffb80d'
			 * @public
			 */
			colorActive: '#ffb80d',

			/**
			 * CSS background-color property indicating inactive state
			 *
			 * @type {String}
			 * @default '#646464'
			 * @public
			 */
			colorInactive: '#646464',

			/**
			 * Label for toggle button's "on" state
			 *
			 * @type {String}
			 * @default 'On'
			 * @public
			 */
			onContent: 'On',
			/**
			 * Label for toggle button's "off" state
			 *
			 * @type {String}
			 * @default 'Off'
			 * @public
			 */
			offContent: 'Off'
		},

		/**
		 * @private
		 */
		events: {
			onChange: ''
		},

		/**
		 * @private
		 */
		handlers: {
			ondragstart: 'dragstart',
			ondrag: 'drag',
			ondragfinish: 'dragfinish'
		},

		/**
		 * @private
		 */
		lastKnobPos: 0,

		/**
		 * @private
		 */
		onXPos: 0,

		/**
		 * @private
		 */
		_canAnimate: false,

		/**
		 * @private
		 */
		components: [
			{name: 'content', style:'width:100%; position:absolute; float:left;'},
			{name: 'toggleKnob', classes: 'mochi-toggle-button-knob'},
			{kind: 'enyo.Animator', onStep: 'animatorStep', onEnd: 'animatorEnd'}
		],

		/**
		 * @private
		 */
		create: function () {
			this.inherited(arguments);
			this.value = Boolean(this.value || this.active);
			this.disabledChanged();
			this.supressAnimation();
		},

		/**
		 * @private
		 */
		rendered: function () {
			this.inherited(arguments);
			this.calcKnob();
			this.valueChanged();
			this.init();
		},

		/**
		 * @private
		 */
		supressAnimation: function () {
			this._canAnimate = this.canAnimate;
			this.canAnimate = false;
		},

		/**
		 * @private
		 */
		init: function () {
			this.setCanAnimate(this._canAnimate);
		},

		/*
		// We can add a class here to animate the background also
		canAnimateChanged: function () {
			if (this.canAnimate) {
				var toggleClass = 'mochi-toggle-animate';
			} else {
				var toggleClass = 'mochi-no-animate';
			}
			this.addClass(toggleClass);
		},
		*/

		/**
		 * @private
		 */
		animatorStep: function (inSender) {
			this.updateKnobPosition(inSender.value);
		},

		/**
		 * @private
		 */
		updateKnobPosition: function(inValue) {
			var xPos = inValue + 'px';
			var inControl = this.$.toggleKnob;
			if (enyo.dom.canTransform()) {
				enyo.dom.transform(inControl, {translateX: xPos});
			} else {
				inControl.applyStyle('left', xPos);
			}
		},

		/**
		 * @private
		 */
		calcKnob: function () {
			this.onXPos = (this.getBounds().width - this.$.toggleKnob.getBounds().width) - (parseInt(enyo.dom.getComputedStyleValue(this.$.toggleKnob.hasNode(), 'margin-left')) * 2);
		},

		/**
		 * @private
		 */
		valueChanged: function () {
			this.applyStyle('background-color', this.value ? this.colorActive : this.colorInactive);
			this.setActive(this.value);
			this.$.content.setContent(this.value?this.onContent:this.offContent);
			this.$.content.applyStyle('float', this.value?'left':'right');
			this.doChange({value: this.value});

			var xPos = (this.value) ? this.onXPos : 0;

			if (this.canAnimate) {
				this.$.animator.play({
					startValue: this.lastKnobPos,
					endValue: xPos,
					node: this.$.toggleKnob.hasNode()
				});
			} else {
				this.updateKnobPosition(xPos);
			}
			this.lastKnobPos = xPos;

		},

		/**
		 * @private
		 */
		activeChanged: function () {
			this.setValue(this.active);
			this.bubble('onActivate');
		},

		/**
		 * @private
		 */
		disabledChanged: function () {
			this.addRemoveClass('disabled', this.disabled);
		},

		/**
		 * @private
		 */
		updateValue: function (inValue) {
			if (!this.disabled) {
				this.setValue(inValue);
			}
		},

		/**
		 * @private
		 */
		tap: function () {
			this.updateValue(!this.value);
		},

		/**
		 * @private
		 */
		dragstart: function (inSender, inEvent) {
			if (inEvent.horizontal) {
				inEvent.preventDefault();
				this.dragging = true;
				this.dragged = false;
				return true;
			}
		},

		/**
		 * @private
		 */
		drag: function (inSender, inEvent) {
			if (this.dragging) {
				var d = inEvent.dx;
				if (Math.abs(d) > 10) {
					this.updateValue(d > 0);
					this.dragged = true;
				}
				return true;
			}
		},

		/**
		 * @private
		 */
		dragfinish: function (inSender, inEvent) {
			this.dragging = false;
			if (this.dragged) {
				inEvent.preventTap();
			}
		},

		/**
		 * @private
		 */
		resizeHandler: function () {
			this.inherited(arguments);
			this.calcKnob();
		}
	})

})(enyo, this);<commit_msg>Fix ToggleButton so displays the knob in the correct position. Also, knob should be in left position for off position and right position for on position.<commit_after>(function (enyo, scope) {
    /**
     * The onChange event fires when the user changes the value of the toggle button, but not when the value is
     * changed programmatically.
     *
     * @event mochi.ToggleButton#onChange
     * @type {Object}
     * @property {Boolean} value - The on/off value of the toggle button.
     * @public
     */

    /**
     * `mochi.ToggleButton` extends {@link enyo.Control} and is a control that looks like a switch with labels for two
     * states. Each time a `mochi.ToggleButton` is tapped, it switches its value and fires an onChange event.
     *
     *     ```
     *     {kind: 'mochi.ToggleButton', onChange: 'buttonToggle'}
     *
     *     buttonToggle: function (inSender, inEvent) {
 *				this.log('Toggled to value ' + inEvent.value);
 *			}
     *    ```
     *
     * To find out the value of the button, use getValue:
     *
     *    ```
     *     queryToggleValue: function () {
 *				return this.$.toggleButton.getValue();
 *			}
     *    ```
     *
     * The color of the toggle button can be customized by applying a background color:
     *
     *    ```
     *    {kind: 'mochi.ToggleButton', style: 'background-color: #35A8EE;'}
     *    ```
     *
     * @class mochi.ToggleButton
     * @extends enyo.Control
     * @ui
     * @public
     */

    enyo.kind(
        /** @lends mochi.ToggleButton.prototype */ {

            /**
             * @private
             */
            name: 'mochi.ToggleButton',

            /**
             * @private
             */
            kind: 'enyo.Control',

            /**
             * @private
             */
            classes: 'mochi-toggle-button',

            /**
             * @private
             */
            published: {
                /**
                 * Boolean indicating whether toggle button is currently the active item in a group
                 *
                 * @type {Boolean}
                 * @default false
                 * @public
                 */
                active: false,

                /**
                 * Boolean indicating whether toggle button is currently in the 'on' state
                 *
                 * @type {Boolean}
                 * @default false
                 * @public
                 */
                value: false,

                /**
                 * Boolean indicating whether toggle button is disabled and does not generate tap events
                 *
                 * @type {Boolean}
                 * @default false
                 * @public
                 */
                disabled: false,

                /**
                 * Boolean indicating whether toggle transitions should be animated
                 *
                 * @type {Boolean}
                 * @default false
                 * @public
                 */
                canAnimate: true,

                /**
                 * CSS background-color property indicating active state
                 *
                 * @type {String}
                 * @default '#ffb80d'
                 * @public
                 */
                colorActive: '#ffb80d',

                /**
                 * CSS background-color property indicating inactive state
                 *
                 * @type {String}
                 * @default '#646464'
                 * @public
                 */
                colorInactive: '#646464',

                /**
                 * Label for toggle button's "on" state
                 *
                 * @type {String}
                 * @default 'On'
                 * @public
                 */
                onContent: 'On',
                /**
                 * Label for toggle button's "off" state
                 *
                 * @type {String}
                 * @default 'Off'
                 * @public
                 */
                offContent: 'Off'
            },

            /**
             * @private
             */
            events: {
                onChange: ''
            },

            /**
             * @private
             */
            handlers: {
                ondragstart: 'dragstart',
                ondrag: 'drag',
                ondragfinish: 'dragfinish'
            },

            /**
             * @private
             */
            lastKnobPos: 0,

            /**
             * @private
             */
            onXPos: 0,

            /**
             * @private
             */
            _canAnimate: false,

            /**
             * @private
             */
            components: [
                {name: 'content', style: 'width:100%; position:absolute;'},
                {name: 'toggleKnob', classes: 'mochi-toggle-button-knob', style: 'float:left;'},
                {kind: 'enyo.Animator', onStep: 'animatorStep', onEnd: 'animatorEnd'}
            ],

            /**
             * @private
             */
            create: function () {
                this.inherited(arguments);
                this.value = Boolean(this.value || this.active);
                this.disabledChanged();
                this.supressAnimation();
            },

            /**
             * @private
             */
            rendered: function () {
                this.inherited(arguments);
                this.calcKnob();
                this.valueChanged();
                this.init();
            },

            /**
             * @private
             */
            supressAnimation: function () {
                this._canAnimate = this.canAnimate;
                this.canAnimate = false;
            },

            /**
             * @private
             */
            init: function () {
                this.setCanAnimate(this._canAnimate);
            },

            /*
             // We can add a class here to animate the background also
             canAnimateChanged: function () {
             if (this.canAnimate) {
             var toggleClass = 'mochi-toggle-animate';
             } else {
             var toggleClass = 'mochi-no-animate';
             }
             this.addClass(toggleClass);
             },
             */

            /**
             * @private
             */
            animatorStep: function (inSender) {
                this.updateKnobPosition(inSender.value);
            },

            /**
             * @private
             */
            updateKnobPosition: function (inValue) {
                var xPos = inValue + 'px';
                var inControl = this.$.toggleKnob;
                if (enyo.dom.canTransform()) {
                    enyo.dom.transform(inControl, {translateX: xPos});
                } else {
                    inControl.applyStyle('left', xPos);
                }
            },

            /**
             * @private
             */
            calcKnob: function () {
                this.onXPos = (this.getBounds().width - this.$.toggleKnob.getBounds().width) - (parseInt(enyo.dom.getComputedStyleValue(this.$.toggleKnob.hasNode(), 'margin-left')) * 2);
            },

            /**
             * @private
             */
            valueChanged: function () {
                this.applyStyle('background-color', this.value ? this.colorActive : this.colorInactive);
                this.setActive(this.value);
                this.$.content.setContent(this.value ? this.onContent : this.offContent);
                this.$.toggleKnob.applyStyle('float', !this.value ? 'left' : 'right');
                this.doChange({value: this.value});

                var xPos = (this.value) ? this.onXPos : 0;

                if (this.canAnimate) {
                    this.$.animator.play({
                        startValue: this.lastKnobPos,
                        endValue: xPos,
                        node: this.$.toggleKnob.hasNode()
                    });
                } else {
                    this.updateKnobPosition(xPos);
                }
                this.lastKnobPos = xPos;

            },

            /**
             * @private
             */
            activeChanged: function () {
                this.setValue(this.active);
                this.bubble('onActivate');
            },

            /**
             * @private
             */
            disabledChanged: function () {
                this.addRemoveClass('disabled', this.disabled);
            },

            /**
             * @private
             */
            updateValue: function (inValue) {
                if (!this.disabled) {
                    this.setValue(inValue);
                }
            },

            /**
             * @private
             */
            tap: function () {
                this.updateValue(!this.value);
            },

            /**
             * @private
             */
            dragstart: function (inSender, inEvent) {
                if (inEvent.horizontal) {
                    inEvent.preventDefault();
                    this.dragging = true;
                    this.dragged = false;
                    return true;
                }
            },

            /**
             * @private
             */
            drag: function (inSender, inEvent) {
                if (this.dragging) {
                    var d = inEvent.dx;
                    if (Math.abs(d) > 10) {
                        this.updateValue(d > 0);
                        this.dragged = true;
                    }
                    return true;
                }
            },

            /**
             * @private
             */
            dragfinish: function (inSender, inEvent) {
                this.dragging = false;
                if (this.dragged) {
                    inEvent.preventTap();
                }
            },

            /**
             * @private
             */
            resizeHandler: function () {
                this.inherited(arguments);
                this.calcKnob();
            }
        })

})(enyo, this);<|endoftext|>


<commit_before>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
var EventEmitter = require("events").EventEmitter;
var async = require("async");
var chokidar = require("chokidar");
var fs = require("graceful-fs");
var path = require("path");

var watcherManager = require("./watcherManager");

var FS_ACCURENCY = 10000;

function withoutCase(str) {
	return str.toLowerCase();
}

function Watcher(directoryWatcher, filePath, startTime) {
	EventEmitter.call(this);
	this.directoryWatcher = directoryWatcher;
	this.path = filePath;
	this.startTime = startTime && +startTime;
	this.data = 0;
}

Watcher.prototype = Object.create(EventEmitter.prototype);
Watcher.prototype.constructor = Watcher;

Watcher.prototype.checkStartTime = function checkStartTime(mtime, initial) {
	if(typeof this.startTime !== "number") return !initial;
	var startTime = this.startTime;
	return startTime <= mtime;
};

Watcher.prototype.close = function close() {
	this.emit("closed");
};

function DirectoryWatcher(directoryPath, options) {
	EventEmitter.call(this);
	this.options = options;
	this.path = directoryPath;
	this.files = {};
	this.directories = {};
	this.watcher = chokidar.watch(directoryPath, {
		ignoreInitial: true,
		persistent: true,
		followSymlinks: false,
		depth: 0,
		atomic: false,
		alwaysStat: true,
		ignorePermissionErrors: true,
		ignored: options.ignored,
		usePolling: options.poll ? true : undefined,
		interval: typeof options.poll === "number" ? options.poll : undefined
	});
	this.watcher.on("add", this.onFileAdded.bind(this));
	this.watcher.on("addDir", this.onDirectoryAdded.bind(this));
	this.watcher.on("change", this.onChange.bind(this));
	this.watcher.on("unlink", this.onFileUnlinked.bind(this));
	this.watcher.on("unlinkDir", this.onDirectoryUnlinked.bind(this));
	this.watcher.on("error", this.onWatcherError.bind(this));
	this.initialScan = true;
	this.nestedWatching = false;
	this.initialScanRemoved = [];
	this.doInitialScan();
	this.watchers = {};
	this.refs = 0;
}
module.exports = DirectoryWatcher;

DirectoryWatcher.prototype = Object.create(EventEmitter.prototype);
DirectoryWatcher.prototype.constructor = DirectoryWatcher;

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
	var now = Date.now();
	var old = this.files[filePath];

	this.files[filePath] = [initial ? Math.min(now, mtime) : now, mtime];

	// we add the fs accurency to reach the maximum possible mtime
	if (mtime) mtime = mtime + FS_ACCURENCY;

	if(!old) {
		if(mtime) {
			if(this.watchers[withoutCase(filePath)]) {
				this.watchers[withoutCase(filePath)].forEach(function(w) {
					if(!initial || w.checkStartTime(mtime, initial)) {
						w.emit("change", mtime);
					}
				});
			}
		}
	} else if(!initial && mtime && type !== "add") {
		if(this.watchers[withoutCase(filePath)]) {
			this.watchers[withoutCase(filePath)].forEach(function(w) {
				w.emit("change", mtime);
			});
		}
	} else if(!initial && !mtime) {
		delete this.files[filePath];
		if(this.watchers[withoutCase(filePath)]) {
			this.watchers[withoutCase(filePath)].forEach(function(w) {
				w.emit("remove");
			});
		}
	}
	if(this.watchers[withoutCase(this.path)]) {
		this.watchers[withoutCase(this.path)].forEach(function(w) {
			if(!initial || w.checkStartTime(mtime, initial)) {
				w.emit("change", filePath, mtime);
			}
		});
	}
};

DirectoryWatcher.prototype.setDirectory = function setDirectory(directoryPath, exist, initial) {
	var old = this.directories[directoryPath];
	if(!old) {
		if(exist) {
			if(this.nestedWatching) {
				this.createNestedWatcher(directoryPath);
			} else {
				this.directories[directoryPath] = true;
			}
		}
	} else {
		if(!exist) {
			if(this.nestedWatching)
				this.directories[directoryPath].close();
			delete this.directories[directoryPath];
			if(!initial && this.watchers[withoutCase(this.path)]) {
				this.watchers[withoutCase(this.path)].forEach(function(w) {
					w.emit("change", directoryPath, w.data);
				});
			}
		}
	}
};

DirectoryWatcher.prototype.createNestedWatcher = function(directoryPath) {
	this.directories[directoryPath] = watcherManager.watchDirectory(directoryPath, this.options, 1);
	this.directories[directoryPath].on("change", function(filePath, mtime) {
		if(this.watchers[withoutCase(this.path)]) {
			this.watchers[withoutCase(this.path)].forEach(function(w) {
				if(w.checkStartTime(mtime, false)) {
					w.emit("change", filePath, mtime);
				}
			});
		}
	}.bind(this));
};

DirectoryWatcher.prototype.setNestedWatching = function(flag) {
	if(this.nestedWatching !== !!flag) {
		this.nestedWatching = !!flag;
		if(this.nestedWatching) {
			Object.keys(this.directories).forEach(function(directory) {
				this.createNestedWatcher(directory);
			}, this);
		} else {
			Object.keys(this.directories).forEach(function(directory) {
				this.directories[directory].close();
				this.directories[directory] = true;
			}, this);
		}
	}
};

DirectoryWatcher.prototype.watch = function watch(filePath, startTime) {
	this.watchers[withoutCase(filePath)] = this.watchers[withoutCase(filePath)] || [];
	this.refs++;
	var watcher = new Watcher(this, filePath, startTime);
	watcher.on("closed", function() {
		var idx = this.watchers[withoutCase(filePath)].indexOf(watcher);
		this.watchers[withoutCase(filePath)].splice(idx, 1);
		if(this.watchers[withoutCase(filePath)].length === 0) {
			delete this.watchers[withoutCase(filePath)];
			if(this.path === filePath)
				this.setNestedWatching(false);
		}
		if(--this.refs <= 0)
			this.close();
	}.bind(this));
	this.watchers[withoutCase(filePath)].push(watcher);
	var data;
	if(filePath === this.path) {
		this.setNestedWatching(true);
		data = false;
		Object.keys(this.files).forEach(function(file) {
			var d = this.files[file];
			if(!data)
				data = d;
			else
				data = [Math.max(data[0], d[0]), Math.max(data[1], d[1])];
		}, this);
	} else {
		data = this.files[filePath];
	}
	process.nextTick(function() {
		if(data) {
			var ts = data[0] === data[1] ? data[0] + FS_ACCURENCY : data[0];
			if(ts > startTime)
				watcher.emit("change", data[1] + FS_ACCURENCY);
		} else if(this.initialScan && this.initialScanRemoved.indexOf(filePath) >= 0) {
			watcher.emit("remove");
		}
	}.bind(this));
	return watcher;
};

DirectoryWatcher.prototype.onFileAdded = function onFileAdded(filePath, stat) {
	if(filePath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;

	this.setFileTime(filePath, +stat.mtime, false, "add");
};

DirectoryWatcher.prototype.onDirectoryAdded = function onDirectoryAdded(directoryPath /*, stat */) {
	if(directoryPath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(directoryPath.substr(this.path.length + 1))) return;
	this.setDirectory(directoryPath, true, false);
};

DirectoryWatcher.prototype.onChange = function onChange(filePath, stat) {
	if(filePath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;
	var mtime = +stat.mtime;
	if(FS_ACCURENCY > 1 && mtime % 1 !== 0)
		FS_ACCURENCY = 1;
	else if(FS_ACCURENCY > 10 && mtime % 10 !== 0)
		FS_ACCURENCY = 10;
	else if(FS_ACCURENCY > 100 && mtime % 100 !== 0)
		FS_ACCURENCY = 100;
	else if(FS_ACCURENCY > 1000 && mtime % 1000 !== 0)
		FS_ACCURENCY = 1000;
	else if(FS_ACCURENCY > 2000 && mtime % 2000 !== 0)
		FS_ACCURENCY = 2000;
	this.setFileTime(filePath, mtime, false, "change");
};

DirectoryWatcher.prototype.onFileUnlinked = function onFileUnlinked(filePath) {
	if(filePath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;
	this.setFileTime(filePath, null, false, "unlink");
	if(this.initialScan) {
		this.initialScanRemoved.push(filePath);
	}
};

DirectoryWatcher.prototype.onDirectoryUnlinked = function onDirectoryUnlinked(directoryPath) {
	if(directoryPath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(directoryPath.substr(this.path.length + 1))) return;
	this.setDirectory(directoryPath, false, false);
	if(this.initialScan) {
		this.initialScanRemoved.push(directoryPath);
	}
};

DirectoryWatcher.prototype.onWatcherError = function onWatcherError(/* err */) {
};

DirectoryWatcher.prototype.doInitialScan = function doInitialScan() {
	fs.readdir(this.path, function(err, items) {
		if(err) {
			this.initialScan = false;
			return;
		}
		async.forEach(items, function(item, callback) {
			var itemPath = path.join(this.path, item);
			fs.stat(itemPath, function(err2, stat) {
				if(!this.initialScan) return;
				if(err2) {
					callback();
					return;
				}
				if(stat.isFile()) {
					if(!this.files[itemPath])
						this.setFileTime(itemPath, +stat.mtime, true);
				} else if(stat.isDirectory()) {
					if(!this.directories[itemPath])
						this.setDirectory(itemPath, true, true);
				}
				callback();
			}.bind(this));
		}.bind(this), function() {
			this.initialScan = false;
			this.initialScanRemoved = null;
		}.bind(this));
	}.bind(this));
};

DirectoryWatcher.prototype.getTimes = function() {
	var obj = {};
	var selfTime = 0;
	Object.keys(this.files).forEach(function(file) {
		var data = this.files[file];
		if(data[1]) {
			var time = Math.max(data[0], data[1] + FS_ACCURENCY);
			obj[file] = time;
			if(time > selfTime)
				selfTime = time;
		}
	}, this);
	if(this.nestedWatching) {
		Object.keys(this.directories).forEach(function(dir) {
			var w = this.directories[dir];
			var times = w.directoryWatcher.getTimes();
			Object.keys(times).forEach(function(file) {
				var time = times[file];
				obj[file] = time;
				if(time > selfTime)
					selfTime = time;
			});
		}, this);
		obj[this.path] = selfTime;
	}
	return obj;
};

DirectoryWatcher.prototype.close = function() {
	this.initialScan = false;
	this.watcher.close();
	if(this.nestedWatching) {
		Object.keys(this.directories).forEach(function(dir) {
			this.directories[dir].close();
		}, this);
	}
	this.emit("closed");
};
<commit_msg>style<commit_after>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
var EventEmitter = require("events").EventEmitter;
var async = require("async");
var chokidar = require("chokidar");
var fs = require("graceful-fs");
var path = require("path");

var watcherManager = require("./watcherManager");

var FS_ACCURENCY = 10000;

function withoutCase(str) {
	return str.toLowerCase();
}

function Watcher(directoryWatcher, filePath, startTime) {
	EventEmitter.call(this);
	this.directoryWatcher = directoryWatcher;
	this.path = filePath;
	this.startTime = startTime && +startTime;
	this.data = 0;
}

Watcher.prototype = Object.create(EventEmitter.prototype);
Watcher.prototype.constructor = Watcher;

Watcher.prototype.checkStartTime = function checkStartTime(mtime, initial) {
	if(typeof this.startTime !== "number") return !initial;
	var startTime = this.startTime;
	return startTime <= mtime;
};

Watcher.prototype.close = function close() {
	this.emit("closed");
};

function DirectoryWatcher(directoryPath, options) {
	EventEmitter.call(this);
	this.options = options;
	this.path = directoryPath;
	this.files = {};
	this.directories = {};
	this.watcher = chokidar.watch(directoryPath, {
		ignoreInitial: true,
		persistent: true,
		followSymlinks: false,
		depth: 0,
		atomic: false,
		alwaysStat: true,
		ignorePermissionErrors: true,
		ignored: options.ignored,
		usePolling: options.poll ? true : undefined,
		interval: typeof options.poll === "number" ? options.poll : undefined
	});
	this.watcher.on("add", this.onFileAdded.bind(this));
	this.watcher.on("addDir", this.onDirectoryAdded.bind(this));
	this.watcher.on("change", this.onChange.bind(this));
	this.watcher.on("unlink", this.onFileUnlinked.bind(this));
	this.watcher.on("unlinkDir", this.onDirectoryUnlinked.bind(this));
	this.watcher.on("error", this.onWatcherError.bind(this));
	this.initialScan = true;
	this.nestedWatching = false;
	this.initialScanRemoved = [];
	this.doInitialScan();
	this.watchers = {};
	this.refs = 0;
}
module.exports = DirectoryWatcher;

DirectoryWatcher.prototype = Object.create(EventEmitter.prototype);
DirectoryWatcher.prototype.constructor = DirectoryWatcher;

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
	var now = Date.now();
	var old = this.files[filePath];

	this.files[filePath] = [initial ? Math.min(now, mtime) : now, mtime];

	// we add the fs accurency to reach the maximum possible mtime
	if(mtime)
		mtime = mtime + FS_ACCURENCY;

	if(!old) {
		if(mtime) {
			if(this.watchers[withoutCase(filePath)]) {
				this.watchers[withoutCase(filePath)].forEach(function(w) {
					if(!initial || w.checkStartTime(mtime, initial)) {
						w.emit("change", mtime);
					}
				});
			}
		}
	} else if(!initial && mtime && type !== "add") {
		if(this.watchers[withoutCase(filePath)]) {
			this.watchers[withoutCase(filePath)].forEach(function(w) {
				w.emit("change", mtime);
			});
		}
	} else if(!initial && !mtime) {
		delete this.files[filePath];
		if(this.watchers[withoutCase(filePath)]) {
			this.watchers[withoutCase(filePath)].forEach(function(w) {
				w.emit("remove");
			});
		}
	}
	if(this.watchers[withoutCase(this.path)]) {
		this.watchers[withoutCase(this.path)].forEach(function(w) {
			if(!initial || w.checkStartTime(mtime, initial)) {
				w.emit("change", filePath, mtime);
			}
		});
	}
};

DirectoryWatcher.prototype.setDirectory = function setDirectory(directoryPath, exist, initial) {
	var old = this.directories[directoryPath];
	if(!old) {
		if(exist) {
			if(this.nestedWatching) {
				this.createNestedWatcher(directoryPath);
			} else {
				this.directories[directoryPath] = true;
			}
		}
	} else {
		if(!exist) {
			if(this.nestedWatching)
				this.directories[directoryPath].close();
			delete this.directories[directoryPath];
			if(!initial && this.watchers[withoutCase(this.path)]) {
				this.watchers[withoutCase(this.path)].forEach(function(w) {
					w.emit("change", directoryPath, w.data);
				});
			}
		}
	}
};

DirectoryWatcher.prototype.createNestedWatcher = function(directoryPath) {
	this.directories[directoryPath] = watcherManager.watchDirectory(directoryPath, this.options, 1);
	this.directories[directoryPath].on("change", function(filePath, mtime) {
		if(this.watchers[withoutCase(this.path)]) {
			this.watchers[withoutCase(this.path)].forEach(function(w) {
				if(w.checkStartTime(mtime, false)) {
					w.emit("change", filePath, mtime);
				}
			});
		}
	}.bind(this));
};

DirectoryWatcher.prototype.setNestedWatching = function(flag) {
	if(this.nestedWatching !== !!flag) {
		this.nestedWatching = !!flag;
		if(this.nestedWatching) {
			Object.keys(this.directories).forEach(function(directory) {
				this.createNestedWatcher(directory);
			}, this);
		} else {
			Object.keys(this.directories).forEach(function(directory) {
				this.directories[directory].close();
				this.directories[directory] = true;
			}, this);
		}
	}
};

DirectoryWatcher.prototype.watch = function watch(filePath, startTime) {
	this.watchers[withoutCase(filePath)] = this.watchers[withoutCase(filePath)] || [];
	this.refs++;
	var watcher = new Watcher(this, filePath, startTime);
	watcher.on("closed", function() {
		var idx = this.watchers[withoutCase(filePath)].indexOf(watcher);
		this.watchers[withoutCase(filePath)].splice(idx, 1);
		if(this.watchers[withoutCase(filePath)].length === 0) {
			delete this.watchers[withoutCase(filePath)];
			if(this.path === filePath)
				this.setNestedWatching(false);
		}
		if(--this.refs <= 0)
			this.close();
	}.bind(this));
	this.watchers[withoutCase(filePath)].push(watcher);
	var data;
	if(filePath === this.path) {
		this.setNestedWatching(true);
		data = false;
		Object.keys(this.files).forEach(function(file) {
			var d = this.files[file];
			if(!data)
				data = d;
			else
				data = [Math.max(data[0], d[0]), Math.max(data[1], d[1])];
		}, this);
	} else {
		data = this.files[filePath];
	}
	process.nextTick(function() {
		if(data) {
			var ts = data[0] === data[1] ? data[0] + FS_ACCURENCY : data[0];
			if(ts > startTime)
				watcher.emit("change", data[1] + FS_ACCURENCY);
		} else if(this.initialScan && this.initialScanRemoved.indexOf(filePath) >= 0) {
			watcher.emit("remove");
		}
	}.bind(this));
	return watcher;
};

DirectoryWatcher.prototype.onFileAdded = function onFileAdded(filePath, stat) {
	if(filePath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;

	this.setFileTime(filePath, +stat.mtime, false, "add");
};

DirectoryWatcher.prototype.onDirectoryAdded = function onDirectoryAdded(directoryPath /*, stat */) {
	if(directoryPath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(directoryPath.substr(this.path.length + 1))) return;
	this.setDirectory(directoryPath, true, false);
};

DirectoryWatcher.prototype.onChange = function onChange(filePath, stat) {
	if(filePath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;
	var mtime = +stat.mtime;
	if(FS_ACCURENCY > 1 && mtime % 1 !== 0)
		FS_ACCURENCY = 1;
	else if(FS_ACCURENCY > 10 && mtime % 10 !== 0)
		FS_ACCURENCY = 10;
	else if(FS_ACCURENCY > 100 && mtime % 100 !== 0)
		FS_ACCURENCY = 100;
	else if(FS_ACCURENCY > 1000 && mtime % 1000 !== 0)
		FS_ACCURENCY = 1000;
	else if(FS_ACCURENCY > 2000 && mtime % 2000 !== 0)
		FS_ACCURENCY = 2000;
	this.setFileTime(filePath, mtime, false, "change");
};

DirectoryWatcher.prototype.onFileUnlinked = function onFileUnlinked(filePath) {
	if(filePath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;
	this.setFileTime(filePath, null, false, "unlink");
	if(this.initialScan) {
		this.initialScanRemoved.push(filePath);
	}
};

DirectoryWatcher.prototype.onDirectoryUnlinked = function onDirectoryUnlinked(directoryPath) {
	if(directoryPath.indexOf(this.path) !== 0) return;
	if(/[\\\/]/.test(directoryPath.substr(this.path.length + 1))) return;
	this.setDirectory(directoryPath, false, false);
	if(this.initialScan) {
		this.initialScanRemoved.push(directoryPath);
	}
};

DirectoryWatcher.prototype.onWatcherError = function onWatcherError(/* err */) {
};

DirectoryWatcher.prototype.doInitialScan = function doInitialScan() {
	fs.readdir(this.path, function(err, items) {
		if(err) {
			this.initialScan = false;
			return;
		}
		async.forEach(items, function(item, callback) {
			var itemPath = path.join(this.path, item);
			fs.stat(itemPath, function(err2, stat) {
				if(!this.initialScan) return;
				if(err2) {
					callback();
					return;
				}
				if(stat.isFile()) {
					if(!this.files[itemPath])
						this.setFileTime(itemPath, +stat.mtime, true);
				} else if(stat.isDirectory()) {
					if(!this.directories[itemPath])
						this.setDirectory(itemPath, true, true);
				}
				callback();
			}.bind(this));
		}.bind(this), function() {
			this.initialScan = false;
			this.initialScanRemoved = null;
		}.bind(this));
	}.bind(this));
};

DirectoryWatcher.prototype.getTimes = function() {
	var obj = {};
	var selfTime = 0;
	Object.keys(this.files).forEach(function(file) {
		var data = this.files[file];
		if(data[1]) {
			var time = Math.max(data[0], data[1] + FS_ACCURENCY);
			obj[file] = time;
			if(time > selfTime)
				selfTime = time;
		}
	}, this);
	if(this.nestedWatching) {
		Object.keys(this.directories).forEach(function(dir) {
			var w = this.directories[dir];
			var times = w.directoryWatcher.getTimes();
			Object.keys(times).forEach(function(file) {
				var time = times[file];
				obj[file] = time;
				if(time > selfTime)
					selfTime = time;
			});
		}, this);
		obj[this.path] = selfTime;
	}
	return obj;
};

DirectoryWatcher.prototype.close = function() {
	this.initialScan = false;
	this.watcher.close();
	if(this.nestedWatching) {
		Object.keys(this.directories).forEach(function(dir) {
			this.directories[dir].close();
		}, this);
	}
	this.emit("closed");
};
<|endoftext|>


<commit_before>var j = require('jasmine'),
	CR = require('ConsoleReporter'),
	describe = j.describe,
	it = j.it,
	expect = j.expect,
	jasmine = j.jasmine;

module.exports = function($) {
	describe('index controller', function() {
		it('has an #index component', function() {
			expect($.index).toBeDefined();
			expect($.index).not.toBeNull();
		});

		it('#index component is a Ti.UI.Window', function() {
			expect(_.isFunction($.index.open)).toBeTruthy();
		});

		it('#index component has correct style', function() {
			expect($.index.backgroundColor).toEqual('#fff');
			expect($.index.exitOnClose).toEqual(true);
			expect($.index.fullscreen).toEqual(false);
			expect($.index.id).toEqual('index');
		});

		it('has an #label component', function() {
			expect($.label).toBeDefined();
			expect($.label).not.toBeNull();
		});

		it('#label component is a Ti.UI.Label', function() {
			expect($.label.text).toBeTruthy();
		});

		it('#label component has correct style', function() {
			expect($.label.color).toEqual('#000');
			expect($.label.font.fontSize).toEqual('18dp');
			expect($.label.font.fontWeight).toEqual('bold');
			expect($.label.height).toEqual(Ti.UI.SIZE);
			expect($.label.width).toEqual(Ti.UI.SIZE);
			expect($.label.text).toEqual('Hello, World!');
			expect($.label.id).toEqual('label');
		});

		it('#label can change text property', function() {
			var newtext = 'TEST TEXT';
			$.label.text = newtext;
			expect($.label.text).toEqual(newtext);
		});
	});

	jasmine.getEnv().addReporter(new CR({
		doneCallback: function(runner) {
			alert(runner.specs().length + ' specs, ' + runner.results().failedCount + ' failed');
		}
	}));
	jasmine.getEnv().execute();
};<commit_msg>adjust style check for mobileweb<commit_after>var j = require('jasmine'),
	CR = require('ConsoleReporter'),
	describe = j.describe,
	it = j.it,
	expect = j.expect,
	jasmine = j.jasmine;

module.exports = function($) {
	describe('index controller', function() {
		it('has an #index component', function() {
			expect($.index).toBeDefined();
			expect($.index).not.toBeNull();
		});

		it('#index component is a Ti.UI.Window', function() {
			expect(_.isFunction($.index.open)).toBeTruthy();
		});

		it('#index component has correct style', function() {
			expect($.index.backgroundColor).toEqual('#fff');
			expect($.index.exitOnClose).toEqual(true);
			expect($.index.fullscreen).toEqual(false);
			expect($.index.id).toEqual('index');
		});

		it('has an #label component', function() {
			expect($.label).toBeDefined();
			expect($.label).not.toBeNull();
		});

		it('#label component is a Ti.UI.Label', function() {
			expect($.label.text).toBeTruthy();
		});

		it('#label component has correct style', function() {
			expect($.label.color).toEqual('#000');
			expect($.label.font.fontSize).toEqual(OS_MOBILEWEB ? '18px' : '18dp');
			expect($.label.font.fontWeight).toEqual('bold');
			expect($.label.height).toEqual(Ti.UI.SIZE);
			expect($.label.width).toEqual(Ti.UI.SIZE);
			expect($.label.text).toEqual('Hello, World!');
			expect($.label.id).toEqual('label');
		});

		it('#label can change text property', function() {
			var newtext = 'TEST TEXT';
			$.label.text = newtext;
			expect($.label.text).toEqual(newtext);
		});
	});

	jasmine.getEnv().addReporter(new CR({
		doneCallback: function(runner) {
			alert(runner.specs().length + ' specs, ' + runner.results().failedCount + ' failed');
		}
	}));
	jasmine.getEnv().execute();
};<|endoftext|>


<commit_before>/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// The receiver is only meant to run on the Chromecast, so we have the
// ability to use modern APIs there that may not be available on all of the
// browsers our library supports.  Because of this, CastReceiver tests will
// only be run on Chrome and Chromecast.
/** @return {boolean} */
const castReceiverIntegrationSupport =
    () => shaka.util.Platform.isChrome() || shaka.util.Platform.isChromecast();
filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
  const CastReceiver = shaka.cast.CastReceiver;
  const CastUtils = shaka.cast.CastUtils;

  const originalCast = window['cast'];
  const originalUserAgent = navigator.userAgent;

  const eventManager = new shaka.util.EventManager();

  let mockReceiverManager;
  let mockReceiverApi;
  let mockShakaMessageBus;
  let mockGenericMessageBus;

  /** @type {shaka.cast.CastReceiver} */
  let receiver;
  /** @type {shaka.Player} */
  let player;
  /** @type {HTMLVideoElement} */
  let video;

  /** @type {shaka.util.PublicPromise} */
  let messageWaitPromise;

  /** @type {!Array.<function()>} */
  let toRestore;
  let pendingWaitWrapperCalls = 0;

  /** @type {!Object.<string, ?shaka.extern.DrmSupportType>} */
  let support = {};

  let fakeInitState;

  beforeAll(async () => {
    // In uncompiled mode, there is a UA check for Chromecast in order to make
    // manual testing easier.  For these automated tests, we want to act as if
    // we are running on the Chromecast, even in Chrome.
    // Since we can't write to window.navigator or navigator.userAgent, we use
    // Object.defineProperty.
    Object.defineProperty(window['navigator'],
        'userAgent', {value: 'CrKey', configurable: true});

    shaka.net.NetworkingEngine.registerScheme(
        'test', shaka.test.TestScheme.plugin);
    shaka.media.ManifestParser.registerParserByMime(
        'application/x-test-manifest',
        shaka.test.TestScheme.ManifestParser.factory);

    await shaka.test.TestScheme.createManifests(shaka, '');
    support = await shaka.media.DrmEngine.probeSupport();
  });

  beforeEach(() => {
    mockReceiverApi = createMockReceiverApi();

    const mockCanDisplayType = jasmine.createSpy('canDisplayType');
    mockCanDisplayType.and.returnValue(true);

    // We're using quotes to access window.cast because the compiler
    // knows about lots of Cast-specific APIs we aren't mocking.  We
    // don't need this mock strictly type-checked.
    window['cast'] = {
      receiver: mockReceiverApi,
      __platform__: {canDisplayType: mockCanDisplayType},
    };

    mockReceiverManager = createMockReceiverManager();
    mockShakaMessageBus = createMockMessageBus();
    mockGenericMessageBus = createMockMessageBus();

    video = shaka.test.UiUtils.createVideoElement();

    document.body.appendChild(video);

    player = new shaka.Player(video);
    receiver = new CastReceiver(video, player);

    toRestore = [];
    pendingWaitWrapperCalls = 0;

    fakeInitState = {
      player: {
        configure: {},
      },
      playerAfterLoad: {
        setTextTrackVisibility: true,
      },
      video: {
        loop: true,
        playbackRate: 5,
      },
      manifest: 'test:sintel_no_text',
      startTime: 0,
    };
  });

  afterEach(async () => {
    for (const restoreCallback of toRestore) {
      restoreCallback();
    }

    await receiver.destroy();
    document.body.removeChild(video);

    player = null;
    video = null;
    receiver = null;
  });

  afterAll(() => {
    if (originalUserAgent) {
      window['cast'] = originalCast;
      Object.defineProperty(window['navigator'],
          'userAgent', {value: originalUserAgent});
    }
  });

  filterDescribe('with drm', () => support['com.widevine.alpha'], () => {
    drmIt('sends reasonably-sized updates', async () => {
      // Use an encrypted asset, to make sure DRM info doesn't balloon the size.
      fakeInitState.manifest = 'test:sintel-enc';
      fakeInitState.player.configure['drm'] = {
        'servers': {
          'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth',
        },
      };

      const p = waitForLoadedData();

      // Start the process of loading by sending a fake init message.
      fakeConnectedSenders(1);
      fakeIncomingMessage({
        type: 'init',
        initState: fakeInitState,
        appData: {},
      }, mockShakaMessageBus);

      await p;
      // Wait for an update message.
      const message = await waitForUpdateMessage();
      // Check that the update message is of a reasonable size. From previous
      // testing we found that the socket would silently reject data that got
      // too big. 5KB is safely below the limit.
      expect(message.length).toBeLessThan(5 * 1024);
    });

    drmIt('has reasonable average message size', async () => {
      // Use an encrypted asset, to make sure DRM info doesn't balloon the size.
      fakeInitState.manifest = 'test:sintel-enc';

      const p = waitForLoadedData();

      // Start the process of loading by sending a fake init message.
      fakeConnectedSenders(1);
      fakeIncomingMessage({
        type: 'init',
        initState: fakeInitState,
        appData: {},
      }, mockShakaMessageBus);

      await p;
      // Collect 50 update messages, and average their length.
      // Not all properties are passed along on every update message, so
      // the average length is expected to be lower than the length of the first
      // update message.
      let totalLength = 0;
      for (const _ of shaka.util.Iterables.range(50)) {
        shaka.util.Functional.ignored(_);
        // eslint-disable-next-line no-await-in-loop
        const message = await waitForUpdateMessage();
        totalLength += message.length;
      }
      expect(totalLength / 50).toBeLessThan(3000);
    });
  });

  it('sends update messages every stage of loading', async () => {
    // Add wrappers to various methods along player.load to make sure that,
    // at each stage, the cast receiver can form an update message without
    // causing an error.
    waitForUpdateMessageWrapper(
        shaka.media.ManifestParser, 'ManifestParser', 'getFactory');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.test.TestScheme.ManifestParser.prototype, 'ManifestParser',
        'start');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.media.DrmEngine.prototype, 'DrmEngine', 'initForPlayback');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.media.DrmEngine.prototype, 'DrmEngine', 'attach');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.media.StreamingEngine.prototype, 'StreamingEngine', 'start');

    const p = waitForLoadedData();

    // Start the process of loading by sending a fake init message.
    fakeConnectedSenders(1);
    fakeIncomingMessage({
      type: 'init',
      initState: fakeInitState,
      appData: {},
    }, mockShakaMessageBus);

    await p;
    // Make sure that each of the methods covered by
    // waitForUpdateMessageWrapper is called by this point.
    expect(pendingWaitWrapperCalls).toBe(0);

    // Wait for a final update message before proceeding.
    await waitForUpdateMessage();
  });

  /**
   * Creates a wrapper around a method on a given prototype, which makes it
   * wait on waitForUpdateMessage before returning, and registers that wrapper
   * to be uninstalled afterwards.
   * The replaced method is expected to be a method that returns a promise.
   * @param {!Object} prototype
   * @param {string} name
   * @param {string} methodName
   */
  function waitForUpdateMessageWrapper(prototype, name, methodName) {
    pendingWaitWrapperCalls += 1;
    const original = prototype[methodName];
    // eslint-disable-next-line no-restricted-syntax
    prototype[methodName] = /** @this {Object} @return {*} */ async function() {
      pendingWaitWrapperCalls -= 1;
      shaka.log.debug(
          'Waiting for update message before calling ' +
          name + '.' + methodName + '...');
      const originalArguments = Array.from(arguments);
      await waitForUpdateMessage();
      // eslint-disable-next-line no-restricted-syntax
      return original.apply(this, originalArguments);
    };
    toRestore.push(() => {
      prototype[methodName] = original;
    });
  }

  function waitForLoadedData() {
    return new Promise((resolve, reject) => {
      eventManager.listenOnce(video, 'loadeddata', resolve);
      eventManager.listenOnce(player, 'error', reject);
    });
  }

  function waitForUpdateMessage() {
    messageWaitPromise = new shaka.util.PublicPromise();
    return messageWaitPromise;
  }

  function createMockReceiverApi() {
    return {
      CastReceiverManager: {
        getInstance: () => mockReceiverManager,
      },
      media: {
        // Defined by the SDK, but we aren't loading it here.
        MetadataType: {
          GENERIC: 0,
          MUSIC_TRACK: 3,
        },
      },
    };
  }

  function createMockReceiverManager() {
    return {
      start: jasmine.createSpy('CastReceiverManager.start'),
      stop: jasmine.createSpy('CastReceiverManager.stop'),
      setSystemVolumeLevel:
          jasmine.createSpy('CastReceiverManager.setSystemVolumeLevel'),
      setSystemVolumeMuted:
          jasmine.createSpy('CastReceiverManager.setSystemVolumeMuted'),
      getSenders: jasmine.createSpy('CastReceiverManager.getSenders'),
      getSystemVolume: () => ({level: 1, muted: false}),
      getCastMessageBus: (namespace) => {
        if (namespace == CastUtils.SHAKA_MESSAGE_NAMESPACE) {
          return mockShakaMessageBus;
        }

        return mockGenericMessageBus;
      },
    };
  }

  function createMockMessageBus() {
    const bus = {
      messages: [],
      broadcast: jasmine.createSpy('CastMessageBus.broadcast'),
      getCastChannel: jasmine.createSpy('CastMessageBus.getCastChannel'),
    };
    // For convenience, deserialize and store sent messages.
    bus.broadcast.and.callFake((message) => {
      bus.messages.push(CastUtils.deserialize(message));
      // Check to see if it's an update message.
      const parsed = CastUtils.deserialize(message);
      if (parsed.type == 'update' && messageWaitPromise) {
        shaka.log.debug('Received update message. Proceeding...');
        messageWaitPromise.resolve(message);
        messageWaitPromise = null;
      }
    });
    const channel = {
      messages: [],
      send: (message) => {
        channel.messages.push(CastUtils.deserialize(message));
      },
    };
    bus.getCastChannel.and.returnValue(channel);
    return bus;
  }

  /**
   * @param {number} num
   */
  function fakeConnectedSenders(num) {
    const senderArray = [];
    while (num--) {
      senderArray.push('senderId');
    }

    mockReceiverManager.getSenders.and.returnValue(senderArray);
    mockReceiverManager.onSenderConnected();
  }

  /**
   * @param {*} message
   * @param {!cast.receiver.CastMessageBus} bus
   * @param {string=} senderId
   */
  function fakeIncomingMessage(message, bus, senderId) {
    const serialized = CastUtils.serialize(message);
    const messageEvent = {
      senderId: senderId,
      data: serialized,
    };
    bus.onMessage(messageEvent);
  }
});
<commit_msg>test: Add missing license server info to cast test<commit_after>/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// The receiver is only meant to run on the Chromecast, so we have the
// ability to use modern APIs there that may not be available on all of the
// browsers our library supports.  Because of this, CastReceiver tests will
// only be run on Chrome and Chromecast.
/** @return {boolean} */
const castReceiverIntegrationSupport =
    () => shaka.util.Platform.isChrome() || shaka.util.Platform.isChromecast();
filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
  const CastReceiver = shaka.cast.CastReceiver;
  const CastUtils = shaka.cast.CastUtils;

  const originalCast = window['cast'];
  const originalUserAgent = navigator.userAgent;

  const eventManager = new shaka.util.EventManager();

  let mockReceiverManager;
  let mockReceiverApi;
  let mockShakaMessageBus;
  let mockGenericMessageBus;

  /** @type {shaka.cast.CastReceiver} */
  let receiver;
  /** @type {shaka.Player} */
  let player;
  /** @type {HTMLVideoElement} */
  let video;

  /** @type {shaka.util.PublicPromise} */
  let messageWaitPromise;

  /** @type {!Array.<function()>} */
  let toRestore;
  let pendingWaitWrapperCalls = 0;

  /** @type {!Object.<string, ?shaka.extern.DrmSupportType>} */
  let support = {};

  let fakeInitState;

  beforeAll(async () => {
    // In uncompiled mode, there is a UA check for Chromecast in order to make
    // manual testing easier.  For these automated tests, we want to act as if
    // we are running on the Chromecast, even in Chrome.
    // Since we can't write to window.navigator or navigator.userAgent, we use
    // Object.defineProperty.
    Object.defineProperty(window['navigator'],
        'userAgent', {value: 'CrKey', configurable: true});

    shaka.net.NetworkingEngine.registerScheme(
        'test', shaka.test.TestScheme.plugin);
    shaka.media.ManifestParser.registerParserByMime(
        'application/x-test-manifest',
        shaka.test.TestScheme.ManifestParser.factory);

    await shaka.test.TestScheme.createManifests(shaka, '');
    support = await shaka.media.DrmEngine.probeSupport();
  });

  beforeEach(() => {
    mockReceiverApi = createMockReceiverApi();

    const mockCanDisplayType = jasmine.createSpy('canDisplayType');
    mockCanDisplayType.and.returnValue(true);

    // We're using quotes to access window.cast because the compiler
    // knows about lots of Cast-specific APIs we aren't mocking.  We
    // don't need this mock strictly type-checked.
    window['cast'] = {
      receiver: mockReceiverApi,
      __platform__: {canDisplayType: mockCanDisplayType},
    };

    mockReceiverManager = createMockReceiverManager();
    mockShakaMessageBus = createMockMessageBus();
    mockGenericMessageBus = createMockMessageBus();

    video = shaka.test.UiUtils.createVideoElement();

    document.body.appendChild(video);

    player = new shaka.Player(video);
    receiver = new CastReceiver(video, player);

    toRestore = [];
    pendingWaitWrapperCalls = 0;

    fakeInitState = {
      player: {
        configure: {},
      },
      playerAfterLoad: {
        setTextTrackVisibility: true,
      },
      video: {
        loop: true,
        playbackRate: 5,
      },
      manifest: 'test:sintel_no_text',
      startTime: 0,
    };
  });

  afterEach(async () => {
    for (const restoreCallback of toRestore) {
      restoreCallback();
    }

    await receiver.destroy();
    document.body.removeChild(video);

    player = null;
    video = null;
    receiver = null;
  });

  afterAll(() => {
    if (originalUserAgent) {
      window['cast'] = originalCast;
      Object.defineProperty(window['navigator'],
          'userAgent', {value: originalUserAgent});
    }
  });

  filterDescribe('with drm', () => support['com.widevine.alpha'], () => {
    drmIt('sends reasonably-sized updates', async () => {
      // Use an encrypted asset, to make sure DRM info doesn't balloon the size.
      fakeInitState.manifest = 'test:sintel-enc';
      fakeInitState.player.configure['drm'] = {
        'servers': {
          'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth',
        },
      };

      const p = waitForLoadedData();

      // Start the process of loading by sending a fake init message.
      fakeConnectedSenders(1);
      fakeIncomingMessage({
        type: 'init',
        initState: fakeInitState,
        appData: {},
      }, mockShakaMessageBus);

      await p;
      // Wait for an update message.
      const message = await waitForUpdateMessage();
      // Check that the update message is of a reasonable size. From previous
      // testing we found that the socket would silently reject data that got
      // too big. 5KB is safely below the limit.
      expect(message.length).toBeLessThan(5 * 1024);
    });

    drmIt('has reasonable average message size', async () => {
      // Use an encrypted asset, to make sure DRM info doesn't balloon the size.
      fakeInitState.manifest = 'test:sintel-enc';
      fakeInitState.player.configure['drm'] = {
        'servers': {
          'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth',
        },
      };

      const p = waitForLoadedData();

      // Start the process of loading by sending a fake init message.
      fakeConnectedSenders(1);
      fakeIncomingMessage({
        type: 'init',
        initState: fakeInitState,
        appData: {},
      }, mockShakaMessageBus);

      await p;
      // Collect 50 update messages, and average their length.
      // Not all properties are passed along on every update message, so
      // the average length is expected to be lower than the length of the first
      // update message.
      let totalLength = 0;
      for (const _ of shaka.util.Iterables.range(50)) {
        shaka.util.Functional.ignored(_);
        // eslint-disable-next-line no-await-in-loop
        const message = await waitForUpdateMessage();
        totalLength += message.length;
      }
      expect(totalLength / 50).toBeLessThan(3000);
    });
  });

  it('sends update messages every stage of loading', async () => {
    // Add wrappers to various methods along player.load to make sure that,
    // at each stage, the cast receiver can form an update message without
    // causing an error.
    waitForUpdateMessageWrapper(
        shaka.media.ManifestParser, 'ManifestParser', 'getFactory');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.test.TestScheme.ManifestParser.prototype, 'ManifestParser',
        'start');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.media.DrmEngine.prototype, 'DrmEngine', 'initForPlayback');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.media.DrmEngine.prototype, 'DrmEngine', 'attach');
    waitForUpdateMessageWrapper(
        // eslint-disable-next-line no-restricted-syntax
        shaka.media.StreamingEngine.prototype, 'StreamingEngine', 'start');

    const p = waitForLoadedData();

    // Start the process of loading by sending a fake init message.
    fakeConnectedSenders(1);
    fakeIncomingMessage({
      type: 'init',
      initState: fakeInitState,
      appData: {},
    }, mockShakaMessageBus);

    await p;
    // Make sure that each of the methods covered by
    // waitForUpdateMessageWrapper is called by this point.
    expect(pendingWaitWrapperCalls).toBe(0);

    // Wait for a final update message before proceeding.
    await waitForUpdateMessage();
  });

  /**
   * Creates a wrapper around a method on a given prototype, which makes it
   * wait on waitForUpdateMessage before returning, and registers that wrapper
   * to be uninstalled afterwards.
   * The replaced method is expected to be a method that returns a promise.
   * @param {!Object} prototype
   * @param {string} name
   * @param {string} methodName
   */
  function waitForUpdateMessageWrapper(prototype, name, methodName) {
    pendingWaitWrapperCalls += 1;
    const original = prototype[methodName];
    // eslint-disable-next-line no-restricted-syntax
    prototype[methodName] = /** @this {Object} @return {*} */ async function() {
      pendingWaitWrapperCalls -= 1;
      shaka.log.debug(
          'Waiting for update message before calling ' +
          name + '.' + methodName + '...');
      const originalArguments = Array.from(arguments);
      await waitForUpdateMessage();
      // eslint-disable-next-line no-restricted-syntax
      return original.apply(this, originalArguments);
    };
    toRestore.push(() => {
      prototype[methodName] = original;
    });
  }

  function waitForLoadedData() {
    return new Promise((resolve, reject) => {
      eventManager.listenOnce(video, 'loadeddata', resolve);
      eventManager.listenOnce(player, 'error', reject);
    });
  }

  function waitForUpdateMessage() {
    messageWaitPromise = new shaka.util.PublicPromise();
    return messageWaitPromise;
  }

  function createMockReceiverApi() {
    return {
      CastReceiverManager: {
        getInstance: () => mockReceiverManager,
      },
      media: {
        // Defined by the SDK, but we aren't loading it here.
        MetadataType: {
          GENERIC: 0,
          MUSIC_TRACK: 3,
        },
      },
    };
  }

  function createMockReceiverManager() {
    return {
      start: jasmine.createSpy('CastReceiverManager.start'),
      stop: jasmine.createSpy('CastReceiverManager.stop'),
      setSystemVolumeLevel:
          jasmine.createSpy('CastReceiverManager.setSystemVolumeLevel'),
      setSystemVolumeMuted:
          jasmine.createSpy('CastReceiverManager.setSystemVolumeMuted'),
      getSenders: jasmine.createSpy('CastReceiverManager.getSenders'),
      getSystemVolume: () => ({level: 1, muted: false}),
      getCastMessageBus: (namespace) => {
        if (namespace == CastUtils.SHAKA_MESSAGE_NAMESPACE) {
          return mockShakaMessageBus;
        }

        return mockGenericMessageBus;
      },
    };
  }

  function createMockMessageBus() {
    const bus = {
      messages: [],
      broadcast: jasmine.createSpy('CastMessageBus.broadcast'),
      getCastChannel: jasmine.createSpy('CastMessageBus.getCastChannel'),
    };
    // For convenience, deserialize and store sent messages.
    bus.broadcast.and.callFake((message) => {
      bus.messages.push(CastUtils.deserialize(message));
      // Check to see if it's an update message.
      const parsed = CastUtils.deserialize(message);
      if (parsed.type == 'update' && messageWaitPromise) {
        shaka.log.debug('Received update message. Proceeding...');
        messageWaitPromise.resolve(message);
        messageWaitPromise = null;
      }
    });
    const channel = {
      messages: [],
      send: (message) => {
        channel.messages.push(CastUtils.deserialize(message));
      },
    };
    bus.getCastChannel.and.returnValue(channel);
    return bus;
  }

  /**
   * @param {number} num
   */
  function fakeConnectedSenders(num) {
    const senderArray = [];
    while (num--) {
      senderArray.push('senderId');
    }

    mockReceiverManager.getSenders.and.returnValue(senderArray);
    mockReceiverManager.onSenderConnected();
  }

  /**
   * @param {*} message
   * @param {!cast.receiver.CastMessageBus} bus
   * @param {string=} senderId
   */
  function fakeIncomingMessage(message, bus, senderId) {
    const serialized = CastUtils.serialize(message);
    const messageEvent = {
      senderId: senderId,
      data: serialized,
    };
    bus.onMessage(messageEvent);
  }
});
<|endoftext|>


<commit_before>/**
 * Created by AsherS on 8/20/15.
 */

var Q = require('q');
var _ = require('underscore');
var path = require('path');
var url = require('url');
var qfs = require("q-io/fs");
var logger = require('./logger/logger')(module);
var loggerDecorator = require('./utils/log-decorator');
var m3u8Handler = require('./manifest/promise-m3u8');
var twoPhasedChunklistManifestGenerator = require('./manifest/TwoPhasedChunklistManifestGenerator');
var util = require("util");
var events = require("events");
var config = require('./../common/Configuration');
var glob = require('glob');
var httpUtils=require('./utils/http-utils');

function isSameM3U8(a, b) {
    if (a.length !== b.length) {
        return false;
    }

    //100 bytes are enough to verify it changed
    for (var i = a.length ; i>=Math.max(a.length-100,0); --i) {
        if (a[i] !== b[i]) {
            return false;
        }
    }

    return true;
};

var FlavorDownloader = (function () {

    var maxAllowedConsecutiveFailures = 2;
    var maxAmountOfDownloadPendingChunks = 20;
    var downloaderEnd = Q.defer();

    function FlavorDownloader(m3u8Url, destPath, entryId, flavor, newPlaylistName, manifestTimeWindow, manifestMaxChunkCount) {
        var that = this;
        events.EventEmitter.call(this);
        this.id = this.constructor.uniqueId;
        this.constructor.uniqueId = this.constructor.uniqueId + 1;
        this.entryId = entryId;
        this.flavor = flavor;
        this.streamUrl = m3u8Url;
        this.destFolderPath = destPath;
        this.shouldRun = true;
        this.runStatus = 'uninitialized';
        this.pollingInterval = config.get("pollingInterval");
        this.tmpM3u8Folder = path.join(destPath, 'tmpM3u8');
        this.downloadedChunks = {};
        this.chunksToDownload = {};
        this.failedAttempts = 0;

        this.logger = loggerDecorator(logger,  "[" + that.entryId + "][" + that.flavor + "][id:" + that.id + "] ");

        var lastM3U8Content=null;
        this._lastM3U8Time=0;

        this.tsHttpUtils = httpUtils.HttpDownloader(this.logger);
        this.m3u8HttpUtils = httpUtils.HttpDownloader(this.logger,function (newM3u8Content) {
            var retValue=false;

            //optimization: if M3U8 has not updated the last 10 seconds then ignore
            if (lastM3U8Content &&
                new Date()-that._lastM3U8Time<10000 &&
                isSameM3U8(lastM3U8Content,newM3u8Content)) {
                retValue=true;
            }

            lastM3U8Content=newM3u8Content;
            return retValue;
        });

        var downloadedChunksGetter = function getDownloadedChunks(){
            return that.downloadedChunks;
        };

        this.m3u8generator = twoPhasedChunklistManifestGenerator(downloadedChunksGetter, this.destFolderPath, newPlaylistName, manifestTimeWindow, manifestMaxChunkCount, this.logger);

        downloaderEnd.promise.then(function(){
            that.emit("downloader-end");
        });
    }
    util.inherits(FlavorDownloader, events.EventEmitter);

    var downloadIndefinitely = function (onIterationEndCallback) {
        var that = this;
        that.logger.info("starting iteration");
        if (!that.shouldRun) {
            downloaderEnd.resolve();
            return Q();
        }

        that.emit("iteration-start");
        return downloadPlaylist(that)
            .then(function(){
                // Reset failed attempts count
                that.failedAttempts = 0;
            })
            .catch(function failure(err) {
                that.emit("iteration-error", err);
                that.logger.error("iteration error: " + err);

                that.failedAttempts = that.failedAttempts + 1;
                var shouldAbort = false;

                if (that.runStatus === 'uninitialized')
                {
                    that.logger.error("Flavor downloader failed to initialize");
                    shouldAbort = true;
                }

                if (that.failedAttempts === maxAllowedConsecutiveFailures) {
                    // Do not try to handle this flavor if maxAllowedConsecutiveFailures attempts were reached
                    that.logger.error("Flavor downloader failed " + maxAllowedConsecutiveFailures + " consecutive times - stopping it");
                    shouldAbort = true;
                }

                if (shouldAbort) {
                    that.runStatus = 'Aborting'; // Fail fast - no need to allow teardown interval
                    that.stop().catch(function (err) {
                        that.logger.error("Failed to stop: " + err);
                    }).then(function(){
                        that.m3u8generator.remove()
                            .catch(function (err) {
                                that.logger.error("Failed to remove manifest: " + err);
                            })
                    });

                    throw err;
                }
            })
            .then(function () {
                if (that.shouldRun) {
                    setTimeout(function () {
                        that.logger.debug('scheduling downloadIndefinitely to be run in ' + that.pollingInterval);
                        downloadIndefinitely.call(that, onIterationEndCallback);
                    }, that.pollingInterval);
                }
            }).finally(function(){
                that.emit("iteration-end");

                if (!that.shouldRun) {
                    downloaderEnd.resolve();
                }
            });
    };

    var downloadChunks = function (that, baseUrl, chunksToDownload) {
        return Q.allSettled(_.map(chunksToDownload, function (chunkName) {
            var chunkUrl = url.resolve(baseUrl + '/', chunkName);

            //return the promise of the downloaded chunk
            return that.tsHttpUtils.downloadFile(chunkUrl, path.join(that.destFolderPath, chunkName))
                .then(function(){
                    delete that.chunksToDownload[chunkName];
                    that.downloadedChunks[chunkName] = true;
                });
        })).then(function filterResolvedPromises(results) {
            return _.filter(results, function (p) {
                return (p.state !== 'fulfilled');
            });
        }).then(function checkRejectedPromises(filteredResults) {
            if (filteredResults.length > 0) {

                //group list of errors:
                var errorsList =  _.map(filteredResults, function (i) {
                    return i.reason + "\n";
                });

                return Q.reject(new Error(errorsList));
            }
            return Q.resolve();
        });
    };

    var downloadPlaylist = function (that) {

        var parsedUrl, baseUrl, playlistName;

        var downloadFlavorManifest = function () {
            that.logger.debug( "Downloading from : %s" , that.streamUrl);
            parsedUrl = url.parse(that.streamUrl);
            baseUrl = parsedUrl.href.substring(0, parsedUrl.href.lastIndexOf('/'));
            playlistName = path.basename(parsedUrl.pathname);
            var playlistDestPath = path.join(that.tmpM3u8Folder, playlistName);
            that.logger.debug( "Temp playlist folder located at: %s" , playlistDestPath);
            that.logger.debug( "Reading current M3U8 from %s" , playlistDestPath);
            return that.m3u8HttpUtils.downloadFile(that.streamUrl, playlistDestPath).then(function () {
                that._lastM3U8Time=new Date();
                return playlistDestPath;
            });
        };

        var updateListOfNewChunksToProcess = function (downloadedM3u8) {
            var newChunks = _.chain(downloadedM3u8.items.PlaylistItem)
                .filter(function (item) {
                    return !that.downloadedChunks[item.get('uri')] && !that.chunksToDownload[item.get('uri')];
                }).value();

            // update that these new chunks need to be downloaded
            _.each(newChunks, function (c) {
                that.logger.debug('Adding ' + c.get('uri') + ' to the list of chunks to download');
                that.chunksToDownload[c.get('uri')] = true;
            });

            if (Object.keys(that.chunksToDownload).length >= maxAmountOfDownloadPendingChunks)
            {
                // This shouldn't happen - stop this downloader
                that.logger.error("Chunk download list size is " + maxAmountOfDownloadPendingChunks + " - this shouldn't happen. Stopping the downloader for this flavor");
                that.stop().catch(function(err){
                    that.logger.error("Failed to stop: " + err);
                });
            }

            return newChunks;
        };

        var processObsoleteChunks = function (results) {
            // Try to delete files that were removed from the manifest
            // The last promise is the one for the update manifest request
            var manifestUpdatePromiseResult = _.last(results);
            if (manifestUpdatePromiseResult.state === 'fulfilled') {
                // Delete the obsolete chunks from disk
                _.each(manifestUpdatePromiseResult.value.removedChunks, function (itemToDelete) {
                    // Delete on a best effort basis
                    var pathToDelete = path.join(that.destFolderPath, itemToDelete.get('uri'));
                    qfs.remove(pathToDelete)
                        .then(function () {
                            that.logger.info('Successfully deleted chunk ' + itemToDelete.get('uri'));
                        })
                        .catch(function () {
                            that.logger.info('Error deleting a chunk which was removed from a manifest - ' + itemToDelete.get('uri'));
                        });
                });

                // Ensure that chunks removed from the intermediate manifest are not in the chunksToDownload list
                // (as they will never be processed)
                _.each(manifestUpdatePromiseResult.value.bumpedChunks, function (chunk) {
                    var chunkName = chunk.get('uri');
                    if (that.chunksToDownload[chunkName]) {
                        that.logger.error("Removing chunk " + chunkName + " from chunksToDownload - this shouldn't happen");
                        delete that.chunksToDownload[chunkName];
                    }
                });

                // Remove the obsolete chunks' reference from persistence
                _.each(manifestUpdatePromiseResult.value.removedChunks, function (chunk) {
                    that.logger.debug('Delete ' + chunk.get('uri') + ' from the list of chunks to download');
                    delete that.downloadedChunks[chunk.get('uri')];
                });
            }
        };

        var concludeOperation = function (results) {
            var rejected = _.filter(results, function (p) {
                return (p.state !== 'fulfilled');
            });
            if (rejected.length > 0) {
                var errorsString = "";
                _.forEach(rejected, function (r) {
                    errorsString += r.reason + "\n" + r.reason.stack + "\n";
                });
                throw new Error(errorsString);
            }
            return Q.resolve();
        };

        return Q.fcall(function () {
            return downloadFlavorManifest()
                .then(function finishedDownload(playlistDestPath) {
                    return m3u8Handler.parseM3U8(playlistDestPath)
                        .then(function (m3u) {
                            that.logger.debug( "Received M3U8: \n%s" , m3u);
                            return m3u;
                        });
                }).then(function (m3u) {
                    return updateListOfNewChunksToProcess(m3u);
                }).then(function (newChunks) {
                    //generate updated m3u8
                    var m3u8generatorPromise = that.m3u8generator.update(newChunks);

                    // Download chunks - all the chunks that were pending download beforehand along with the new chunks
                    var downloadChunksPromise = downloadChunks(that, baseUrl, Object.keys(that.chunksToDownload));
                    return Q.allSettled([downloadChunksPromise, m3u8generatorPromise]);
                }).then(function (results) {
                    processObsoleteChunks(results); // Trigger deletion on a best-effort basis
                    return results;
                }).then(function (results) {
                    return concludeOperation(results);
                }).catch(function (err) {
                    if (err==httpUtils.HttpDownloadNotChanged) {
                        return Q.resolve();
                    } else {
                        return Q.reject(err);
                    }
                });
        });
    };

    FlavorDownloader.prototype.start = function (onIterationEndCallback) {
        var that = this;
        that.logger.info("Flavor start requested");
        that.logger.info("Starting flavor downloader - url: %s destination folder: %s" , this.streamUrl , this.destFolderPath);
        that.logger.debug("Polling interval: %s " , this.pollingInterval);

        //create tmp destination folder for media-server m3u8 files
        return qfs.makeTree(this.tmpM3u8Folder)
            .then(function() {
                return that.m3u8generator.init();
            })
            .then(function(){
                var pGlob = Q.denodeify(glob);
                return pGlob(path.join(that.destFolderPath, '*.ts')).then(function(downloadedChunks){
                    _.each(downloadedChunks, function(c){
                        that.downloadedChunks[path.basename(c)] = true;
                    });
                });
            })
            .then(function(){
                return downloadIndefinitely.call(that, onIterationEndCallback);
            })
            .then(function(){
                FlavorDownloader.activeInstances[that.id] = that.entryId + "_" + that.flavor;
                that.runStatus = 'running';
            })
            .catch(function(err){
                that.logger.error("Error occurred while starting flavor downloader: " + err);
                err.flavor = that.flavor;
                throw err;
            });
    };

    FlavorDownloader.prototype.stop = function () {
        var that = this;
        if (that.stopPromise)
        {
            // Stop already requested
            that.logger.info('stop requested - ignoring request as this flavor downloader either already stopped or is currently stopping');
            return that.stopPromise;
        }

        var d = Q.defer();
        that.stopPromise = d.promise;
        that.stopPromise.then(function(){
            that.runStatus = 'stopped';
            that.emit("stopped", that.flavor);
        });

        var teardown = Q();
        if (that.runStatus === 'running')
        {
            var teardownInterval = config.get("flavorDownloaderTeardownInterval");
            that.logger.info('stop requested - allowing ' + teardownInterval + 'ms for orderly tear-down');
            teardown = Q.delay(teardownInterval).then(function(){
                that.logger.info('stopping flavor downloader after allowing orderly tear-down');
            });
        }

        return teardown.then(function(){
            that.logger.info('stopping flavor downloader');
            that.shouldRun = false;
            downloaderEnd.promise.then(function(){
                that.logger.info("Stopped");
                delete FlavorDownloader.activeInstances[that.id];
                d.resolve();
            });

            return d.promise;
        });
    };

    FlavorDownloader.activeInstances = {};
    FlavorDownloader.uniqueId = 0;
    return FlavorDownloader;
})();

module.exports = FlavorDownloader;<commit_msg>small fix<commit_after>/**
 * Created by AsherS on 8/20/15.
 */

var Q = require('q');
var _ = require('underscore');
var path = require('path');
var url = require('url');
var qfs = require("q-io/fs");
var logger = require('./logger/logger')(module);
var loggerDecorator = require('./utils/log-decorator');
var m3u8Handler = require('./manifest/promise-m3u8');
var twoPhasedChunklistManifestGenerator = require('./manifest/TwoPhasedChunklistManifestGenerator');
var util = require("util");
var events = require("events");
var config = require('./../common/Configuration');
var glob = require('glob');
var httpUtils=require('./utils/http-utils');

function isSameM3U8(a, b) {
    if (a.length !== b.length) {
        return false;
    }

    //100 bytes are enough to verify it changed
    for (var i = a.length-1 ; i>=Math.max(a.length-100,0); i--) {
        if (a[i] !== b[i]) {
            return false;
        }
    }

    return true;
};

var FlavorDownloader = (function () {

    var maxAllowedConsecutiveFailures = 2;
    var maxAmountOfDownloadPendingChunks = 20;
    var downloaderEnd = Q.defer();

    function FlavorDownloader(m3u8Url, destPath, entryId, flavor, newPlaylistName, manifestTimeWindow, manifestMaxChunkCount) {
        var that = this;
        events.EventEmitter.call(this);
        this.id = this.constructor.uniqueId;
        this.constructor.uniqueId = this.constructor.uniqueId + 1;
        this.entryId = entryId;
        this.flavor = flavor;
        this.streamUrl = m3u8Url;
        this.destFolderPath = destPath;
        this.shouldRun = true;
        this.runStatus = 'uninitialized';
        this.pollingInterval = config.get("pollingInterval");
        this.tmpM3u8Folder = path.join(destPath, 'tmpM3u8');
        this.downloadedChunks = {};
        this.chunksToDownload = {};
        this.failedAttempts = 0;

        this.logger = loggerDecorator(logger,  "[" + that.entryId + "][" + that.flavor + "][id:" + that.id + "] ");

        var lastM3U8Content=null;
        this._lastM3U8Time=0;

        this.tsHttpUtils = httpUtils.HttpDownloader(this.logger);
        this.m3u8HttpUtils = httpUtils.HttpDownloader(this.logger,function (newM3u8Content) {
            var retValue=false;

            //optimization: if M3U8 has not updated the last 10 seconds then ignore
            if (lastM3U8Content &&
                new Date()-that._lastM3U8Time<10000 &&
                isSameM3U8(lastM3U8Content,newM3u8Content)) {
                retValue=true;
            }

            lastM3U8Content=newM3u8Content;
            return retValue;
        });

        var downloadedChunksGetter = function getDownloadedChunks(){
            return that.downloadedChunks;
        };

        this.m3u8generator = twoPhasedChunklistManifestGenerator(downloadedChunksGetter, this.destFolderPath, newPlaylistName, manifestTimeWindow, manifestMaxChunkCount, this.logger);

        downloaderEnd.promise.then(function(){
            that.emit("downloader-end");
        });
    }
    util.inherits(FlavorDownloader, events.EventEmitter);

    var downloadIndefinitely = function (onIterationEndCallback) {
        var that = this;
        that.logger.info("starting iteration");
        if (!that.shouldRun) {
            downloaderEnd.resolve();
            return Q();
        }

        that.emit("iteration-start");
        return downloadPlaylist(that)
            .then(function(){
                // Reset failed attempts count
                that.failedAttempts = 0;
            })
            .catch(function failure(err) {
                that.emit("iteration-error", err);
                that.logger.error("iteration error: " + err);

                that.failedAttempts = that.failedAttempts + 1;
                var shouldAbort = false;

                if (that.runStatus === 'uninitialized')
                {
                    that.logger.error("Flavor downloader failed to initialize");
                    shouldAbort = true;
                }

                if (that.failedAttempts === maxAllowedConsecutiveFailures) {
                    // Do not try to handle this flavor if maxAllowedConsecutiveFailures attempts were reached
                    that.logger.error("Flavor downloader failed " + maxAllowedConsecutiveFailures + " consecutive times - stopping it");
                    shouldAbort = true;
                }

                if (shouldAbort) {
                    that.runStatus = 'Aborting'; // Fail fast - no need to allow teardown interval
                    that.stop().catch(function (err) {
                        that.logger.error("Failed to stop: " + err);
                    }).then(function(){
                        that.m3u8generator.remove()
                            .catch(function (err) {
                                that.logger.error("Failed to remove manifest: " + err);
                            })
                    });

                    throw err;
                }
            })
            .then(function () {
                if (that.shouldRun) {
                    setTimeout(function () {
                        that.logger.debug('scheduling downloadIndefinitely to be run in ' + that.pollingInterval);
                        downloadIndefinitely.call(that, onIterationEndCallback);
                    }, that.pollingInterval);
                }
            }).finally(function(){
                that.emit("iteration-end");

                if (!that.shouldRun) {
                    downloaderEnd.resolve();
                }
            });
    };

    var downloadChunks = function (that, baseUrl, chunksToDownload) {
        return Q.allSettled(_.map(chunksToDownload, function (chunkName) {
            var chunkUrl = url.resolve(baseUrl + '/', chunkName);

            //return the promise of the downloaded chunk
            return that.tsHttpUtils.downloadFile(chunkUrl, path.join(that.destFolderPath, chunkName))
                .then(function(){
                    delete that.chunksToDownload[chunkName];
                    that.downloadedChunks[chunkName] = true;
                });
        })).then(function filterResolvedPromises(results) {
            return _.filter(results, function (p) {
                return (p.state !== 'fulfilled');
            });
        }).then(function checkRejectedPromises(filteredResults) {
            if (filteredResults.length > 0) {

                //group list of errors:
                var errorsList =  _.map(filteredResults, function (i) {
                    return i.reason + "\n";
                });

                return Q.reject(new Error(errorsList));
            }
            return Q.resolve();
        });
    };

    var downloadPlaylist = function (that) {

        var parsedUrl, baseUrl, playlistName;

        var downloadFlavorManifest = function () {
            that.logger.debug( "Downloading from : %s" , that.streamUrl);
            parsedUrl = url.parse(that.streamUrl);
            baseUrl = parsedUrl.href.substring(0, parsedUrl.href.lastIndexOf('/'));
            playlistName = path.basename(parsedUrl.pathname);
            var playlistDestPath = path.join(that.tmpM3u8Folder, playlistName);
            that.logger.debug( "Temp playlist folder located at: %s" , playlistDestPath);
            that.logger.debug( "Reading current M3U8 from %s" , playlistDestPath);
            return that.m3u8HttpUtils.downloadFile(that.streamUrl, playlistDestPath).then(function () {
                that._lastM3U8Time=new Date();
                return playlistDestPath;
            });
        };

        var updateListOfNewChunksToProcess = function (downloadedM3u8) {
            var newChunks = _.chain(downloadedM3u8.items.PlaylistItem)
                .filter(function (item) {
                    return !that.downloadedChunks[item.get('uri')] && !that.chunksToDownload[item.get('uri')];
                }).value();

            // update that these new chunks need to be downloaded
            _.each(newChunks, function (c) {
                that.logger.debug('Adding ' + c.get('uri') + ' to the list of chunks to download');
                that.chunksToDownload[c.get('uri')] = true;
            });

            if (Object.keys(that.chunksToDownload).length >= maxAmountOfDownloadPendingChunks)
            {
                // This shouldn't happen - stop this downloader
                that.logger.error("Chunk download list size is " + maxAmountOfDownloadPendingChunks + " - this shouldn't happen. Stopping the downloader for this flavor");
                that.stop().catch(function(err){
                    that.logger.error("Failed to stop: " + err);
                });
            }

            return newChunks;
        };

        var processObsoleteChunks = function (results) {
            // Try to delete files that were removed from the manifest
            // The last promise is the one for the update manifest request
            var manifestUpdatePromiseResult = _.last(results);
            if (manifestUpdatePromiseResult.state === 'fulfilled') {
                // Delete the obsolete chunks from disk
                _.each(manifestUpdatePromiseResult.value.removedChunks, function (itemToDelete) {
                    // Delete on a best effort basis
                    var pathToDelete = path.join(that.destFolderPath, itemToDelete.get('uri'));
                    qfs.remove(pathToDelete)
                        .then(function () {
                            that.logger.info('Successfully deleted chunk ' + itemToDelete.get('uri'));
                        })
                        .catch(function () {
                            that.logger.info('Error deleting a chunk which was removed from a manifest - ' + itemToDelete.get('uri'));
                        });
                });

                // Ensure that chunks removed from the intermediate manifest are not in the chunksToDownload list
                // (as they will never be processed)
                _.each(manifestUpdatePromiseResult.value.bumpedChunks, function (chunk) {
                    var chunkName = chunk.get('uri');
                    if (that.chunksToDownload[chunkName]) {
                        that.logger.error("Removing chunk " + chunkName + " from chunksToDownload - this shouldn't happen");
                        delete that.chunksToDownload[chunkName];
                    }
                });

                // Remove the obsolete chunks' reference from persistence
                _.each(manifestUpdatePromiseResult.value.removedChunks, function (chunk) {
                    that.logger.debug('Delete ' + chunk.get('uri') + ' from the list of chunks to download');
                    delete that.downloadedChunks[chunk.get('uri')];
                });
            }
        };

        var concludeOperation = function (results) {
            var rejected = _.filter(results, function (p) {
                return (p.state !== 'fulfilled');
            });
            if (rejected.length > 0) {
                var errorsString = "";
                _.forEach(rejected, function (r) {
                    errorsString += r.reason + "\n" + r.reason.stack + "\n";
                });
                throw new Error(errorsString);
            }
            return Q.resolve();
        };

        return Q.fcall(function () {
            return downloadFlavorManifest()
                .then(function finishedDownload(playlistDestPath) {
                    return m3u8Handler.parseM3U8(playlistDestPath)
                        .then(function (m3u) {
                            that.logger.debug( "Received M3U8: \n%s" , m3u);
                            return m3u;
                        });
                }).then(function (m3u) {
                    return updateListOfNewChunksToProcess(m3u);
                }).then(function (newChunks) {
                    //generate updated m3u8
                    var m3u8generatorPromise = that.m3u8generator.update(newChunks);

                    // Download chunks - all the chunks that were pending download beforehand along with the new chunks
                    var downloadChunksPromise = downloadChunks(that, baseUrl, Object.keys(that.chunksToDownload));
                    return Q.allSettled([downloadChunksPromise, m3u8generatorPromise]);
                }).then(function (results) {
                    processObsoleteChunks(results); // Trigger deletion on a best-effort basis
                    return results;
                }).then(function (results) {
                    return concludeOperation(results);
                }).catch(function (err) {
                    if (err==httpUtils.HttpDownloadNotChanged) {
                        return Q.resolve();
                    } else {
                        return Q.reject(err);
                    }
                });
        });
    };

    FlavorDownloader.prototype.start = function (onIterationEndCallback) {
        var that = this;
        that.logger.info("Flavor start requested");
        that.logger.info("Starting flavor downloader - url: %s destination folder: %s" , this.streamUrl , this.destFolderPath);
        that.logger.debug("Polling interval: %s " , this.pollingInterval);

        //create tmp destination folder for media-server m3u8 files
        return qfs.makeTree(this.tmpM3u8Folder)
            .then(function() {
                return that.m3u8generator.init();
            })
            .then(function(){
                var pGlob = Q.denodeify(glob);
                return pGlob(path.join(that.destFolderPath, '*.ts')).then(function(downloadedChunks){
                    _.each(downloadedChunks, function(c){
                        that.downloadedChunks[path.basename(c)] = true;
                    });
                });
            })
            .then(function(){
                return downloadIndefinitely.call(that, onIterationEndCallback);
            })
            .then(function(){
                FlavorDownloader.activeInstances[that.id] = that.entryId + "_" + that.flavor;
                that.runStatus = 'running';
            })
            .catch(function(err){
                that.logger.error("Error occurred while starting flavor downloader: " + err);
                err.flavor = that.flavor;
                throw err;
            });
    };

    FlavorDownloader.prototype.stop = function () {
        var that = this;
        if (that.stopPromise)
        {
            // Stop already requested
            that.logger.info('stop requested - ignoring request as this flavor downloader either already stopped or is currently stopping');
            return that.stopPromise;
        }

        var d = Q.defer();
        that.stopPromise = d.promise;
        that.stopPromise.then(function(){
            that.runStatus = 'stopped';
            that.emit("stopped", that.flavor);
        });

        var teardown = Q();
        if (that.runStatus === 'running')
        {
            var teardownInterval = config.get("flavorDownloaderTeardownInterval");
            that.logger.info('stop requested - allowing ' + teardownInterval + 'ms for orderly tear-down');
            teardown = Q.delay(teardownInterval).then(function(){
                that.logger.info('stopping flavor downloader after allowing orderly tear-down');
            });
        }

        return teardown.then(function(){
            that.logger.info('stopping flavor downloader');
            that.shouldRun = false;
            downloaderEnd.promise.then(function(){
                that.logger.info("Stopped");
                delete FlavorDownloader.activeInstances[that.id];
                d.resolve();
            });

            return d.promise;
        });
    };

    FlavorDownloader.activeInstances = {};
    FlavorDownloader.uniqueId = 0;
    return FlavorDownloader;
})();

module.exports = FlavorDownloader;<|endoftext|>


<commit_before><commit_msg>Update jsonmvc-schema errors.<commit_after><|endoftext|>