fix(visualizer): auto-enable actually starts capture; persist audio device

Auto-enable was a no-op
- Writing 'visualizerEnabled'='true' to localStorage from app.js did
  not update the exported `let visualizerEnabled` in player.js. So
  applyVisualizerMode() saw the stale `false` and went into the
  DISABLE branch — leaving the device 'available, not capturing'.
- Add a setVisualizerEnabled() setter exported from player.js and
  call it before applyVisualizerMode() during boot.

Audio device persistence
- Save the selected device name to localStorage on change.
- On loadAudioDevices(), prefer status.current_device (server's
  current state) but fall back to the localStorage value if the
  server doesn't know one (e.g. after a server restart).
- If the saved device wasn't recognized by the server, push it back
  via POST /api/media/visualizer/device so capture lands on it
  immediately. Best-effort; no toast on failure.
This commit is contained in:
2026-04-25 02:17:03 +03:00
parent 153424eff8
commit 6066b4a2c5
2 changed files with 45 additions and 11 deletions
+8 -9
View File
@@ -22,7 +22,7 @@ import {
initTheme, toggleTheme, initAccentColor, applyAccentColor, initTheme, toggleTheme, initAccentColor, applyAccentColor,
renderAccentSwatches, selectAccentColor, toggleAccentPicker, lightenColor, renderAccentSwatches, selectAccentColor, toggleAccentPicker, lightenColor,
toggleVinylMode, applyVinylMode, toggleVinylMode, applyVinylMode,
visualizerEnabled, visualizerAvailable, visualizerEnabled, visualizerAvailable, setVisualizerEnabled,
checkVisualizerAvailability, toggleVisualizer, applyVisualizerMode, checkVisualizerAvailability, toggleVisualizer, applyVisualizerMode,
loadAudioDevices, onAudioDeviceChanged, loadAudioDevices, onAudioDeviceChanged,
setupProgressDrag, updateUI, updatePlaybackState, stopPositionInterpolation, setupProgressDrag, updateUI, updatePlaybackState, stopPositionInterpolation,
@@ -186,14 +186,13 @@ window.addEventListener('DOMContentLoaded', async () => {
// Initialize audio visualizer — auto-enable when supported so the // Initialize audio visualizer — auto-enable when supported so the
// spectrum shows real audio out of the box. // spectrum shows real audio out of the box.
checkVisualizerAvailability().then(() => { checkVisualizerAvailability().then(() => {
if (visualizerAvailable && !visualizerEnabled) { if (!visualizerAvailable) return;
// Auto-enable on first install if loopback capture works. // First install: opt the user in by default since the spectrum
if (localStorage.getItem('visualizerEnabled') === null) { // is the centerpiece of the player view.
localStorage.setItem('visualizerEnabled', 'true'); const stored = localStorage.getItem('visualizerEnabled');
} const shouldEnable = stored === null ? true : stored === 'true';
} if (shouldEnable) {
if ((visualizerEnabled || localStorage.getItem('visualizerEnabled') === 'true') setVisualizerEnabled(true); // updates the let in player.js
&& visualizerAvailable) {
applyVisualizerMode(); applyVisualizerMode();
} }
}); });
+37 -2
View File
@@ -277,6 +277,10 @@ function updateVinylSpin() {
// Audio Visualizer // Audio Visualizer
export let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true'; export let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true';
export let visualizerAvailable = false; export let visualizerAvailable = false;
export function setVisualizerEnabled(value) {
visualizerEnabled = !!value;
localStorage.setItem('visualizerEnabled', visualizerEnabled);
}
let visualizerCtx = null; let visualizerCtx = null;
let visualizerAnimFrame = null; let visualizerAnimFrame = null;
export let frequencyData = null; export let frequencyData = null;
@@ -506,13 +510,24 @@ export async function loadAudioDevices() {
select.appendChild(opt); select.appendChild(opt);
} }
if (status.current_device) { // Prefer server-reported device; fall back to the last user choice
// saved in localStorage (so reloads persist even if the server
// forgets between restarts).
const savedDevice = localStorage.getItem('audioDevice') || '';
const targetDevice = status.current_device || savedDevice;
let pendingPushToServer = false;
if (targetDevice) {
for (let i = 0; i < select.options.length; i++) { for (let i = 0; i < select.options.length; i++) {
if (select.options[i].value === status.current_device) { if (select.options[i].value === targetDevice) {
select.selectedIndex = i; select.selectedIndex = i;
break; break;
} }
} }
// If the saved device wasn't on the server, push it back so
// capture starts on the right one.
if (!status.current_device && savedDevice) {
pendingPushToServer = true;
}
} }
// Enhance with icon grid // Enhance with icon grid
@@ -545,6 +560,19 @@ export async function loadAudioDevices() {
ws.send(JSON.stringify({ type: 'enable_visualizer' })); ws.send(JSON.stringify({ type: 'enable_visualizer' }));
} }
} }
// If the user's previously-chosen device wasn't recognized by
// the server (e.g. server restart cleared in-memory state),
// push it back so capture lands on the right one.
if (pendingPushToServer && savedDevice) {
try {
await fetch('/api/media/visualizer/device', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
body: JSON.stringify({ device_name: savedDevice })
});
} catch (_) { /* best-effort */ }
}
} catch (e) { } catch (e) {
section.style.display = 'none'; section.style.display = 'none';
} }
@@ -572,6 +600,13 @@ export async function onAudioDeviceChanged() {
const deviceName = select.value || null; const deviceName = select.value || null;
// Persist locally so reloads survive even if the server doesn't.
if (deviceName) {
localStorage.setItem('audioDevice', deviceName);
} else {
localStorage.removeItem('audioDevice');
}
try { try {
const resp = await fetch('/api/media/visualizer/device', { const resp = await fetch('/api/media/visualizer/device', {
method: 'POST', method: 'POST',