text: (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 = '...'
                  break
              case SyncedProgressTimer.SYNC_STATE.SYNCING:
                  // Busy seeking to new target position.
                  this.positionNode.nodeValue = '...'
                  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
  }))
  