mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
fix(audio): don't restart playback on device change when engine is paused
Defence-in-depth for #1094: the device-changed/-reset handlers restarted
playback based on the UI `isPlaying` flag alone, which can be stale or
desynced at the moment of a device change. Gate the restart on the
engine-paused flag as well (`isPlaying && !getIsAudioPaused()`), so a
paused engine never auto-restarts regardless of how `isPlaying` got set;
when paused it just resets for the cold path. Adds a regression test.
(cherry picked from commit 588dd8c48d)
This commit is contained in:
@@ -18,6 +18,7 @@ import { makeTrack } from '@/test/helpers/factories';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { getSeekFallbackVisualTarget, setSeekFallbackVisualTarget } from '@/store/seekFallbackState';
|
||||
import { setIsAudioPaused } from '@/store/engineState';
|
||||
import { useAudioDeviceBridge } from './useAudioDeviceBridge';
|
||||
|
||||
const track = makeTrack({ id: 't1', duration: 300 });
|
||||
@@ -29,6 +30,8 @@ function mountBridge() {
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
setSeekFallbackVisualTarget(null);
|
||||
// Module-level engine flag isn't covered by resetAllStores — reset explicitly.
|
||||
setIsAudioPaused(false);
|
||||
// Default: a track is playing.
|
||||
usePlayerStore.setState({ currentTrack: track, isPlaying: true });
|
||||
});
|
||||
@@ -92,6 +95,19 @@ describe('audio:device-changed', () => {
|
||||
|
||||
expect(playTrack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not restart when the engine is paused even if isPlaying is stale-true (#1094)', () => {
|
||||
const playTrack = vi.fn();
|
||||
const resetAudioPause = vi.fn();
|
||||
usePlayerStore.setState({ playTrack, resetAudioPause, isPlaying: true } as never);
|
||||
setIsAudioPaused(true);
|
||||
mountBridge();
|
||||
|
||||
emitTauriEvent('audio:device-changed', 45.0);
|
||||
|
||||
expect(playTrack).not.toHaveBeenCalled();
|
||||
expect(resetAudioPause).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── audio:device-reset ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -3,6 +3,7 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { setSeekFallbackVisualTarget } from '../../store/seekFallbackState';
|
||||
import { getIsAudioPaused } from '../../store/engineState';
|
||||
|
||||
/** Audio output device lifecycle: device switches (Bluetooth headphones, USB
|
||||
* DAC, …) and pinned-device-unplugged fallbacks emitted by the Rust
|
||||
@@ -23,7 +24,10 @@ export function useAudioDeviceBridge() {
|
||||
const resumeAt = typeof event.payload === 'number' ? event.payload : 0;
|
||||
const { currentTrack, isPlaying, playTrack, resetAudioPause } = usePlayerStore.getState();
|
||||
if (!currentTrack) return;
|
||||
if (isPlaying) {
|
||||
// Only restart playback when transport is *provably* active. `isPlaying`
|
||||
// alone can be stale/desynced on a device change (#1094); the engine-paused
|
||||
// flag is the source of truth — if paused, just reset for the cold path.
|
||||
if (isPlaying && !getIsAudioPaused()) {
|
||||
if (resumeAt > 0.5 && currentTrack.duration > 0) {
|
||||
setSeekFallbackVisualTarget({
|
||||
trackId: currentTrack.id,
|
||||
@@ -55,7 +59,10 @@ export function useAudioDeviceBridge() {
|
||||
const resumeAt = typeof event.payload === 'number' ? event.payload : 0;
|
||||
const { currentTrack, isPlaying, playTrack, resetAudioPause } = usePlayerStore.getState();
|
||||
if (!currentTrack) return;
|
||||
if (isPlaying) {
|
||||
// Only restart playback when transport is *provably* active. `isPlaying`
|
||||
// alone can be stale/desynced on a device change (#1094); the engine-paused
|
||||
// flag is the source of truth — if paused, just reset for the cold path.
|
||||
if (isPlaying && !getIsAudioPaused()) {
|
||||
if (resumeAt > 0.5 && currentTrack.duration > 0) {
|
||||
setSeekFallbackVisualTarget({
|
||||
trackId: currentTrack.id,
|
||||
|
||||
Reference in New Issue
Block a user