mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
refactor(player): E.4 — extract playback-progress pub/sub into module (#567)
The high-frequency PlaybackProgressSnapshot channel — type, mutable snapshot, listener Set, emit/get/subscribe — moves into a dedicated src/store/playbackProgress.ts. playerStore re-exports the public 3 (getPlaybackProgressSnapshot, subscribePlaybackProgress, the type) so the 5+ external callers (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView, TauriEventBridge) keep their existing imports. Direct unit tests pin the delta short-circuit (`currentTime <0.005`, `progress/buffered <0.0002`) that keeps idle CPU bounded. The end-to-end Tauri-event drive path remains covered by the existing playerStore.progress characterization test (unchanged). playerStore 3516 → 3490 LOC.
This commit is contained in:
committed by
GitHub
parent
d24514d67e
commit
5b89051817
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Direct unit coverage for the playback-progress pub/sub — focused on the
|
||||||
|
* delta-short-circuit behaviour that keeps idle CPU bounded. The end-to-end
|
||||||
|
* "Tauri event → emit → subscriber" path is covered by
|
||||||
|
* `playerStore.progress.test.ts`.
|
||||||
|
*/
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
_resetPlaybackProgressForTest,
|
||||||
|
emitPlaybackProgress,
|
||||||
|
getPlaybackProgressSnapshot,
|
||||||
|
subscribePlaybackProgress,
|
||||||
|
} from './playbackProgress';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
_resetPlaybackProgressForTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPlaybackProgressSnapshot', () => {
|
||||||
|
it('starts at zero', () => {
|
||||||
|
const snap = getPlaybackProgressSnapshot();
|
||||||
|
expect(snap).toEqual({ currentTime: 0, progress: 0, buffered: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the latest snapshot after an emit', () => {
|
||||||
|
emitPlaybackProgress({ currentTime: 42, progress: 0.5, buffered: 0.7 });
|
||||||
|
expect(getPlaybackProgressSnapshot()).toEqual({ currentTime: 42, progress: 0.5, buffered: 0.7 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subscribePlaybackProgress', () => {
|
||||||
|
it('fires the listener with (next, prev) on a meaningful change', () => {
|
||||||
|
const cb = vi.fn();
|
||||||
|
subscribePlaybackProgress(cb);
|
||||||
|
emitPlaybackProgress({ currentTime: 1, progress: 0.1, buffered: 0.2 });
|
||||||
|
expect(cb).toHaveBeenCalledTimes(1);
|
||||||
|
expect(cb.mock.calls[0][0]).toEqual({ currentTime: 1, progress: 0.1, buffered: 0.2 });
|
||||||
|
expect(cb.mock.calls[0][1]).toEqual({ currentTime: 0, progress: 0, buffered: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an unsubscribe that detaches the listener', () => {
|
||||||
|
const cb = vi.fn();
|
||||||
|
const unsub = subscribePlaybackProgress(cb);
|
||||||
|
unsub();
|
||||||
|
emitPlaybackProgress({ currentTime: 1, progress: 0.1, buffered: 0.2 });
|
||||||
|
expect(cb).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fans out to multiple listeners', () => {
|
||||||
|
const a = vi.fn();
|
||||||
|
const b = vi.fn();
|
||||||
|
subscribePlaybackProgress(a);
|
||||||
|
subscribePlaybackProgress(b);
|
||||||
|
emitPlaybackProgress({ currentTime: 1, progress: 0.1, buffered: 0.2 });
|
||||||
|
expect(a).toHaveBeenCalledTimes(1);
|
||||||
|
expect(b).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emitPlaybackProgress delta short-circuit', () => {
|
||||||
|
it('skips emits whose deltas are below thresholds (currentTime <0.005, progress/buffered <0.0002)', () => {
|
||||||
|
const cb = vi.fn();
|
||||||
|
subscribePlaybackProgress(cb);
|
||||||
|
emitPlaybackProgress({ currentTime: 1.0, progress: 0.5, buffered: 0.6 });
|
||||||
|
expect(cb).toHaveBeenCalledTimes(1);
|
||||||
|
// All deltas below the cut-off — must be suppressed.
|
||||||
|
emitPlaybackProgress({ currentTime: 1.001, progress: 0.50005, buffered: 0.60005 });
|
||||||
|
expect(cb).toHaveBeenCalledTimes(1);
|
||||||
|
// currentTime delta crosses the 0.005 threshold — fires.
|
||||||
|
emitPlaybackProgress({ currentTime: 1.01, progress: 0.50005, buffered: 0.60005 });
|
||||||
|
expect(cb).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires when only progress crosses its threshold', () => {
|
||||||
|
const cb = vi.fn();
|
||||||
|
subscribePlaybackProgress(cb);
|
||||||
|
emitPlaybackProgress({ currentTime: 1.0, progress: 0.5, buffered: 0.6 });
|
||||||
|
cb.mockClear();
|
||||||
|
emitPlaybackProgress({ currentTime: 1.0, progress: 0.5005, buffered: 0.6 });
|
||||||
|
expect(cb).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not advance the snapshot when an emit is suppressed', () => {
|
||||||
|
emitPlaybackProgress({ currentTime: 1.0, progress: 0.5, buffered: 0.6 });
|
||||||
|
emitPlaybackProgress({ currentTime: 1.001, progress: 0.5, buffered: 0.6 });
|
||||||
|
expect(getPlaybackProgressSnapshot().currentTime).toBe(1.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* High-frequency playback-progress channel. Decoupled from the main Zustand
|
||||||
|
* store so subscribers (waveform, time labels, lyrics scroller, mini player
|
||||||
|
* mirror) can re-render on every audio tick without invalidating selectors
|
||||||
|
* that watch unrelated player state.
|
||||||
|
*
|
||||||
|
* `emitPlaybackProgress` short-circuits when the next snapshot is within a
|
||||||
|
* sub-perceptible delta of the previous one — keeps idle CPU bounded when
|
||||||
|
* the player is paused or the engine reports identical frames in a row.
|
||||||
|
*/
|
||||||
|
export type PlaybackProgressSnapshot = {
|
||||||
|
currentTime: number;
|
||||||
|
progress: number;
|
||||||
|
buffered: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let playbackProgressSnapshot: PlaybackProgressSnapshot = {
|
||||||
|
currentTime: 0,
|
||||||
|
progress: 0,
|
||||||
|
buffered: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const playbackProgressListeners = new Set<(
|
||||||
|
next: PlaybackProgressSnapshot,
|
||||||
|
prev: PlaybackProgressSnapshot,
|
||||||
|
) => void>();
|
||||||
|
|
||||||
|
export function emitPlaybackProgress(next: PlaybackProgressSnapshot): void {
|
||||||
|
const prev = playbackProgressSnapshot;
|
||||||
|
if (
|
||||||
|
Math.abs(prev.currentTime - next.currentTime) < 0.005 &&
|
||||||
|
Math.abs(prev.progress - next.progress) < 0.0002 &&
|
||||||
|
Math.abs(prev.buffered - next.buffered) < 0.0002
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playbackProgressSnapshot = next;
|
||||||
|
playbackProgressListeners.forEach(cb => cb(next, prev));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlaybackProgressSnapshot(): PlaybackProgressSnapshot {
|
||||||
|
return playbackProgressSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribePlaybackProgress(
|
||||||
|
cb: (next: PlaybackProgressSnapshot, prev: PlaybackProgressSnapshot) => void,
|
||||||
|
): () => void {
|
||||||
|
playbackProgressListeners.add(cb);
|
||||||
|
return () => {
|
||||||
|
playbackProgressListeners.delete(cb);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only: reset module state between specs so suites stay isolated. */
|
||||||
|
export function _resetPlaybackProgressForTest(): void {
|
||||||
|
playbackProgressSnapshot = { currentTime: 0, progress: 0, buffered: 0 };
|
||||||
|
playbackProgressListeners.clear();
|
||||||
|
}
|
||||||
+16
-42
@@ -33,6 +33,22 @@ import {
|
|||||||
import { coerceWaveformBins, waveformBlobLenOk } from '../utils/waveformParse';
|
import { coerceWaveformBins, waveformBlobLenOk } from '../utils/waveformParse';
|
||||||
import { normalizationAlmostEqual } from '../utils/normalizationCompare';
|
import { normalizationAlmostEqual } from '../utils/normalizationCompare';
|
||||||
import { isRecoverableSeekError } from '../utils/seekErrors';
|
import { isRecoverableSeekError } from '../utils/seekErrors';
|
||||||
|
import {
|
||||||
|
emitPlaybackProgress,
|
||||||
|
getPlaybackProgressSnapshot,
|
||||||
|
subscribePlaybackProgress,
|
||||||
|
type PlaybackProgressSnapshot,
|
||||||
|
} from './playbackProgress';
|
||||||
|
|
||||||
|
// Re-export the playback-progress public surface so existing call sites
|
||||||
|
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
|
||||||
|
// TauriEventBridge, plus the progress characterization test) keep their
|
||||||
|
// `from './playerStore'` imports working.
|
||||||
|
export {
|
||||||
|
getPlaybackProgressSnapshot,
|
||||||
|
subscribePlaybackProgress,
|
||||||
|
type PlaybackProgressSnapshot,
|
||||||
|
};
|
||||||
import { getWindowKind } from '../app/windowKind';
|
import { getWindowKind } from '../app/windowKind';
|
||||||
import {
|
import {
|
||||||
_resetQueueUndoStacksForTest,
|
_resetQueueUndoStacksForTest,
|
||||||
@@ -475,48 +491,6 @@ const STORE_PROGRESS_COMMIT_MIN_MS = 20_000;
|
|||||||
const STORE_PROGRESS_COMMIT_MIN_DELTA_SEC = 5.0;
|
const STORE_PROGRESS_COMMIT_MIN_DELTA_SEC = 5.0;
|
||||||
let lastStoreProgressCommitAt = 0;
|
let lastStoreProgressCommitAt = 0;
|
||||||
|
|
||||||
export type PlaybackProgressSnapshot = {
|
|
||||||
currentTime: number;
|
|
||||||
progress: number;
|
|
||||||
buffered: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
let playbackProgressSnapshot: PlaybackProgressSnapshot = {
|
|
||||||
currentTime: 0,
|
|
||||||
progress: 0,
|
|
||||||
buffered: 0,
|
|
||||||
};
|
|
||||||
const playbackProgressListeners = new Set<(
|
|
||||||
next: PlaybackProgressSnapshot,
|
|
||||||
prev: PlaybackProgressSnapshot
|
|
||||||
) => void>();
|
|
||||||
|
|
||||||
function emitPlaybackProgress(next: PlaybackProgressSnapshot): void {
|
|
||||||
const prev = playbackProgressSnapshot;
|
|
||||||
if (
|
|
||||||
Math.abs(prev.currentTime - next.currentTime) < 0.005 &&
|
|
||||||
Math.abs(prev.progress - next.progress) < 0.0002 &&
|
|
||||||
Math.abs(prev.buffered - next.buffered) < 0.0002
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
playbackProgressSnapshot = next;
|
|
||||||
playbackProgressListeners.forEach(cb => cb(next, prev));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPlaybackProgressSnapshot(): PlaybackProgressSnapshot {
|
|
||||||
return playbackProgressSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function subscribePlaybackProgress(
|
|
||||||
cb: (next: PlaybackProgressSnapshot, prev: PlaybackProgressSnapshot) => void,
|
|
||||||
): () => void {
|
|
||||||
playbackProgressListeners.add(cb);
|
|
||||||
return () => {
|
|
||||||
playbackProgressListeners.delete(cb);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Deferred pause / resume — cleared on stop, new track, manual pause/resume. */
|
/** Deferred pause / resume — cleared on stop, new track, manual pause/resume. */
|
||||||
let scheduledPauseTimer: number | null = null;
|
let scheduledPauseTimer: number | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user