(function (root, factory) { if (typeof define === 'function' && define.amd) { define([], factory) } else if (typeof module === 'object' && module.exports) { module.exports = factory() } else { root.SyncedProgressTimer = factory() } }(this, function () { 'use strict' function delay_exponential (base, growthFactor, attempts) { /* Calculate number of beats between syncs based on exponential function. The format is:: base * growthFactor ^ (attempts - 1) If ``base`` is set to 'rand' then a random number between 0 and 1 will be used as the base. Base must be greater than 0. */ if (base === 'rand') { base = Math.random() } // console.log(base + ' * (Math.pow(' + growthFactor + ', (' + attempts + ' - 1)) = ' + base * (Math.pow(growthFactor, (attempts - 1)))) return base * (Math.pow(growthFactor, (attempts - 1))) } function SyncedProgressTimer (maxAttempts, mopidy) { if (!(this instanceof SyncedProgressTimer)) { return new SyncedProgressTimer(maxAttempts, mopidy) } this.positionNode = document.createTextNode('') this.durationNode = document.createTextNode('') $('#songelapsed').empty().append(this.positionNode) $('#songlength').empty().append(this.durationNode) this._progressTimer = new ProgressTimer({ // Make sure that the timer object's context is available. callback: $.proxy(this.timerCallback, this) }) this._maxAttempts = maxAttempts this._mopidy = mopidy this._isConnected = false this._mopidy.on('state:online', $.proxy(function () { this._isConnected = true }), this) this._mopidy.on('state:offline', $.proxy(function () { this._isConnected = false }), this) this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED this._isSyncScheduled = false this._scheduleID = null this._syncAttemptsRemaining = this._maxAttempts this._previousSyncPosition = null this._duration = null } SyncedProgressTimer.SYNC_STATE = { NOT_SYNCED: 0, SYNCING: 1, SYNCED: 2 } SyncedProgressTimer.format = function (milliseconds) { if (milliseconds === Infinity) { return '' } else if (milliseconds === 0) { return '0:00' } var seconds = Math.floor(milliseconds / 1000) var minutes = Math.floor(seconds / 60) seconds = seconds % 60 seconds = seconds < 10 ? '0' + seconds : seconds return minutes + ':' + seconds } SyncedProgressTimer.prototype.timerCallback = function (position, duration) { this._update(position, duration) if (this._isSyncScheduled && this._isConnected) { this._doSync(position, duration) } } SyncedProgressTimer.prototype._update = function (position, duration) { switch (this.syncState) { case SyncedProgressTimer.SYNC_STATE.NOT_SYNCED: // Waiting for Mopidy to provide a target position. this.positionNode.nodeValue = '(wait)' break case SyncedProgressTimer.SYNC_STATE.SYNCING: // Busy seeking to new target position. this.positionNode.nodeValue = '(sync)' break case SyncedProgressTimer.SYNC_STATE.SYNCED: this._previousSyncPosition = position this.positionNode.nodeValue = SyncedProgressTimer.format(position) $('#trackslider').val(position).slider('refresh') break } } SyncedProgressTimer.prototype._scheduleSync = function (milliseconds) { // Use an anonymous callback to set a boolean value, which should be faster to // check in the timeout callback than doing another function call. clearTimeout(this._scheduleID) this._isSyncScheduled = false if (milliseconds >= 0) { this._scheduleID = setTimeout($.proxy(function () { this._isSyncScheduled = true }, this), milliseconds) } } SyncedProgressTimer.prototype._doSync = function (position, duration) { var ready = !(duration === Infinity && position === 0) // Timer has been properly initialized. if (!ready) { // Don't try to sync if progress timer has not been initialized yet. return } this._scheduleSync(-1) // Ensure that only one sync process is active at a time. var _this = this _this._mopidy.playback.getTimePosition().then(function (targetPosition) { if (_this.syncState === SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) { _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING } if (Math.abs(targetPosition - position) <= 500) { // Less than 500ms == in sync. _this._syncAttemptsRemaining = Math.max(_this._syncAttemptsRemaining - 1, 0) if (_this._syncAttemptsRemaining < _this._maxAttempts - 1 && _this._previousSyncPosition !== targetPosition) { // Need at least two consecutive syncs to know that Mopidy // is progressing playback and we are in sync. _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED } _this._previousSyncPosition = targetPosition // Step back exponentially while increasing number of callbacks. _this._scheduleSync(delay_exponential(0.25, 2, _this._maxAttempts - _this._syncAttemptsRemaining) * 1000) } else { // Drift is too large, re-sync with Mopidy. _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING _this._syncAttemptsRemaining = _this._maxAttempts _this._previousSyncPosition = null _this._scheduleSync(1000) _this._progressTimer.set(targetPosition) } }) } SyncedProgressTimer.prototype.set = function (position, duration) { if (arguments.length === 0) { throw new Error('"SyncedProgressTimer.set" requires the "position" argument.') } this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED this._syncAttemptsRemaining = this._maxAttempts // Workaround for https://github.com/adamcik/media-progress-timer/issues/3 // This causes the timer to die unexpectedly if the position exceeds // the duration slightly. if (this._duration && this._duration < position) { position = this._duration - 1 } if (arguments.length === 1) { this._progressTimer.set(position) } else { this._duration = duration this._progressTimer.set(position, duration) this.durationNode.nodeValue = SyncedProgressTimer.format(duration) } this.updatePosition(position, duration) $('#trackslider').val(position).slider('refresh') return this } SyncedProgressTimer.prototype.start = function () { this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED this._scheduleSync(0) this._progressTimer.start() return this } SyncedProgressTimer.prototype.stop = function () { this._progressTimer.stop() this._scheduleSync(-1) if (this.syncState !== SyncedProgressTimer.SYNC_STATE.SYNCED && this._previousSyncPosition) { // Timer was busy trying to sync when it was stopped, fallback to displaying the last synced position on screen. this.positionNode.nodeValue = SyncedProgressTimer.format(this._previousSyncPosition) } return this } SyncedProgressTimer.prototype.reset = function () { this.stop() this.set(0, Infinity) return this } SyncedProgressTimer.prototype.updatePosition = function (position) { if (!(this._duration === Infinity && position === 0)) { this.positionNode.nodeValue = SyncedProgressTimer.format(position) } else { this.positionNode.nodeValue = '' } } return SyncedProgressTimer }))