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 iScroll libraries and references.
- Remove unused jQuery.Mobile.iScrollView libraries and references. - Remove unused jQuery.Mobile.iScrollView libraries and references.
- Remove unused jQuery.Truncate 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** **Fixes**

View File

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

View File

@ -297,10 +297,10 @@
</div> </div>
<div id="slidercontainer"><!-- slider for track position --> <div id="slidercontainer"><!-- slider for track position -->
<span id="songelapsed" class="pull-left"> 0:00 </span> <span id="songelapsed" class="pull-left"></span>
<span id="songlength" class="pull-right"> 0:00 </span> <span id="songlength" class="pull-right"></span>
<label for="trackslider" disabled="disabled" class="ui-hidden-accessible">Position</label> <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>
</div> </div>
<!-- /nowplaying --> <!-- /nowplaying -->
@ -469,6 +469,8 @@
<!-- /page one --> <!-- /page one -->
<script type="text/javascript" src="../mopidy/mopidy.min.js"></script> <script type="text/javascript" src="../mopidy/mopidy.min.js"></script>
<script src="vendors/jquery_cookie/jquery.cookie.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/controls.js"></script>
<script src="js/library.js"></script> <script src="js/library.js"></script>
<script src="js/functionsvars.js"></script> <script src="js/functionsvars.js"></script>

View File

@ -260,11 +260,14 @@ function setPlayState(nwplay) {
$("#btplayNowPlaying").attr('title', 'Pause'); $("#btplayNowPlaying").attr('title', 'Pause');
$("#btplay >i").removeClass('fa-play').addClass('fa-pause'); $("#btplay >i").removeClass('fa-play').addClass('fa-pause');
$("#btplay").attr('title', 'Pause'); $("#btplay").attr('title', 'Pause');
mopidy.playback.getTimePosition().then(processCurrentposition, console.error);
startProgressTimer();
} else { } else {
$("#btplayNowPlaying >i").removeClass('fa-pause').addClass('fa-play'); $("#btplayNowPlaying >i").removeClass('fa-pause').addClass('fa-play');
$("#btplayNowPlaying").attr('title', 'Play'); $("#btplayNowPlaying").attr('title', 'Play');
$("#btplay >i").removeClass('fa-pause').addClass('fa-play'); $("#btplay >i").removeClass('fa-pause').addClass('fa-play');
$("#btplay").attr('title', 'Play'); $("#btplay").attr('title', 'Play');
progressTimer.stop();
} }
play = nwplay; play = nwplay;
} }
@ -357,40 +360,13 @@ function doSingle() {
* Use a timer to prevent looping of commands * * Use a timer to prevent looping of commands *
***********************************************/ ***********************************************/
function doSeekPos(value) { 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) { if (mopidy) {
posChanging = true; mopidy.playback.seek({'time_position': Math.round(value)});
mopidy.playback.seek({'time_position': newposition});
resumePosTimer();
posChanging = false;
} }
} }
function setPosition(pos) { function setPosition(pos) {
if (posChanging) { setProgressTimer(pos);
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));
} }
/*********************************************** /***********************************************
@ -430,33 +406,6 @@ function doMute() {
mopidy.mixer.setMute({'mute': !mute}); 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 * * Stream *
************/ ************/

View File

@ -15,11 +15,8 @@ var single;
var currentVolume = -1; var currentVolume = -1;
var mute; var mute;
var volumeChanging = false; var volumeChanging = false;
var posChanging = false;
var posTimer;
var volumeTimer; var volumeTimer;
var seekTimer;
var initgui = true; var initgui = true;
var currentpos = 0; var currentpos = 0;
var popupData = {}; var popupData = {};
@ -74,12 +71,6 @@ PLAY_NOW_SEARCH = 5;
MAX_TABLEROWS = 50; 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 // the first part of Mopidy extensions which serve radio streams
var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict']; var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict'];

View File

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

View File

@ -60,8 +60,7 @@ function processSingle(data) {
* process results of current position * process results of current position
*********************************************************/ *********************************************************/
function processCurrentposition(data) { function processCurrentposition(data) {
var pos = parseInt(data); setPosition(parseInt(data));
setPosition(pos);
} }
/******************************************************** /********************************************************
@ -70,7 +69,6 @@ function processCurrentposition(data) {
function processPlaystate(data) { function processPlaystate(data) {
if (data == 'playing') { if (data == 'playing') {
setPlayState(true); setPlayState(true);
resumePosTimer();
} else { } else {
setPlayState(false); 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 CACHE MANIFEST
# 2016-02-14:v1 # 2016-02-14:v2
NETWORK: NETWORK:
* *
@ -27,6 +27,7 @@ js/gui.js
js/images.js js/images.js
js/library.js js/library.js
js/process_ws.js js/process_ws.js
js/progress_timer.js
mb.manifest mb.manifest
system.html system.html
vendors/font_awesome/css/font-awesome.css 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.cache.js
vendors/lastfm/lastfm.api.js vendors/lastfm/lastfm.api.js
vendors/lastfm/lastfm.api.md5.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;
}
}());