diff --git a/static/app.js b/static/app.js index 304e553..29911f5 100644 --- a/static/app.js +++ b/static/app.js @@ -80,6 +80,9 @@ const avrTrack = { progress: ui.avrProgress, // Only a Zidoo ever fills this card's now-playing in, and it can always seek. canSeek: () => true, + // A film paused on the Zidoo reports its own play_state; nothing else + // ever lands here with a position and duration to tick. + playing: () => avrNowPlaying?.play_state === 'play', seek: null, }; // Same reasoning as a room's own cover: drop one that will not load rather @@ -152,6 +155,7 @@ els('.room').forEach((node) => { // HEOS cannot seek, so only a stream Spotify can be asked to seek in: the // same one the transport buttons show for (see paintRoom). canSeek: () => room.onSpotify && room.spotifyAccount && !room.grouped, + playing: () => room.playState === 'play', seek: null, // {ms, stage, ...} from a drag of the cursor until the player catches up }; rooms[key] = room; @@ -248,27 +252,42 @@ function renderTrack(track, refs) { {position_ms, duration_ms} the poll hands back -- absent for anything HEOS never sends a progress event for (an AVR input, an internet radio stream with no fixed length), in which case the line stays plain. Around - a seek, the position seekPosition() picks shows instead of the one reported. */ + a seek, the position seekPosition() picks shows instead of the one + reported. Also leaves refs.tick pointing at this position and the moment + it was true, for tickProgress() to run forward from until the next call + here -- a poll, a drag, or a seek landing -- corrects it and moves the + mark again. */ function renderProgress(track, refs) { const { progress } = refs; progress.hidden = !track; refs.shown = null; + refs.tick = null; if (!track) return; const { duration_ms: duration } = track; const position = seekPosition(track, refs); const known = typeof duration === 'number' && duration > 0 && typeof position === 'number'; - const percent = known ? Math.min(100, Math.max(0, (position / duration) * 100)) : 0; if (known) { refs.shown = track; refs.shownMs = position; + refs.tick = { ms: position, at: Date.now(), duration }; } + paintProgress(refs, known, position, duration); +} + +/* Just the painting, shared with tickProgress() below so a between-polls + nudge repaints exactly the same way a real sync does. */ +function paintProgress(refs, known, position, duration) { + const { progress } = refs; + // Whole seconds on both sides, so elapsed and remaining always add up to + // the total shown at the end of the line -- and the knob is placed off + // these same whole seconds, not the raw fractional position, so it never + // creeps past where the digits still say it is. + const total = known ? Math.floor(duration / 1000) : 0; + const elapsed = known ? Math.min(total, Math.max(0, Math.floor(position / 1000))) : 0; + const percent = known ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 0; progress.classList.toggle('known', known); progress.classList.toggle('seekable', known && Boolean(refs.canSeek())); progress.style.setProperty('--progress', percent); - // Whole seconds on both sides, so elapsed and remaining always add up to - // the total shown at the end of the line. - const total = known ? Math.floor(duration / 1000) : 0; - const elapsed = known ? Math.min(total, Math.max(0, Math.floor(position / 1000))) : 0; const labels = { elapsed: known ? '+' + clock(elapsed) : '', remaining: known ? '−' + clock(total - elapsed) : '', @@ -282,6 +301,24 @@ function renderProgress(track, refs) { el('[data-role="progress-total"]', progress).textContent = known ? clock(total) : ''; } +/* Between polls the cursor otherwise sits still for POLL_MS at a time and + then jumps -- so this nudges it along at real speed from the last known + position, purely cosmetic until the next renderProgress() (a poll, most + often) corrects it and moves the mark again. Skipped while a seek owns + the display (seekPosition already covers that) or nothing is actually + playing, so a paused cursor stays put. */ +function tickProgress(refs) { + if (!refs.tick || !refs.shown || refs.seek || !refs.playing()) return; + const { ms, at, duration } = refs.tick; + const position = Math.min(duration, ms + (Date.now() - at)); + paintProgress(refs, true, position, duration); +} + +setInterval(() => { + tickProgress(avrTrack); + Object.values(rooms).forEach(tickProgress); +}, 250); + /* Drag the cursor, or either time riding with it, to jump backwards or forwards. Like the volume knob, it moves by how far the finger travels rather than jumping to where it lands, so grabbing a time off-centre never