var progressTimer; var progressElement = document.getElementById('trackslider'); var positionNode = document.createTextNode(''); var durationNode = document.createTextNode(''); var START_BEATS = 5 // 0.5 seconds, needs to be less than 1s to prevent unwanted updates. var RUN_BEATS = 300 // 30 seconds assuming default timer update rate of 100ms var callbackHeartbeats = 0; // Timer will check syncs on every n-number of calls. var targetPosition = null; var MAX_SYNCS = 5; // Maximum number of consecutive successful syncs to perform. var syncsLeft = MAX_SYNCS; var synced = false; var consecutiveSyncs = 0; document.getElementById('songelapsed').appendChild(positionNode); document.getElementById('songlength').appendChild(durationNode); function timerCallback(position, duration, isRunning) { updateTimers(position, duration, isRunning); if (callbackHeartbeats == 0) { callbackHeartbeats = getHeartbeat(); } if (mopidy && position > 0) { // Mopidy and timer are both initialized. if (callbackHeartbeats-- == 1) { // Get time position from Mopidy on every nth callback until // synced. mopidy.playback.getTimePosition().then( function(mopidy_position) { syncTimer(position, mopidy_position); } ); } } } function updateTimers(position, duration, isRunning) { var ready = !(duration == Infinity && position == 0 && !isRunning); // Timer has been properly initialized. var streaming = (duration == Infinity && position > 0); // Playing a stream. var ok = synced && isRunning; // Normal operation. var syncing = !synced && isRunning; // Busy syncing. if (!ready) { //Make sure that default values are displayed while the timer is being initialized. positionNode.nodeValue = ''; durationNode.nodeValue = ''; $("#trackslider").val(0).slider('refresh'); } else { durationNode.nodeValue = format(duration || Infinity); if (syncing) { if (!targetPosition) { // Waiting for Mopidy to provide a target position. positionNode.nodeValue = '(wait)'; } else { // Busy seeking to new target position. positionNode.nodeValue = '(sync)'; } } else if (synced || streaming) { positionNode.nodeValue = format(position);; } } if (ok) { // Don't update the track slider unless it is synced and running. // (prevents awkward 'jitter' animation). $("#trackslider").val(position).slider('refresh'); } } function getHeartbeat() { if (syncsLeft > 0 && callbackHeartbeats == 0) { // Step back exponentially while increasing heartbeat. return Math.round(delay_exponential(5, 2, MAX_SYNCS - syncsLeft)); } else if (syncsLeft == 0 && callbackHeartbeats == 0) { // Sync completed, keep checking using maximum number of heartbeats. return RUN_BEATS; } else { return START_BEATS; } } function syncTimer(current, target) { if (target) { var drift = Math.abs(target - current); if (drift <= 500) { syncsLeft--; // Less than 500ms == in sync. if (++consecutiveSyncs == 2) { // Need at least two consecutive syncs to know that Mopidy // is progressing playback and we are in sync. synced = true; targetPosition = null; consecutiveSyncs = 0; } } else { // Drift is too large, re-sync with Mopidy. reset(); targetPosition = target; progressTimer.set(targetPosition); } } } function toInt(value) { return value.match(/^\w*\d+\w*$/) ? parseInt(value) : null; } function format(milliseconds) { if (milliseconds === Infinity) { return '(n/a)'; } else if (milliseconds == 0) { return ''; } var seconds = Math.floor(milliseconds / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; seconds = seconds < 10 ? '0' + seconds : seconds; return minutes + ':' + seconds; } 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(); } beats = base * (Math.pow(growthFactor, (attempts - 1))); return beats; } function reset() { synced = false; consecutiveSyncs = 0; syncsLeft = MAX_SYNCS; callbackHeartbeats = START_BEATS; targetPosition = null; } function setProgressTimer(pos) { reset(); targetPosition = pos; progressTimer.set(pos); if (!play) { // Set lapsed time and slider position directly as timer callback is not currently // running. positionNode.nodeValue = format(pos); $("#trackslider").val(pos).slider('refresh'); } } function startProgressTimer() { reset(); progressTimer.start(); } function resetProgressTimer() { progressTimer.reset(); reset(); targetPosition = 0; }