Merge pull request #159 from jcass77/fix/40_avoid_polling

Avoid polling for current track and time changes.
This commit is contained in:
John Cass 2016-02-14 17:16:52 +02:00
commit 985a853ca3
10 changed files with 347 additions and 116 deletions

View File

@ -69,6 +69,7 @@ v2.2.0 (UNRELEASED)
- Remove unused iScroll libraries and references.
- Remove unused jQuery.Mobile.iScrollView libraries and references.
- Remove unused jQuery.Truncate libraries and references.
- Avoid polling for current track and time changes. (Fixes: `#40 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/40>`_).
**Fixes**

View File

@ -179,11 +179,6 @@
display: none !important;
}
#songelapsed, #songlength {
font-size: 10px;
margin-top: 12px;
}
/************************
* Volume Slider
***********************/
@ -432,10 +427,14 @@ a {
.pull-right {
float: right;
font-size: 10px;
margin-top: 12px;
}
.pull-left {
float: left;
font-size: 10px;
margin-top: 12px;
}
.hidden, #allresultloader, .loader {

View File

@ -297,10 +297,10 @@
</div>
<div id="slidercontainer"><!-- slider for track position -->
<span id="songelapsed" class="pull-left"> 0:00 </span>
<span id="songlength" class="pull-right"> 0:00 </span>
<span id="songelapsed" class="pull-left"></span>
<span id="songlength" class="pull-right"></span>
<label for="trackslider" disabled="disabled" class="ui-hidden-accessible">Position</label>
<input id="trackslider" name="trackslider" data-mini="true" type="range" onchange="doSeekPos(this.value);" />
<input id="trackslider" name="trackslider" data-mini="true" type="range" />
</div>
</div>
<!-- /nowplaying -->
@ -469,6 +469,8 @@
<!-- /page one -->
<script type="text/javascript" src="../mopidy/mopidy.min.js"></script>
<script src="vendors/jquery_cookie/jquery.cookie.js"></script>
<script src="vendors/media_progress_timer/timer.js"></script>
<script src="js/progress_timer.js"></script>
<script src="js/controls.js"></script>
<script src="js/library.js"></script>
<script src="js/functionsvars.js"></script>

View File

@ -260,11 +260,14 @@ function setPlayState(nwplay) {
$("#btplayNowPlaying").attr('title', 'Pause');
$("#btplay >i").removeClass('fa-play').addClass('fa-pause');
$("#btplay").attr('title', 'Pause');
mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
startProgressTimer();
} else {
$("#btplayNowPlaying >i").removeClass('fa-pause').addClass('fa-play');
$("#btplayNowPlaying").attr('title', 'Play');
$("#btplay >i").removeClass('fa-pause').addClass('fa-play');
$("#btplay").attr('title', 'Play');
progressTimer.stop();
}
play = nwplay;
}
@ -357,40 +360,13 @@ function doSingle() {
* Use a timer to prevent looping of commands *
***********************************************/
function doSeekPos(value) {
var val = $("#trackslider").val();
newposition = Math.round(val);
if (!initgui) {
pausePosTimer();
//set timer to not trigger it too much
clearTimeout(seekTimer);
$("#songelapsed").html(timeFromSeconds(val / 1000));
seekTimer = setTimeout(triggerPos, 500);
}
}
function triggerPos() {
if (mopidy) {
posChanging = true;
mopidy.playback.seek({'time_position': newposition});
resumePosTimer();
posChanging = false;
mopidy.playback.seek({'time_position': Math.round(value)});
}
}
function setPosition(pos) {
if (posChanging) {
return;
}
var oldval = initgui;
if (pos > songlength) {
pos = songlength;
pausePosTimer();
}
currentposition = pos;
initgui = true;
$("#trackslider").val(currentposition).slider('refresh');
initgui = oldval;
$("#songelapsed").html(timeFromSeconds(currentposition / 1000));
setProgressTimer(pos);
}
/***********************************************
@ -430,33 +406,6 @@ function doMute() {
mopidy.mixer.setMute({'mute': !mute});
}
/**************************
* Track position timer *
**************************/
//timer function to update interface
function updatePosTimer() {
currentposition += TRACK_TIMER;
setPosition(currentposition);
}
function resumePosTimer() {
pausePosTimer();
if (songlength > 0) {
posTimer = setInterval(updatePosTimer, TRACK_TIMER);
}
}
function initPosTimer() {
pausePosTimer();
// setPosition(0);
resumePosTimer();
}
function pausePosTimer() {
clearInterval(posTimer);
}
/************
* Stream *
************/

View File

@ -15,11 +15,8 @@ var single;
var currentVolume = -1;
var mute;
var volumeChanging = false;
var posChanging = false;
var posTimer;
var volumeTimer;
var seekTimer;
var initgui = true;
var currentpos = 0;
var popupData = {};
@ -74,12 +71,6 @@ PLAY_NOW_SEARCH = 5;
MAX_TABLEROWS = 50;
//update track slider timer, milliseconds
TRACK_TIMER = 1000;
//check status timer, every 5 or 10 sec
STATUS_TIMER = 10000;
// the first part of Mopidy extensions which serve radio streams
var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict'];

View File

@ -7,19 +7,17 @@
* Song Info Sreen *
********************/
function resetSong() {
if (!posChanging) {
pausePosTimer();
setPlayState(false);
setPosition(0);
var data = new Object;
data.tlid = -1;
data.track = new Object;
data.track.name = '';
data.track.artists = '';
data.track.length = 0;
data.track.uri = ' ';
setSongInfo(data);
}
resetProgressTimer();
setPlayState(false);
setPosition(0);
var data = new Object;
data.tlid = -1;
data.track = new Object;
data.track.name = '';
data.track.artists = '';
data.track.length = 0;
data.track.uri = ' ';
setSongInfo(data);
}
function resizeMb() {
@ -104,18 +102,18 @@ function setSongInfo(data) {
songdata = data;
setSongTitle(data.track.name, false);
songlength = Infinity;
if (!data.track.length || data.track.length == 0) {
songlength = 0;
$("#songlength").html('');
pausePosTimer();
resetProgressTimer();
$('#trackslider').next().find('.ui-slider-handle').hide();
$('#trackslider').slider('disable');
// $('#streamnameinput').val(data.track.name);
// $('#streamuriinput').val(data.track.uri);
} else {
songlength = data.track.length;
$("#songlength").html(timeFromSeconds(data.track.length / 1000));
$('#trackslider').slider('enable');
$('#trackslider').next().find('.ui-slider-handle').show();
}
var arttmp = '';
@ -143,7 +141,8 @@ function setSongInfo(data) {
$("#modalartist").html(arttmp);
$("#trackslider").attr("min", 0);
$("#trackslider").attr("max", data.track.length);
$("#trackslider").attr("max", songlength);
progressTimer.reset().set(0, songlength);
resizeMb();
}
@ -248,15 +247,8 @@ function initSocketevents() {
mopidy.on("event:optionsChanged", updateOptions);
mopidy.on("event:trackPlaybackStarted", function(data) {
mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
setPlayState(true);
setSongInfo(data.tl_track);
initPosTimer();
});
mopidy.on("event:trackPlaybackPaused", function(data) {
pausePosTimer();
setPlayState(false);
setPlayState(true);
});
mopidy.on("event:playlistsLoaded", function(data) {
@ -276,12 +268,11 @@ function initSocketevents() {
mopidy.on("event:playbackStateChanged", function(data) {
switch (data["new_state"]) {
case "paused":
case "stopped":
resetSong();
setPlayState(false);
break;
case "playing":
mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
resumePosTimer();
setPlayState(true);
break;
}
@ -293,6 +284,9 @@ function initSocketevents() {
mopidy.on("event:seeked", function(data) {
setPosition(parseInt(data["time_position"]));
if (play) {
startProgressTimer();
}
});
mopidy.on("event:streamTitleChanged", function(data) {
@ -354,13 +348,6 @@ function setHeadline(site){
$('#contentHeadline').html('<a href="#home" onclick="switchContent(\'home\'); return false;">' + str + '</a>');
}
//update timer
function updateStatusTimer() {
mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error);
mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
//TODO check offline?
}
//update tracklist options.
function updateOptions() {
mopidy.tracklist.getRepeat().then(processRepeat, console.error);
@ -483,6 +470,11 @@ $(document).ready(function(event) {
//initialize events
initSocketevents();
progressTimer = new ProgressTimer({
callback: timerCallback,
//updateRate: 2000,
});
resetSong();
if (location.hash.length < 2) {
@ -492,9 +484,6 @@ $(document).ready(function(event) {
initgui = false;
window.onhashchange = locationHashChanged;
//update gui status every x seconds from mopdidy
setInterval(updateStatusTimer, STATUS_TIMER);
//only show backbutton if in UIWebview
if (window.navigator.standalone) {
$("#btback").show();
@ -581,6 +570,10 @@ $(document).ready(function(event) {
$("#panel").panel("close");
event.stopImmediatePropagation(); }
} );
$( "#trackslider" ).on( "slidestart", function() { progressTimer.stop(); } )
$( "#trackslider" ).on( "slidestop", function() { doSeekPos( $(this).val() ); } );
});
function updatePlayIcons (uri, tlid) {

View File

@ -60,8 +60,7 @@ function processSingle(data) {
* process results of current position
*********************************************************/
function processCurrentposition(data) {
var pos = parseInt(data);
setPosition(pos);
setPosition(parseInt(data));
}
/********************************************************
@ -70,7 +69,6 @@ function processCurrentposition(data) {
function processPlaystate(data) {
if (data == 'playing') {
setPlayState(true);
resumePosTimer();
} else {
setPlayState(false);
}

View File

@ -0,0 +1,171 @@
var progressTimer;
var progressElement = document.getElementById('trackslider');
var positionNode = document.createTextNode('0:00');
var durationNode = document.createTextNode('0:00');
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 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 (streaming) {
positionNode.nodeValue = format(position);;
durationNode.nodeValue = '(n/a)';
} else if (synced) {
positionNode.nodeValue = format(position);
durationNode.nodeValue = format(duration || 0);
}
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 '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 startProgressTimer() {
reset();
progressTimer.start();
}
function resetProgressTimer() {
progressTimer.reset();
reset();
targetPosition = 0;
}

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# 2016-02-14:v1
# 2016-02-14:v2
NETWORK:
*
@ -27,6 +27,7 @@ js/gui.js
js/images.js
js/library.js
js/process_ws.js
js/progress_timer.js
mb.manifest
system.html
vendors/font_awesome/css/font-awesome.css
@ -100,3 +101,4 @@ vendors/jquery_mobile_flat_ui_theme/jquery.mobile.flatui.min.css
vendors/lastfm/lastfm.api.cache.js
vendors/lastfm/lastfm.api.js
vendors/lastfm/lastfm.api.md5.js
vendors/media_progress_timer/timer.js

View File

@ -0,0 +1,125 @@
/*! timer.js v2.0.2
* https://github.com/adamcik/media-progress-timer
* Copyright (c) 2015 Thomas Adamcik
* Licensed under the Apache License, Version 2.0 */
(function() {
'use strict';
var now = typeof window.performance !== 'undefined' &&
typeof window.performance.now !== 'undefined' &&
window.performance.now.bind(window.performance) || Date.now ||
function() { return new Date().getTime(); };
function ProgressTimer(options) {
if (!(this instanceof ProgressTimer)) {
return new ProgressTimer(options);
} else if (typeof options === 'function') {
options = {'callback': options};
} else if (typeof options !== 'object') {
throw 'ProgressTimer must be called with a callback or options.';
} else if (typeof options.callback !== 'function') {
throw 'ProgressTimer needs a callback to operate.';
}
this._running = false;
this._updateRate = Math.max(options.updateRate || 100, 10);
this._callback = options.callback;
this._fallback = typeof window.requestAnimationFrame === 'undefined' ||
options.disableRequestAnimationFrame|| false;
if (!this._fallback) {
this._callUpdate = this._scheduleAnimationFrame;
this._scheduleUpdate = this._scheduleAnimationFrame;
}
this._boundCallUpdate = this._callUpdate.bind(this);
this._boundUpdate = this._update.bind(this);
this.reset();
}
ProgressTimer.prototype.set = function(position, duration) {
if (arguments.length === 0) {
throw 'set requires at least a position argument.';
} else if (arguments.length === 1) {
duration = this._state.duration;
} else {
duration = Math.floor(
Math.max(duration === null ? Infinity : duration || 0, 0));
}
position = Math.floor(Math.min(Math.max(position || 0, 0), duration));
this._state = {
initialTimestamp: null,
initialPosition: position,
previousPosition: position,
duration: duration
};
this._callback(position, duration, this._running);
return this;
};
ProgressTimer.prototype.start = function() {
this._running = true;
this._callUpdate();
return this;
};
ProgressTimer.prototype.stop = function() {
this._running = false;
var state = this._state;
return this.set(state.previousPosition, state.duration);
};
ProgressTimer.prototype.reset = function() {
this._running = false;
return this.set(0, Infinity);
};
ProgressTimer.prototype._callUpdate = function() {
this._update(now());
};
ProgressTimer.prototype._scheduleUpdate = function(timestamp) {
var adjustedTimeout = timestamp + this._updateRate - now();
setTimeout(this._boundCallUpdate, adjustedTimeout);
};
ProgressTimer.prototype._scheduleAnimationFrame = function() {
window.requestAnimationFrame(this._boundUpdate);
};
ProgressTimer.prototype._update = function(timestamp) {
if (!this._running) {
return;
}
var state = this._state;
state.initialTimestamp = state.initialTimestamp || timestamp;
var position = state.initialPosition + timestamp - state.initialTimestamp;
var duration = state.duration;
if (position < duration || duration === null) {
var delta = position - state.previousPosition;
if (delta >= this._updateRate || this._fallback) {
this._callback(Math.floor(position), duration, this._running);
state.previousPosition = position;
}
this._scheduleUpdate(timestamp);
} else {
this._running = false;
this._callback(duration, duration, this._running);
}
};
if(typeof module !== 'undefined') {
module.exports = ProgressTimer;
} else {
window.ProgressTimer = ProgressTimer;
}
}());