From f5e372a70dbec5095d78c977d0f9bba8f84b7f00 Mon Sep 17 00:00:00 2001 From: jcass Date: Sun, 14 Feb 2016 17:09:40 +0200 Subject: [PATCH] fix:Avoid polling for current track and time changes. Fixes #40. --- README.rst | 1 + .../static/css/webclient.css | 9 +- mopidy_musicbox_webclient/static/index.html | 8 +- .../static/js/controls.js | 61 +------ .../static/js/functionsvars.js | 9 - mopidy_musicbox_webclient/static/js/gui.js | 71 ++++---- .../static/js/process_ws.js | 4 +- .../static/js/progress_timer.js | 171 ++++++++++++++++++ .../vendors/media_progress_timer/timer.js | 125 +++++++++++++ 9 files changed, 344 insertions(+), 115 deletions(-) create mode 100644 mopidy_musicbox_webclient/static/js/progress_timer.js create mode 100644 mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js diff --git a/README.rst b/README.rst index 25e2281..b2d08b1 100644 --- a/README.rst +++ b/README.rst @@ -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 `_). **Fixes** diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css index fb85e39..5d9273b 100644 --- a/mopidy_musicbox_webclient/static/css/webclient.css +++ b/mopidy_musicbox_webclient/static/css/webclient.css @@ -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 { diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 6eef6c5..5dbe21c 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -297,10 +297,10 @@
- 0:00 - 0:00 + + - +
@@ -469,6 +469,8 @@ + + diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index 524ca10..12c25ca 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -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 * ************/ diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 86fb55b..4fcd982 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -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']; diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 60938c3..b8a0f6c 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -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('' + str + ''); } -//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) { diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index ef6ec6a..df0513a 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -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); } diff --git a/mopidy_musicbox_webclient/static/js/progress_timer.js b/mopidy_musicbox_webclient/static/js/progress_timer.js new file mode 100644 index 0000000..563737b --- /dev/null +++ b/mopidy_musicbox_webclient/static/js/progress_timer.js @@ -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; +} diff --git a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js new file mode 100644 index 0000000..744e3de --- /dev/null +++ b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js @@ -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; +} + +}()); \ No newline at end of file