mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(preview): sync audio start, ring animation, and download timeout (#423)
* fix(preview): sync audio start, ring animation, and download timeout Three coupled fixes for the track-preview engine: 1. Audio sync. `Sink::try_seek` was running on a worker thread after `sink.append(source)`, so the sink began playing position 0 while the seek was still iterating to the mid-track target. With the 30 s `take_duration` cap counting wall-clock from append, audio could only become audible ~25% into the preview window. The seek now runs on the bare source before append, then `take_duration` wraps it — playback starts at the seek position with the cap measured from there. 2. Ring animation gating. The CSS progress-ring animation was bound to `is-previewing` (set on click), so the ring sprinted ahead of any download/decode/seek warmup and didn't reset cleanly when switching from one preview to another. Added an `audioStarted` flag in `previewStore` that flips on `audio:preview-start` from the engine; CSS animation is now gated on `audio-started` instead. `is-previewing` still drives tooltip/icon for instant click feedback. Same SVG is reused for a 25%-arc rotating loading spinner while waiting for audio, with a 150 ms delay so cached/short previews don't flash. 3. Download timeout. The shared `audio_http_client` caps at 30 s, which aborts mid-download on multi-hundred-MB uncompressed files (e.g. 18-min Hi-Res WAV ~600 MB). The preview engine now builds a dedicated client with a 5 min timeout for the bytes fetch. Watchdog still bounds the playback window at 30 s once the audio actually starts. Touches `audio/preview.rs`, `previewStore.ts`, `components.css` plus the eight tracklist/player-bar components that render the preview button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(changelog): add preview audio sync fix for PR #423 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
9cc74a7f88
commit
297c9f1125
@@ -21,6 +21,15 @@ interface PreviewState {
|
||||
elapsed: number;
|
||||
/** Total preview window in seconds (echoes the duration_sec arg). */
|
||||
duration: number;
|
||||
/**
|
||||
* True only after the engine has emitted `audio:preview-start` for the
|
||||
* current `previewingId` — i.e. audio is actually playing. Drives the
|
||||
* progress-ring animation so the ring doesn't run ahead of the speaker
|
||||
* during the engine's download/decode/seek warmup. Reset to false on every
|
||||
* `startPreview` call so a switch from track A to track B doesn't carry
|
||||
* over A's animation state.
|
||||
*/
|
||||
audioStarted: boolean;
|
||||
|
||||
startPreview: (song: { id: string; title: string; artist: string; coverArt?: string; duration?: number }, location: TrackPreviewLocation) => Promise<void>;
|
||||
stopPreview: () => Promise<void>;
|
||||
@@ -40,6 +49,7 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
previewingTrack: null,
|
||||
elapsed: 0,
|
||||
duration: 30,
|
||||
audioStarted: false,
|
||||
|
||||
startPreview: async (song, location) => {
|
||||
const auth = useAuthStore.getState();
|
||||
@@ -77,6 +87,7 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
previewingTrack: { id: song.id, title: song.title, artist: song.artist, coverArt: song.coverArt },
|
||||
elapsed: 0,
|
||||
duration: previewDuration,
|
||||
audioStarted: false,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -90,7 +101,7 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
} catch (e) {
|
||||
// Roll back optimistic state on failure.
|
||||
if (get().previewingId === song.id) {
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -102,15 +113,19 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
await invoke('audio_preview_stop');
|
||||
} catch {
|
||||
/* engine will emit preview-end anyway; clear locally as fallback */
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
}
|
||||
},
|
||||
|
||||
_onStart: (id) => {
|
||||
if (get().previewingId !== id) {
|
||||
const current = get().previewingId;
|
||||
if (current !== id) {
|
||||
// Engine fired start for an id we didn't track locally — keep id but
|
||||
// leave previewingTrack as-is (the caller's startPreview() set it).
|
||||
set({ previewingId: id, elapsed: 0 });
|
||||
set({ previewingId: id, elapsed: 0, audioStarted: true });
|
||||
} else {
|
||||
// Audio is now actually playing — unblock the progress-ring animation.
|
||||
set({ audioStarted: true });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -121,6 +136,6 @@ export const usePreviewStore = create<PreviewState>((set, get) => ({
|
||||
|
||||
_onEnd: (id) => {
|
||||
if (get().previewingId !== id) return;
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0 });
|
||||
set({ previewingId: null, previewingTrack: null, elapsed: 0, audioStarted: false });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user