"use client"; import { createContext, useContext, useCallback, useRef, useState, useEffect } from "react"; import type { SanityImageSource } from "@sanity/image-url"; export type PlayerTrack = { previewMp3Url: string; title: string; /** Movement name, displayed in regular weight after the bold work title. */ movement?: string; subtitle: string; albumCover?: SanityImageSource; /** Full track duration in seconds (from tracklist metadata). */ trackDuration?: number; /** Release slug for linking back to the release page. */ releaseSlug?: string; }; type PlayerState = { playlist: PlayerTrack[]; currentIndex: number | null; isPlaying: boolean; currentTime: number; audioDuration: number; }; type PlayerActions = { loadPlaylist: (tracks: PlayerTrack[], startIndex: number) => void; togglePlayback: () => void; next: () => void; previous: () => void; seekTo: (fraction: number) => void; stop: () => void; }; type PlayerContextValue = PlayerState & PlayerActions; const PlayerContext = createContext(null); export function usePlayer() { const ctx = useContext(PlayerContext); if (!ctx) throw new Error("usePlayer must be used within a PlayerProvider"); return ctx; } export function usePlayerOptional() { return useContext(PlayerContext); } export function PlayerProvider({ children }: { children: React.ReactNode }) { const audioRef = useRef(null); const [playlist, setPlaylist] = useState([]); const [currentIndex, setCurrentIndex] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [audioDuration, setAudioDuration] = useState(0); // Create audio element once useEffect(() => { const audio = new Audio(); audioRef.current = audio; let rafId: number | null = null; // Poll currentTime at ~60fps for a silky-smooth progress bar const tick = () => { setCurrentTime(audio.currentTime); rafId = requestAnimationFrame(tick); }; const onDurationChange = () => setAudioDuration(audio.duration || 0); const onPlay = () => { setIsPlaying(true); if (rafId === null) rafId = requestAnimationFrame(tick); }; const onPause = () => { setIsPlaying(false); if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } }; audio.addEventListener("durationchange", onDurationChange); audio.addEventListener("play", onPlay); audio.addEventListener("pause", onPause); return () => { if (rafId !== null) cancelAnimationFrame(rafId); audio.removeEventListener("durationchange", onDurationChange); audio.removeEventListener("play", onPlay); audio.removeEventListener("pause", onPause); audio.pause(); audio.src = ""; }; }, []); // Handle track ended → auto-advance useEffect(() => { const audio = audioRef.current; if (!audio) return; const onEnded = () => { if (currentIndex !== null && currentIndex < playlist.length - 1) { const nextIdx = currentIndex + 1; setCurrentIndex(nextIdx); audio.src = playlist[nextIdx].previewMp3Url; audio.play().catch(() => {}); } else { audio.pause(); audio.src = ""; setPlaylist([]); setCurrentIndex(null); setCurrentTime(0); setAudioDuration(0); setIsPlaying(false); } }; audio.addEventListener("ended", onEnded); return () => audio.removeEventListener("ended", onEnded); }, [currentIndex, playlist]); const loadPlaylist = useCallback((tracks: PlayerTrack[], startIndex: number) => { const audio = audioRef.current; if (!audio) return; setPlaylist(tracks); setCurrentIndex(startIndex); setCurrentTime(0); setAudioDuration(0); audio.src = tracks[startIndex].previewMp3Url; audio.play().catch(() => {}); }, []); const togglePlayback = useCallback(() => { const audio = audioRef.current; if (!audio || currentIndex === null) return; if (audio.paused) { audio.play().catch(() => {}); } else { audio.pause(); } }, [currentIndex]); const next = useCallback(() => { const audio = audioRef.current; if (!audio || currentIndex === null) return; if (currentIndex < playlist.length - 1) { const nextIdx = currentIndex + 1; setCurrentIndex(nextIdx); setCurrentTime(0); audio.src = playlist[nextIdx].previewMp3Url; audio.play().catch(() => {}); } }, [currentIndex, playlist]); const previous = useCallback(() => { const audio = audioRef.current; if (!audio || currentIndex === null) return; if (currentIndex > 0) { const prevIdx = currentIndex - 1; setCurrentIndex(prevIdx); setCurrentTime(0); audio.src = playlist[prevIdx].previewMp3Url; audio.play().catch(() => {}); } }, [currentIndex, playlist]); const seekTo = useCallback((fraction: number) => { const audio = audioRef.current; if (!audio || !audio.duration) return; audio.currentTime = fraction * audio.duration; }, []); const stop = useCallback(() => { const audio = audioRef.current; if (!audio) return; audio.pause(); audio.src = ""; setPlaylist([]); setCurrentIndex(null); setCurrentTime(0); setAudioDuration(0); setIsPlaying(false); }, []); return ( {children} ); }