mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
refactor(player): E.22 — extract seek-fallback retry + visual target state (#586)
The streaming-fallback seek recovery loop + its visual coverup move
into `src/store/seekFallbackState.ts`:
- 6 module mutables (retry timer + start + target; fallback trackId +
restartAt; visual target)
- 3 const gates (180 ms retry interval, 6 s retry budget, 1.6 s
visual guard)
- `scheduleSeekFallbackRetry(trackId, seconds)` — bounded retry loop
that hits `audio_seek` every 180 ms up to 6 s, re-scheduling on
recoverable errors and clearing the visual target on success or
hard failure
- `clearSeekFallbackRetry()` — cancel timer + reset state
- get/set accessors for the three caller-touched fields
(`SeekFallbackVisualTarget`, trackId, restartAt)
playerStore's ~30 direct-access sites become accessor calls. Inside
the progress handler the visual target is captured into a local once
per call so type narrowing + repeated reads stay clean.
17 focused tests pin the constants, get/set round-trips, the retry
loop's happy path (setSeekTarget + clear visual), recoverable retry
re-schedule, non-recoverable abort, track-change abort, retry-budget
exhaustion, and the three coalesce cases (different track id /
seconds delta > 0.25 s / seconds delta ≤ 0.25 s).
playerStore 2909 → 2869 LOC.
This commit is contained in:
committed by
GitHub
parent
c10eabe114
commit
0eb084c5f8
@@ -0,0 +1,128 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isRecoverableSeekError } from '../utils/seekErrors';
|
||||
import { usePlayerStore } from './playerStore';
|
||||
import { setSeekTarget } from './seekTargetState';
|
||||
|
||||
/**
|
||||
* Streaming-fallback seek recovery + visual coverup.
|
||||
*
|
||||
* The Rust seek pipeline can reject a seek as "not seekable" while the
|
||||
* stream is still settling (sink not bound yet, codec hasn't reported
|
||||
* seekability). Instead of surfacing the failure straight away, this
|
||||
* module schedules bounded retries every 180 ms up to a 6 s ceiling.
|
||||
*
|
||||
* In parallel, `seekFallbackVisualTarget` holds the seekbar at the
|
||||
* requested position so the user doesn't see it snap back to the
|
||||
* pre-seek time while the retry loop is in flight. The progress handler
|
||||
* reads this and prefers the target value until the engine actually
|
||||
* reaches it (within ~2 s) or the 1.6 s visual-guard window elapses.
|
||||
*
|
||||
* `seekFallbackTrackId` + `seekFallbackRestartAt` track the most recent
|
||||
* fallback-restart so a quick subsequent seek on the same track can
|
||||
* decide whether to debounce a full restart vs reuse the loop.
|
||||
*/
|
||||
|
||||
export const SEEK_FALLBACK_VISUAL_GUARD_MS = 1600;
|
||||
export const SEEK_FALLBACK_RETRY_INTERVAL_MS = 180;
|
||||
export const SEEK_FALLBACK_RETRY_MAX_MS = 6000;
|
||||
|
||||
let seekFallbackRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let seekFallbackRetryStartedAt = 0;
|
||||
let seekFallbackRetryTarget: { trackId: string; seconds: number } | null = null;
|
||||
let seekFallbackTrackId: string | null = null;
|
||||
let seekFallbackRestartAt = 0;
|
||||
let seekFallbackVisualTarget: { trackId: string; seconds: number; setAtMs: number } | null = null;
|
||||
|
||||
export function clearSeekFallbackRetry(): void {
|
||||
if (seekFallbackRetryTimer) {
|
||||
clearTimeout(seekFallbackRetryTimer);
|
||||
seekFallbackRetryTimer = null;
|
||||
}
|
||||
seekFallbackRetryStartedAt = 0;
|
||||
seekFallbackRetryTarget = null;
|
||||
}
|
||||
|
||||
export function scheduleSeekFallbackRetry(trackId: string, seconds: number): void {
|
||||
const now = Date.now();
|
||||
if (
|
||||
!seekFallbackRetryTarget
|
||||
|| seekFallbackRetryTarget.trackId !== trackId
|
||||
|| Math.abs(seekFallbackRetryTarget.seconds - seconds) > 0.25
|
||||
) {
|
||||
clearSeekFallbackRetry();
|
||||
seekFallbackRetryStartedAt = now;
|
||||
seekFallbackRetryTarget = { trackId, seconds };
|
||||
} else if (seekFallbackRetryStartedAt === 0) {
|
||||
seekFallbackRetryStartedAt = now;
|
||||
}
|
||||
if (seekFallbackRetryTimer) clearTimeout(seekFallbackRetryTimer);
|
||||
seekFallbackRetryTimer = setTimeout(() => {
|
||||
seekFallbackRetryTimer = null;
|
||||
const target = seekFallbackRetryTarget;
|
||||
const s = usePlayerStore.getState();
|
||||
if (!target || !s.currentTrack || s.currentTrack.id !== target.trackId) {
|
||||
clearSeekFallbackRetry();
|
||||
return;
|
||||
}
|
||||
if (Date.now() - seekFallbackRetryStartedAt > SEEK_FALLBACK_RETRY_MAX_MS) {
|
||||
clearSeekFallbackRetry();
|
||||
seekFallbackVisualTarget = null;
|
||||
return;
|
||||
}
|
||||
invoke('audio_seek', { seconds: target.seconds }).then(() => {
|
||||
setSeekTarget(target.seconds);
|
||||
seekFallbackVisualTarget = null;
|
||||
clearSeekFallbackRetry();
|
||||
}).catch((err: unknown) => {
|
||||
const msg = String(err ?? '');
|
||||
if (!isRecoverableSeekError(msg)) {
|
||||
console.error(err);
|
||||
seekFallbackVisualTarget = null;
|
||||
clearSeekFallbackRetry();
|
||||
return;
|
||||
}
|
||||
scheduleSeekFallbackRetry(target.trackId, target.seconds);
|
||||
});
|
||||
}, SEEK_FALLBACK_RETRY_INTERVAL_MS);
|
||||
}
|
||||
|
||||
export type SeekFallbackVisualTarget = {
|
||||
trackId: string;
|
||||
seconds: number;
|
||||
setAtMs: number;
|
||||
};
|
||||
|
||||
export function getSeekFallbackVisualTarget(): SeekFallbackVisualTarget | null {
|
||||
return seekFallbackVisualTarget;
|
||||
}
|
||||
|
||||
export function setSeekFallbackVisualTarget(target: SeekFallbackVisualTarget | null): void {
|
||||
seekFallbackVisualTarget = target;
|
||||
}
|
||||
|
||||
export function getSeekFallbackTrackId(): string | null {
|
||||
return seekFallbackTrackId;
|
||||
}
|
||||
|
||||
export function setSeekFallbackTrackId(id: string | null): void {
|
||||
seekFallbackTrackId = id;
|
||||
}
|
||||
|
||||
export function getSeekFallbackRestartAt(): number {
|
||||
return seekFallbackRestartAt;
|
||||
}
|
||||
|
||||
export function setSeekFallbackRestartAt(t: number): void {
|
||||
seekFallbackRestartAt = t;
|
||||
}
|
||||
|
||||
/** Test-only: reset every mutable to its initial value. */
|
||||
export function _resetSeekFallbackStateForTest(): void {
|
||||
if (seekFallbackRetryTimer) clearTimeout(seekFallbackRetryTimer);
|
||||
seekFallbackRetryTimer = null;
|
||||
seekFallbackRetryStartedAt = 0;
|
||||
seekFallbackRetryTarget = null;
|
||||
seekFallbackTrackId = null;
|
||||
seekFallbackRestartAt = 0;
|
||||
seekFallbackVisualTarget = null;
|
||||
}
|
||||
Reference in New Issue
Block a user