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 [isPlaying, setIsPlaying] = useState(false); const [isReady, setIsReady] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [error, setError] = useState(null); const markersRef = useRef([]); // Global player state - use shallow comparison to reduce re-renders const { isPlaying: globalIsPlaying, currentTime: globalCurrentTime, currentSongId, currentBandId: globalBandId, currentPlayingSongId, currentPlayingBandId, setCurrentSong } = usePlayerStore(state => ({ isPlaying: state.isPlaying, currentTime: state.currentTime, currentSongId: state.currentSongId, currentBandId: state.currentBandId, currentPlayingSongId: state.currentPlayingSongId, currentPlayingBandId: state.currentPlayingBandId, setCurrentSong: state.setCurrentSong })); 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!); // Set up local state synchronization with requestAnimationFrame for smoother updates let animationFrameId: number | null = null; let lastUpdateTime = 0; const updateInterval = 1000 / 15; // ~15fps for state updates const handleStateUpdate = () => { const now = Date.now(); if (now - lastUpdateTime >= updateInterval) { const state = usePlayerStore.getState(); setIsPlaying(state.isPlaying); setCurrentTime(state.currentTime); setDuration(state.duration); lastUpdateTime = now; } animationFrameId = requestAnimationFrame(handleStateUpdate); }; // Start the animation frame loop animationFrameId = requestAnimationFrame(handleStateUpdate); const unsubscribe = () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } }; // Update global song context if (options.songId && options.bandId) { setCurrentSong(options.songId, options.bandId); } // If this is the currently playing song, restore play state if (options.songId && options.bandId && currentPlayingSongId === options.songId && currentPlayingBandId === options.bandId && globalIsPlaying) { // Wait for the waveform to be ready and audio context to be available const checkReady = setInterval(() => { if (audioService.getDuration() > 0) { clearInterval(checkReady); audioService.play(options.songId, options.bandId); if (globalCurrentTime > 0) { audioService.seekTo(globalCurrentTime); } } }, 50); } setIsReady(true); options.onReady?.(audioService.getDuration()); return () => { unsubscribe(); // Note: We don't cleanup the audio service here to maintain persistence // audioService.cleanup(); }; } catch (error) { console.error('useWaveform: initialization failed', error); setIsReady(false); setError(error instanceof Error ? error.message : 'Failed to initialize audio'); return () => {}; } }; initializeAudio(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options.url, options.songId, options.bandId, containerRef, currentSongId, globalBandId, globalCurrentTime, globalIsPlaying, setCurrentSong]); const play = () => { try { audioService.play(options.songId || null, options.bandId || null); } catch (error) { console.error('useWaveform.play failed:', error); } }; const pause = () => { try { audioService.pause(); } catch (error) { console.error('useWaveform.pause failed:', error); } }; const seekTo = (time: number) => { try { if (isReady && isFinite(time)) { audioService.seekTo(time); } } catch (error) { console.error('useWaveform.seekTo failed:', error); } }; const addMarker = (marker: CommentMarker) => { if (isReady) { try { // This would need proper implementation with the actual wavesurfer instance 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 (error) { console.error('useWaveform.addMarker failed:', error); } } }; 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")}`; }