Fix tick progression
Deploy HEOS panel / deploy (push) Successful in 27s

This commit is contained in:
franzz committed 2026-09-18 12:29:16 +02:00
1 parent c20f241583
commit 94f4feeac1
1 file changed
+43 -6
+43 -6
View File
@@ -80,6 +80,9 @@ const avrTrack = {
progress: ui.avrProgress, progress: ui.avrProgress,
// Only a Zidoo ever fills this card's now-playing in, and it can always seek. // Only a Zidoo ever fills this card's now-playing in, and it can always seek.
canSeek: () => true, 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, seek: null,
}; };
// Same reasoning as a room's own cover: drop one that will not load rather // 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 // HEOS cannot seek, so only a stream Spotify can be asked to seek in: the
// same one the transport buttons show for (see paintRoom). // same one the transport buttons show for (see paintRoom).
canSeek: () => room.onSpotify && room.spotifyAccount && !room.grouped, 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 seek: null, // {ms, stage, ...} from a drag of the cursor until the player catches up
}; };
rooms[key] = room; rooms[key] = room;
@@ -248,27 +252,42 @@ function renderTrack(track, refs) {
{position_ms, duration_ms} the poll hands back -- absent for anything {position_ms, duration_ms} the poll hands back -- absent for anything
HEOS never sends a progress event for (an AVR input, an internet radio 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 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) { function renderProgress(track, refs) {
const { progress } = refs; const { progress } = refs;
progress.hidden = !track; progress.hidden = !track;
refs.shown = null; refs.shown = null;
refs.tick = null;
if (!track) return; if (!track) return;
const { duration_ms: duration } = track; const { duration_ms: duration } = track;
const position = seekPosition(track, refs); const position = seekPosition(track, refs);
const known = typeof duration === 'number' && duration > 0 && typeof position === 'number'; 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) { if (known) {
refs.shown = track; refs.shown = track;
refs.shownMs = position; 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('known', known);
progress.classList.toggle('seekable', known && Boolean(refs.canSeek())); progress.classList.toggle('seekable', known && Boolean(refs.canSeek()));
progress.style.setProperty('--progress', percent); 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 = { const labels = {
elapsed: known ? '+' + clock(elapsed) : '', elapsed: known ? '+' + clock(elapsed) : '',
remaining: known ? '−' + clock(total - elapsed) : '', remaining: known ? '−' + clock(total - elapsed) : '',
@@ -282,6 +301,24 @@ function renderProgress(track, refs) {
el('[data-role="progress-total"]', progress).textContent = known ? clock(total) : ''; 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 /* 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 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 rather than jumping to where it lands, so grabbing a time off-centre never