mopidy-musicbox-webclient/mopidy_musicbox_webclient/static/js/progress_timer.js
2016-02-29 20:22:30 +02:00

177 lines
5.4 KiB
JavaScript

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 '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;
}
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 updatePosition(pos) {
positionNode.nodeValue = format(pos);
}
function startProgressTimer() {
reset();
progressTimer.start();
}
function resetProgressTimer() {
progressTimer.reset();
reset();
targetPosition = 0;
}