From 7dd70a52e4308e1cb432ef13638d94ba694141f4 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Mon, 16 Mar 2026 12:29:10 -0700 Subject: [PATCH] fix audio player freezing and improve UX Switch WaveSurfer from MediaElement to WebAudio backend to prevent WKWebView deadlocks that were freezing the entire Tauri app during audio playback. Reuse a single WaveSurfer instance across track changes instead of destroying and recreating on every URL change, which was exhausting the browser's AudioContext pool. Other improvements: - spacebar play/pause with capture phase to prevent history item activation - drag-to-seek on waveform with silent scrub to avoid WebAudio popping - slider always mounted to prevent layout shift during track transitions - play button fills icon, accent bg when playing, loop button accent bg when active - fix AudioBars animation getting stuck by keying on mode - remove focus ring on history items - sync slider position on pause and during seek - remove title text from player bar - thicker cursor (3px) --- .../components/AudioPlayer/AudioPlayer.tsx | 595 ++++++------------ app/src/components/History/HistoryTable.tsx | 4 +- app/src/lib/hooks/useSystemAudioCapture.ts | 10 +- 3 files changed, 195 insertions(+), 414 deletions(-) diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx index 667404f3..7a3c0f68 100644 --- a/app/src/components/AudioPlayer/AudioPlayer.tsx +++ b/app/src/components/AudioPlayer/AudioPlayer.tsx @@ -17,7 +17,6 @@ export function AudioPlayer() { audioUrl, audioId, profileId, - title, isPlaying, currentTime, duration, @@ -63,7 +62,7 @@ export function AudioPlayer() { ); return shouldUseNative; - }, [profileChannels, channels, profileId]); + }, [profileChannels, channels, platform.metadata.isTauri]); const waveformRef = useRef(null); const wavesurferRef = useRef(null); @@ -73,31 +72,21 @@ export function AudioPlayer() { const isUsingNativePlaybackRef = useRef(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [wsReady, setWsReady] = useState(false); - // Initialize WaveSurfer (only when audioUrl exists and container is ready) + // Create WaveSurfer once when the player becomes visible (audioUrl is set). + // This instance is reused for all subsequent audio loads - never destroyed until unmount. useEffect(() => { - // Don't initialize if no audioUrl or already initialized - if (!audioUrl) { - return; - } + if (!audioUrl) return; + if (wavesurferRef.current) return; // already created - if (wavesurferRef.current) { - debug.log('WaveSurfer already initialized, skipping'); - return; - } - - debug.log('Creating NEW WaveSurfer instance'); - - // Wait for container to be properly rendered const initWaveSurfer = () => { const container = waveformRef.current; if (!container) { - // Container not ready yet, retry setTimeout(initWaveSurfer, 50); return; } - // Check if container has dimensions and is visible const rect = container.getBoundingClientRect(); const style = window.getComputedStyle(container); const isVisible = @@ -107,412 +96,221 @@ export function AudioPlayer() { style.visibility !== 'hidden'; if (!isVisible) { - // Retry after a short delay setTimeout(initWaveSurfer, 50); return; } - debug.log('Initializing WaveSurfer...', { - container, + debug.log('Creating WaveSurfer instance', { width: rect.width, height: rect.height, }); try { - // Get computed CSS variable values const root = document.documentElement; const getCSSVar = (varName: string) => { const value = getComputedStyle(root).getPropertyValue(varName).trim(); return value ? `hsl(${value})` : ''; }; - const waveColor = getCSSVar('--muted'); - const progressColor = getCSSVar('--accent'); - const cursorColor = getCSSVar('--accent'); - const wavesurfer = WaveSurfer.create({ - container: container, - waveColor: waveColor, - progressColor: progressColor, - cursorColor: cursorColor, + container, + waveColor: getCSSVar('--muted'), + progressColor: getCSSVar('--accent'), + cursorColor: getCSSVar('--accent'), + cursorWidth: 3, barWidth: 2, barRadius: 2, height: 80, normalize: true, - // Use MediaElement backend (default). Unlike the WebAudio backend, - // MediaElement uses a standard