mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
refactor(player): E.21 — extract playback-progress throttle timestamps (#585)
Three time-based throttles used by the audio-progress handler move into
`src/store/playbackThrottles.ts`:
- **Live progress emit** (1.5 s / 0.9 s position delta) — feeds the
high-frequency pub/sub
- **Store progress commit** (20 s / 5 s position delta) — writes the
Zustand store
- **Normalization UI update** (120 ms) — live dB readout throttle
Module owns three private mutables, exports five constants (the four
window/delta gates + NORMALIZATION_UI_THROTTLE_MS) and six accessors
(get / mark for each), plus `resetProgressEmitThrottles` for the
track-boundary reset path that handleAudioPlaying calls.
playerStore's six direct-access sites + two reset writes collapse to
imports. Behaviour preserved verbatim.
13 focused tests pin each throttle's get/mark cycle, the partial reset
(progress only, not normalization), and constant values.
playerStore +6 LOC net (more import surface than freed mutables) —
shrinkage will come back on the next bigger encapsulation; this PR's
win is logical separation + testability rather than line count.
This commit is contained in:
committed by
GitHub
parent
6d07720b4f
commit
c10eabe114
@@ -0,0 +1,77 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
LIVE_PROGRESS_EMIT_MIN_DELTA_SEC,
|
||||
LIVE_PROGRESS_EMIT_MIN_MS,
|
||||
NORMALIZATION_UI_THROTTLE_MS,
|
||||
STORE_PROGRESS_COMMIT_MIN_DELTA_SEC,
|
||||
STORE_PROGRESS_COMMIT_MIN_MS,
|
||||
_resetPlaybackThrottlesForTest,
|
||||
getLastLiveProgressEmitAt,
|
||||
getLastNormalizationUiUpdateAtMs,
|
||||
getLastStoreProgressCommitAt,
|
||||
markLiveProgressEmit,
|
||||
markNormalizationUiUpdate,
|
||||
markStoreProgressCommit,
|
||||
resetProgressEmitThrottles,
|
||||
} from './playbackThrottles';
|
||||
|
||||
afterEach(() => {
|
||||
_resetPlaybackThrottlesForTest();
|
||||
});
|
||||
|
||||
describe('constants', () => {
|
||||
it('match the values the runtime expects', () => {
|
||||
expect(LIVE_PROGRESS_EMIT_MIN_MS).toBe(1500);
|
||||
expect(LIVE_PROGRESS_EMIT_MIN_DELTA_SEC).toBe(0.9);
|
||||
expect(STORE_PROGRESS_COMMIT_MIN_MS).toBe(20_000);
|
||||
expect(STORE_PROGRESS_COMMIT_MIN_DELTA_SEC).toBe(5.0);
|
||||
expect(NORMALIZATION_UI_THROTTLE_MS).toBe(120);
|
||||
});
|
||||
});
|
||||
|
||||
describe('throttle accessors', () => {
|
||||
it('all start at 0', () => {
|
||||
expect(getLastLiveProgressEmitAt()).toBe(0);
|
||||
expect(getLastStoreProgressCommitAt()).toBe(0);
|
||||
expect(getLastNormalizationUiUpdateAtMs()).toBe(0);
|
||||
});
|
||||
|
||||
it('mark + get round-trip independently for each throttle', () => {
|
||||
markLiveProgressEmit(1000);
|
||||
markStoreProgressCommit(2000);
|
||||
markNormalizationUiUpdate(3000);
|
||||
expect(getLastLiveProgressEmitAt()).toBe(1000);
|
||||
expect(getLastStoreProgressCommitAt()).toBe(2000);
|
||||
expect(getLastNormalizationUiUpdateAtMs()).toBe(3000);
|
||||
});
|
||||
|
||||
it('mark overwrites a previous value', () => {
|
||||
markLiveProgressEmit(1000);
|
||||
markLiveProgressEmit(2000);
|
||||
expect(getLastLiveProgressEmitAt()).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetProgressEmitThrottles', () => {
|
||||
it('zeros both progress throttles but leaves normalization UI alone', () => {
|
||||
markLiveProgressEmit(1000);
|
||||
markStoreProgressCommit(2000);
|
||||
markNormalizationUiUpdate(3000);
|
||||
resetProgressEmitThrottles();
|
||||
expect(getLastLiveProgressEmitAt()).toBe(0);
|
||||
expect(getLastStoreProgressCommitAt()).toBe(0);
|
||||
expect(getLastNormalizationUiUpdateAtMs()).toBe(3000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_resetPlaybackThrottlesForTest', () => {
|
||||
it('zeros all three timestamps', () => {
|
||||
markLiveProgressEmit(1);
|
||||
markStoreProgressCommit(2);
|
||||
markNormalizationUiUpdate(3);
|
||||
_resetPlaybackThrottlesForTest();
|
||||
expect(getLastLiveProgressEmitAt()).toBe(0);
|
||||
expect(getLastStoreProgressCommitAt()).toBe(0);
|
||||
expect(getLastNormalizationUiUpdateAtMs()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Three time-based throttles used by the audio-progress handler so it
|
||||
* doesn't saturate the WebView2 renderer with redundant emits / setState
|
||||
* commits on a noisy `audio:progress` stream:
|
||||
*
|
||||
* - **Live progress emit** — feeds the high-frequency `playbackProgress`
|
||||
* pub/sub. Gate: 1.5 s elapsed OR ≥0.9 s position delta.
|
||||
* - **Store progress commit** — writes the Zustand player store. Gate:
|
||||
* 20 s elapsed OR ≥5 s position delta. Heavier than the live emit
|
||||
* because subscribers to the store re-render.
|
||||
* - **Normalization UI update** — 120 ms throttle on the live dB readout
|
||||
* written to the store during a track.
|
||||
*
|
||||
* The progress emit + commit pair share `resetProgressEmitThrottles` so
|
||||
* the runtime can force a fresh emit/commit immediately after a track
|
||||
* boundary (`handleAudioPlaying`).
|
||||
*/
|
||||
|
||||
export const LIVE_PROGRESS_EMIT_MIN_MS = 1500;
|
||||
export const LIVE_PROGRESS_EMIT_MIN_DELTA_SEC = 0.9;
|
||||
export const STORE_PROGRESS_COMMIT_MIN_MS = 20_000;
|
||||
export const STORE_PROGRESS_COMMIT_MIN_DELTA_SEC = 5.0;
|
||||
export const NORMALIZATION_UI_THROTTLE_MS = 120;
|
||||
|
||||
let lastLiveProgressEmitAt = 0;
|
||||
let lastStoreProgressCommitAt = 0;
|
||||
let lastNormalizationUiUpdateAtMs = 0;
|
||||
|
||||
export function getLastLiveProgressEmitAt(): number {
|
||||
return lastLiveProgressEmitAt;
|
||||
}
|
||||
|
||||
export function markLiveProgressEmit(nowMs: number): void {
|
||||
lastLiveProgressEmitAt = nowMs;
|
||||
}
|
||||
|
||||
export function getLastStoreProgressCommitAt(): number {
|
||||
return lastStoreProgressCommitAt;
|
||||
}
|
||||
|
||||
export function markStoreProgressCommit(nowMs: number): void {
|
||||
lastStoreProgressCommitAt = nowMs;
|
||||
}
|
||||
|
||||
export function getLastNormalizationUiUpdateAtMs(): number {
|
||||
return lastNormalizationUiUpdateAtMs;
|
||||
}
|
||||
|
||||
export function markNormalizationUiUpdate(nowMs: number): void {
|
||||
lastNormalizationUiUpdateAtMs = nowMs;
|
||||
}
|
||||
|
||||
/** Reset the two progress throttles — used by `handleAudioPlaying` so the first emit + commit after a track boundary go through immediately. */
|
||||
export function resetProgressEmitThrottles(): void {
|
||||
lastLiveProgressEmitAt = 0;
|
||||
lastStoreProgressCommitAt = 0;
|
||||
}
|
||||
|
||||
/** Test-only: wipe all three timestamps so each spec starts fresh. */
|
||||
export function _resetPlaybackThrottlesForTest(): void {
|
||||
lastLiveProgressEmitAt = 0;
|
||||
lastStoreProgressCommitAt = 0;
|
||||
lastNormalizationUiUpdateAtMs = 0;
|
||||
}
|
||||
+21
-15
@@ -106,6 +106,20 @@ import {
|
||||
setRadioVolume,
|
||||
stopRadio,
|
||||
} from './radioPlayer';
|
||||
import {
|
||||
LIVE_PROGRESS_EMIT_MIN_DELTA_SEC,
|
||||
LIVE_PROGRESS_EMIT_MIN_MS,
|
||||
NORMALIZATION_UI_THROTTLE_MS,
|
||||
STORE_PROGRESS_COMMIT_MIN_DELTA_SEC,
|
||||
STORE_PROGRESS_COMMIT_MIN_MS,
|
||||
getLastLiveProgressEmitAt,
|
||||
getLastNormalizationUiUpdateAtMs,
|
||||
getLastStoreProgressCommitAt,
|
||||
markLiveProgressEmit,
|
||||
markNormalizationUiUpdate,
|
||||
markStoreProgressCommit,
|
||||
resetProgressEmitThrottles,
|
||||
} from './playbackThrottles';
|
||||
|
||||
// Re-export so TauriEventBridge + persistence test keep their existing
|
||||
// `from './playerStore'` imports.
|
||||
@@ -380,7 +394,6 @@ let currentRadioArtistId: string | null = null;
|
||||
// the queue and the next Last.fm/topSongs response could re-add it. Reset
|
||||
// on `setRadioArtistId(other)` and on `clearQueue()`. Issue #500.
|
||||
let radioSessionSeenIds = new Set<string>();
|
||||
let lastNormalizationUiUpdateAtMs = 0;
|
||||
|
||||
/** Reload Rust audio to match a queue-undo snapshot (Zustand alone does not move the engine). */
|
||||
function queueUndoRestoreAudioEngine(opts: {
|
||||
@@ -469,12 +482,6 @@ let seekFallbackVisualTarget: { trackId: string; seconds: number; setAtMs: numbe
|
||||
const SEEK_FALLBACK_VISUAL_GUARD_MS = 1600;
|
||||
const SEEK_FALLBACK_RETRY_INTERVAL_MS = 180;
|
||||
const SEEK_FALLBACK_RETRY_MAX_MS = 6000;
|
||||
const LIVE_PROGRESS_EMIT_MIN_MS = 1500;
|
||||
const LIVE_PROGRESS_EMIT_MIN_DELTA_SEC = 0.9;
|
||||
let lastLiveProgressEmitAt = 0;
|
||||
const STORE_PROGRESS_COMMIT_MIN_MS = 20_000;
|
||||
const STORE_PROGRESS_COMMIT_MIN_DELTA_SEC = 5.0;
|
||||
let lastStoreProgressCommitAt = 0;
|
||||
|
||||
|
||||
function clearSeekFallbackRetry() {
|
||||
@@ -546,8 +553,7 @@ function prefetchLoudnessForEnqueuedTracks(
|
||||
|
||||
function handleAudioPlaying(_duration: number) {
|
||||
setDeferHotCachePrefetch(false);
|
||||
lastLiveProgressEmitAt = 0;
|
||||
lastStoreProgressCommitAt = 0;
|
||||
resetProgressEmitThrottles();
|
||||
usePlayerStore.setState({ isPlaying: true });
|
||||
}
|
||||
|
||||
@@ -606,7 +612,7 @@ function handleAudioProgress(current_time: number, duration: number) {
|
||||
const live = getPlaybackProgressSnapshot();
|
||||
const liveTimeDelta = Math.abs(live.currentTime - displayTime);
|
||||
if (
|
||||
nowLive - lastLiveProgressEmitAt >= LIVE_PROGRESS_EMIT_MIN_MS ||
|
||||
nowLive - getLastLiveProgressEmitAt() >= LIVE_PROGRESS_EMIT_MIN_MS ||
|
||||
liveTimeDelta >= LIVE_PROGRESS_EMIT_MIN_DELTA_SEC ||
|
||||
seekFallbackVisualTarget != null
|
||||
) {
|
||||
@@ -615,7 +621,7 @@ function handleAudioProgress(current_time: number, duration: number) {
|
||||
progress,
|
||||
buffered: 0,
|
||||
});
|
||||
lastLiveProgressEmitAt = nowLive;
|
||||
markLiveProgressEmit(nowLive);
|
||||
}
|
||||
}
|
||||
// Heartbeat: push current position to the server every 15 s while playing so
|
||||
@@ -644,11 +650,11 @@ function handleAudioProgress(current_time: number, duration: number) {
|
||||
const commitDelta = Math.abs(store.currentTime - displayTime);
|
||||
const shouldCommitStore =
|
||||
seekFallbackVisualTarget != null ||
|
||||
nowCommit - lastStoreProgressCommitAt >= STORE_PROGRESS_COMMIT_MIN_MS ||
|
||||
nowCommit - getLastStoreProgressCommitAt() >= STORE_PROGRESS_COMMIT_MIN_MS ||
|
||||
commitDelta >= STORE_PROGRESS_COMMIT_MIN_DELTA_SEC;
|
||||
if (shouldCommitStore) {
|
||||
usePlayerStore.setState({ currentTime: displayTime, progress, buffered: 0 });
|
||||
lastStoreProgressCommitAt = nowCommit;
|
||||
markStoreProgressCommit(nowCommit);
|
||||
}
|
||||
|
||||
// Pre-buffer / pre-chain next track based on preload mode and crossfade.
|
||||
@@ -1002,12 +1008,12 @@ export function initAudioListeners(): () => void {
|
||||
&& prev.normalizationNowDb == null;
|
||||
if (
|
||||
!isFirstNumericGain
|
||||
&& nowMs - lastNormalizationUiUpdateAtMs < 120
|
||||
&& nowMs - getLastNormalizationUiUpdateAtMs() < NORMALIZATION_UI_THROTTLE_MS
|
||||
&& engine === prev.normalizationEngineLive
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastNormalizationUiUpdateAtMs = nowMs;
|
||||
markNormalizationUiUpdate(nowMs);
|
||||
emitNormalizationDebug('event:audio:normalization-state', {
|
||||
trackId: usePlayerStore.getState().currentTrack?.id ?? null,
|
||||
payload,
|
||||
|
||||
Reference in New Issue
Block a user