import { useEffect, useRef, useState } from "react"; import { audioService } from "../services/audioService"; import { usePlayerStore } from "../stores/playerStore"; export interface UseWaveformOptions { url: string | null; peaksUrl: string | null; onReady?: (duration: number) => void; onTimeUpdate?: (currentTime: number) => void; songId?: string | null; bandId?: string | null; } export interface CommentMarker { id: string; time: number; // Time in seconds onClick: () => void; icon?: string; // URL for the account icon } export function useWaveform( containerRef: React.RefObject, options: UseWaveformOptions ) { const [isReady, setIsReady] = useState(false); const [error, setError] = useState(null); const markersRef = useRef([]); // Playback state comes directly from the store — no intermediate local state // or RAF polling loop needed. The store is updated by WaveSurfer event handlers // in AudioService, so these values are always in sync. const isPlaying = usePlayerStore(state => state.isPlaying); const currentTime = usePlayerStore(state => state.currentTime); const duration = usePlayerStore(state => state.duration); useEffect(() => { if (!containerRef.current) return; if (!options.url || options.url === 'null' || options.url === 'undefined') return; const initializeAudio = async () => { try { await audioService.initialize(containerRef.current!, options.url!); // Restore playback if this song was already playing when the page loaded. // Read as a one-time snapshot — these values must NOT be reactive deps or // the effect would re-run on every time update (re-initializing WaveSurfer). const { currentSongId, currentBandId, isPlaying: wasPlaying, currentTime: savedTime, } = usePlayerStore.getState(); if ( options.songId && options.bandId && currentSongId === options.songId && currentBandId === options.bandId && wasPlaying && audioService.isWaveformReady() ) { try { await audioService.play(options.songId, options.bandId); if (savedTime > 0) audioService.seekTo(savedTime); } catch (err) { console.warn('Auto-play prevented during initialization:', err); } } setIsReady(true); options.onReady?.(audioService.getDuration()); } catch (err) { console.error('useWaveform: initialization failed', err); setIsReady(false); setError(err instanceof Error ? err.message : 'Failed to initialize audio'); } }; initializeAudio(); // containerRef is a stable ref object — safe to include. // options.onReady is intentionally omitted: it's a callback that callers // may not memoize, and re-running initialization on every render would be // worse than stale-closing over it for the brief window after mount. // eslint-disable-next-line react-hooks/exhaustive-deps }, [options.url, options.songId, options.bandId, containerRef]); const play = () => { audioService.play(options.songId ?? null, options.bandId ?? null) .catch(err => console.error('[useWaveform] play failed:', err)); }; const pause = () => { audioService.pause(); }; const seekTo = (time: number) => { audioService.seekTo(time); }; const addMarker = (marker: CommentMarker) => { if (!isReady) return; try { const markerElement = document.createElement("div"); markerElement.style.position = "absolute"; markerElement.style.width = "24px"; markerElement.style.height = "24px"; markerElement.style.borderRadius = "50%"; markerElement.style.backgroundColor = "var(--accent)"; markerElement.style.cursor = "pointer"; markerElement.style.zIndex = "9999"; markerElement.style.left = `${(marker.time / audioService.getDuration()) * 100}%`; markerElement.style.transform = "translateX(-50%) translateY(-50%)"; markerElement.style.top = "50%"; markerElement.style.border = "2px solid white"; markerElement.style.boxShadow = "0 0 4px rgba(0, 0, 0, 0.3)"; markerElement.title = `Comment at ${formatTime(marker.time)}`; markerElement.onclick = marker.onClick; if (marker.icon) { const iconElement = document.createElement("img"); iconElement.src = marker.icon; iconElement.style.width = "100%"; iconElement.style.height = "100%"; iconElement.style.borderRadius = "50%"; iconElement.style.objectFit = "cover"; markerElement.appendChild(iconElement); } const waveformContainer = containerRef.current; if (waveformContainer) { waveformContainer.style.position = "relative"; waveformContainer.appendChild(markerElement); } markersRef.current.push(marker); } catch (err) { console.error('useWaveform.addMarker failed:', err); } }; const clearMarkers = () => { const waveformContainer = containerRef.current; if (waveformContainer) { const markerElements = waveformContainer.querySelectorAll("div[title^='Comment at']"); markerElements.forEach((element) => { waveformContainer.removeChild(element); }); } markersRef.current = []; }; return { isPlaying, isReady, currentTime, duration, play, pause, seekTo, addMarker, clearMarkers, error }; } function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${String(s).padStart(2, "0")}`; }