Web UI: player progress bar with seek
Follows /api/player's position, moves on locally between polls while playing, seeks on release; disabled when the source can't seek. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4
|
|||||||
- An old, long-idle session can get out of sync with the app: it sends empty Load frames ("No tracks in frame") instead of Pause/Play, so controls do nothing. Quitting and reopening Spotify on the Mac fixes it.
|
- An old, long-idle session can get out of sync with the app: it sends empty Load frames ("No tracks in frame") instead of Pause/Play, so controls do nothing. Quitting and reopening Spotify on the Mac fixes it.
|
||||||
- [x] failover (auto mode): no session or (with `failover_on_pause`) paused for `failover_delay_s` -> HLS; Spotify playing -> Spotify within ~1 s; 30 ms fades; HLS suspended while Spotify plays; cspot is held back (not drained) while it isn't Spotify's turn, so it resumes where it paused. Verified: no session -> HLS, play -> Spotify, pause stays (on_pause off), pause -> HLS after 5 s (on_pause on), play -> Spotify at the paused position.
|
- [x] failover (auto mode): no session or (with `failover_on_pause`) paused for `failover_delay_s` -> HLS; Spotify playing -> Spotify within ~1 s; 30 ms fades; HLS suspended while Spotify plays; cspot is held back (not drained) while it isn't Spotify's turn, so it resumes where it paused. Verified: no session -> HLS, play -> Spotify, pause stays (on_pause off), pause -> HLS after 5 s (on_pause on), play -> Spotify at the paused position.
|
||||||
- Open: `aes67_tx resync` (TX 20-27 ms late) ~11 s after each switch to Spotify (seen 3 times); clicks at switches not yet checked in a recording; HLS start sometimes hits CDN read timeouts.
|
- Open: `aes67_tx resync` (TX 20-27 ms late) ~11 s after each switch to Spotify (seen 3 times); clicks at switches not yet checked in a recording; HLS start sometimes hits CDN read timeouts.
|
||||||
- [ ] /api/player: GET, transport (play/pause/toggle/stop/next/prev), seek and volume done (the app follows). Next: progress bar (seek) in the web UI, then source override/url.
|
- [ ] /api/player: GET, transport (play/pause/toggle/stop/next/prev), seek and volume done (the app follows). Web UI progress bar (seek on release) done. Next: source override/url.
|
||||||
- [ ] 8. Mono sum, gain, polish.
|
- [ ] 8. Mono sum, gain, polish.
|
||||||
|
|
||||||
## Phase 2 (parked)
|
## Phase 2 (parked)
|
||||||
|
|||||||
+19
-3
@@ -39,6 +39,9 @@ pre{overflow-x:auto}
|
|||||||
<button onclick="ctl('toggle')">⏯</button>
|
<button onclick="ctl('toggle')">⏯</button>
|
||||||
<button onclick="ctl('next')">⏭</button>
|
<button onclick="ctl('next')">⏭</button>
|
||||||
<button onclick="ctl('stop')">⏹</button>
|
<button onclick="ctl('stop')">⏹</button>
|
||||||
|
<label><span>Position</span><input id="pos" type="range" min="0" max="0" value="0" step="1000" disabled
|
||||||
|
oninput="seekDrag=true; showPos()" onchange="seekDrag=false; ctl('seek',{ms:+this.value})">
|
||||||
|
<small id="postime"></small></label>
|
||||||
<label><span>Volume</span><input id="vol" type="range" min="0" max="100"
|
<label><span>Volume</span><input id="vol" type="range" min="0" max="100"
|
||||||
onchange="ctl('volume',{value:+this.value})"></label>
|
onchange="ctl('volume',{value:+this.value})"></label>
|
||||||
<label><span>Force source</span><select id="force" onchange="ctl('source',{source:this.value})">
|
<label><span>Force source</span><select id="force" onchange="ctl('source',{source:this.value})">
|
||||||
@@ -237,12 +240,25 @@ const PROJECT = {
|
|||||||
async function ctl(cmd, body) {
|
async function ctl(cmd, body) {
|
||||||
try { showPlayer(await api('POST', '/api/player/' + cmd, body)); } catch {}
|
try { showPlayer(await api('POST', '/api/player/' + cmd, body)); } catch {}
|
||||||
}
|
}
|
||||||
|
// Progress bar: follows the polled position, moves on locally while playing, seeks on release.
|
||||||
|
let np = null, npAt = 0, seekDrag = false;
|
||||||
|
const mmss = ms => `${Math.floor(ms/60000)}:${String(Math.floor(ms/1000)%60).padStart(2,'0')}`;
|
||||||
|
function showPos() {
|
||||||
|
const pos = document.getElementById('pos'), dur = np?.duration_ms;
|
||||||
|
pos.disabled = !(dur && np.can?.seek);
|
||||||
|
if (!seekDrag) {
|
||||||
|
const at = !dur ? 0 : np.state === 'playing' ? Math.min(np.position_ms + Date.now() - npAt, dur) : np.position_ms;
|
||||||
|
pos.max = dur || 0;
|
||||||
|
pos.value = at;
|
||||||
|
}
|
||||||
|
document.getElementById('postime').textContent = dur ? `${mmss(+pos.value)} / ${mmss(dur)}` : '';
|
||||||
|
}
|
||||||
|
setInterval(showPos, 500);
|
||||||
function showPlayer(p) {
|
function showPlayer(p) {
|
||||||
if (!p || !p.state) return;
|
if (!p || !p.state) return;
|
||||||
const t = s => s == null ? '' : `${Math.floor(s/60000)}:${String(Math.floor(s/1000)%60).padStart(2,'0')}`;
|
np = p; npAt = Date.now(); showPos();
|
||||||
document.getElementById('np').textContent =
|
document.getElementById('np').textContent =
|
||||||
`[${p.source}] ${p.state}: ${[p.artist, p.title].filter(Boolean).join(' – ') || '–'}` +
|
`[${p.source}] ${p.state}: ${[p.artist, p.title].filter(Boolean).join(' – ') || '–'}`;
|
||||||
(p.duration_ms ? ` ${t(p.position_ms)} / ${t(p.duration_ms)}` : '');
|
|
||||||
const v = document.getElementById('vol');
|
const v = document.getElementById('vol');
|
||||||
if (document.activeElement !== v) v.value = p.volume;
|
if (document.activeElement !== v) v.value = p.volume;
|
||||||
document.getElementById('force').value = p.forced || 'config';
|
document.getElementById('force').value = p.forced || 'config';
|
||||||
|
|||||||
Reference in New Issue
Block a user