mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
6ea0acede5
* feat(playback): stream buffering UI, ranged M4A tail prefetch, demuxer fix Defer seekbar/progress until HTTP stream is armed for both legacy and RangedHttpSource; show buffering overlay on cover art. Add MP4 tail prefetch and Symphonia isomp4 bounded-mdat/moov-at-EOF probing so moov-at-end M4A can start without reading the full mdat. * feat(hot-cache): spill large ranged streams to disk for promote When a ranged HTTP download completes above the 64 MiB RAM promote cap, write the existing buffer once to app-data stream-spill/ and register it for hot-cache promote (rename) and replay via fetch_data. Analysis seeds from the spill file up to the local-file cap (512 MiB). * fix(ui): stream buffering — grayscale cover and static clock icon Desaturate player and queue cover art while isPlaybackBuffering; keep a non-animated clock overlay for visibility without the spinning animation. * fix(playback): review follow-up — tests, i18n, spill cleanup, changelog Clippy and test layout fixes; stream spill orphan cleanup on startup; buffering flag guard in progress handler; bufferingStream in all player locales; CHANGELOG and contributor credits for stream/M4A work. * docs: attribute stream buffering and M4A streaming to PR #737 * test(audio): avoid create_engine in stream spill unit test CI runners have no audio output device; test spill take/consume via the Mutex slot only, matching install_stream_completed_spill tests.
93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
/**
|
|
* 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, buffering: false });
|
|
});
|
|
|
|
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, buffering: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
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, buffering: false,
|
|
});
|
|
expect(cb.mock.calls[0][1]).toEqual({ currentTime: 0, progress: 0, buffered: 0, buffering: false });
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|