74bae74f9f
test-web / test (push) Failing after 45s
PlaylistCard: per-artist system variants (songs_like_artist) all share one variant tag but exist as one playlist per seed artist; routing their play handler through systemShuffle(variant) hit the wrong playlist (or 404). Detect via seed_artist_id != null and fall through to getPlaylist(playlist.id) for those; still tag the queue with the variant so source attribution stays correct. smoothPosition.svelte.ts: new useSmoothPosition() hook mirrors Android's rememberSmoothPositionMs. player.position only updates ~4 Hz (HTML audio timeupdate), so the seek thumb stepped visibly; $effect resets on each canonical tick / seek / track-change and a rAF loop extrapolates at playback rate between ticks. Wired into both PlayerBar.svelte (mini + expanded seek rows) and now-playing/+page.svelte. Seek input handler still reads the raw range value (not smoothed.value) so user drags stay authoritative.
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import { player } from './store.svelte';
|
|
|
|
/**
|
|
* Reactive smoothed playhead position in seconds. Mirrors Android's
|
|
* `rememberSmoothPositionMs`: the underlying `player.position` updates
|
|
* every ~250 ms (HTML audio `timeupdate` fires ~4 Hz), so reading it
|
|
* directly snaps the scrubber forward in discrete steps. This helper
|
|
* extrapolates per-frame between server ticks so the bar glides at
|
|
* playback rate.
|
|
*
|
|
* The `$effect` re-runs whenever the canonical position / isPlaying /
|
|
* duration changes, which both handles natural ticks (re-sync to the
|
|
* new authoritative value) and seeks (instant jump on user action).
|
|
* The rAF loop advances at wall-clock seconds since the segment
|
|
* started, so as long as the canonical position keeps up with real
|
|
* time the resync on each tick is sub-frame and invisible.
|
|
*
|
|
* Returns a `{ value }` accessor instead of a bare number so callers
|
|
* can read it reactively inside components (Svelte's rune wiring
|
|
* follows the property access).
|
|
*/
|
|
export function useSmoothPosition(): { readonly value: number } {
|
|
let displayed = $state(player.position);
|
|
|
|
$effect(() => {
|
|
// Reset whenever the canonical position changes (server tick,
|
|
// seek, track change) so we anchor to the truth.
|
|
displayed = player.position;
|
|
|
|
if (!player.isPlaying || player.duration <= 0) return;
|
|
|
|
const startReal = performance.now();
|
|
const startPos = player.position;
|
|
const duration = player.duration;
|
|
|
|
let rafId = 0;
|
|
const tick = () => {
|
|
const elapsed = (performance.now() - startReal) / 1000;
|
|
displayed = Math.min(startPos + elapsed, duration);
|
|
rafId = requestAnimationFrame(tick);
|
|
};
|
|
rafId = requestAnimationFrame(tick);
|
|
return () => cancelAnimationFrame(rafId);
|
|
});
|
|
|
|
return {
|
|
get value() {
|
|
return displayed;
|
|
}
|
|
};
|
|
}
|