diag(#1090/#1072): trace device-changed event into the frontend hang

The Rust reopen path returns cleanly (watcher keeps polling), so the UI
hang lives in the frontend after audio:device-changed. Log the emitted
payload (resumed null vs f64) and trace the bridge: event received → store
state → before/after playTrack. Last surviving line localises the hang.
TEMPORARY — revert before the real fix.
This commit is contained in:
Psychotoxical
2026-06-15 20:14:05 +02:00
parent da1ad78fbe
commit 9ead421451
2 changed files with 20 additions and 0 deletions
@@ -93,6 +93,15 @@ pub(crate) async fn reopen_output_stream(
// Falls back gracefully to the frontend path if conditions aren't met.
let resumed = try_resume_after_device_change(app, &snapshot).await;
// [DIAG #1090 — TEMPORARY] What does Rust hand to the frontend? resumed=true
// → null payload (frontend does nothing). resumed=false → f64 → frontend
// calls playTrack. This tells us which frontend branch the hang follows.
crate::app_eprintln!(
"[psysonic] DIAG-1090 reopen returning: resumed={resumed} was_playing={} payload={}",
snapshot.is_playing,
if resumed { "null".to_string() } else { format!("{:?}", snapshot.current_time_secs) }
);
match notify {
ReopenNotify::DeviceChanged => {
// null → Rust already resumed; frontend skips playTrack
@@ -1,9 +1,16 @@
import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '../../store/playerStore';
import { useAuthStore } from '../../store/authStore';
import { setSeekFallbackVisualTarget } from '../../store/seekFallbackState';
// [DIAG #1090 — TEMPORARY] Forward a marker to the Rust debug log (only when
// Settings → Logging = Debug). Used to find where the UI hangs on device switch.
const diag1090 = (message: string) => {
void invoke('frontend_debug_log', { scope: 'diag1090', message }).catch(() => {});
};
/** Audio output device lifecycle: device switches (Bluetooth headphones, USB
* DAC, …) and pinned-device-unplugged fallbacks emitted by the Rust
* device-watcher.
@@ -17,11 +24,13 @@ export function useAudioDeviceBridge() {
useEffect(() => {
let unlisten: (() => void) | undefined;
listen('audio:device-changed', (event) => {
diag1090(`device-changed received payload=${JSON.stringify(event.payload)}`);
// null payload = Rust handled internal replay; nothing to do here.
if (event.payload === null) return;
const resumeAt = typeof event.payload === 'number' ? event.payload : 0;
const { currentTrack, isPlaying, playTrack, resetAudioPause } = usePlayerStore.getState();
diag1090(`state currentTrack=${!!currentTrack} isPlaying=${isPlaying} resumeAt=${resumeAt}`);
if (!currentTrack) return;
if (isPlaying) {
if (resumeAt > 0.5 && currentTrack.duration > 0) {
@@ -31,7 +40,9 @@ export function useAudioDeviceBridge() {
setAtMs: Date.now(),
});
}
diag1090('before playTrack');
playTrack(currentTrack);
diag1090('after playTrack (returned)');
} else {
// Paused: clear warm-pause flag so the next resume uses the cold path
// (audio_play + seek) which creates a new Sink on the new device.