import { createContext, useContext, useRef, useState, useEffect, useCallback, type ReactNode, } from "react"; import { type Track } from "./playlist"; // --------------------------------------------------------------------------- // 타입 // --------------------------------------------------------------------------- type PlayerState = { currentIndex: number; currentTrack: Track | null; isPlaying: boolean; progress: number; currentTime: number; duration: number; volume: number; muted: boolean; userClosed: boolean; hasTrack: boolean; }; type PlayerActions = { play: () => void; pause: () => void; toggle: () => void; next: () => void; prev: () => void; seek: (ratio: number) => void; setVolume: (v: number) => void; toggleMute: () => void; close: () => void; open: () => void; goTo: (index: number) => void; loadPlaylist: (tracks: Track[]) => void; }; type PlayerContextValue = PlayerState & PlayerActions; // --------------------------------------------------------------------------- // Context // --------------------------------------------------------------------------- const PlayerContext = createContext(null); // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- export function PlayerProvider({ children }: { children: ReactNode }) { const audioRef = useRef(null); const currentSrcRef = useRef(""); const autoPlayRef = useRef(false); const isPlayingRef = useRef(false); const [playlist, setPlaylistState] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [loadKey, setLoadKey] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolumeState] = useState(0.4); const [muted, setMuted] = useState(false); const [userClosed, setUserClosed] = useState(false); const hasTrack = playlist.length > 0; const currentTrack = hasTrack ? playlist[currentIndex] : null; // 오디오 인스턴스 초기화 (마운트 시 1회) useEffect(() => { const audio = new Audio(); audio.preload = "metadata"; audio.volume = 0.4; audioRef.current = audio; const onTimeUpdate = () => { setCurrentTime(audio.currentTime); setProgress(audio.duration ? audio.currentTime / audio.duration : 0); }; const onLoaded = () => setDuration(audio.duration || 0); const onPlay = () => { setIsPlaying(true); isPlayingRef.current = true; }; const onPause = () => { setIsPlaying(false); isPlayingRef.current = false; }; audio.addEventListener("timeupdate", onTimeUpdate); audio.addEventListener("loadedmetadata", onLoaded); audio.addEventListener("durationchange", onLoaded); audio.addEventListener("play", onPlay); audio.addEventListener("pause", onPause); return () => { audio.pause(); audio.removeEventListener("timeupdate", onTimeUpdate); audio.removeEventListener("loadedmetadata", onLoaded); audio.removeEventListener("durationchange", onLoaded); audio.removeEventListener("play", onPlay); audio.removeEventListener("pause", onPause); audioRef.current = null; }; }, []); // ended 시 순환 처리 (playlist 길이 반영) const playlistLenRef = useRef(playlist.length); useEffect(() => { playlistLenRef.current = playlist.length; }, [playlist.length]); useEffect(() => { const audio = audioRef.current; if (!audio) return; const onEnded = () => { autoPlayRef.current = true; setCurrentIndex((prev) => playlistLenRef.current > 0 ? (prev + 1) % playlistLenRef.current : 0 ); }; audio.addEventListener("ended", onEnded); return () => audio.removeEventListener("ended", onEnded); }, []); // 트랙 변경 시 src 교체 useEffect(() => { const audio = audioRef.current; if (!audio || !currentTrack) return; const shouldPlay = isPlayingRef.current || autoPlayRef.current; autoPlayRef.current = false; currentSrcRef.current = currentTrack.src; audio.src = currentTrack.src; audio.load(); setProgress(0); setCurrentTime(0); setDuration(0); if (shouldPlay) audio.play().catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentIndex, currentTrack?.src, loadKey]); // --------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------- const play = useCallback(() => { const audio = audioRef.current; if (!audio) return; if (currentTrack && currentSrcRef.current !== currentTrack.src) { currentSrcRef.current = currentTrack.src; audio.src = currentTrack.src; audio.load(); } audio.play().catch(() => {}); }, [currentTrack]); const pause = useCallback(() => { audioRef.current?.pause(); }, []); const toggle = useCallback(() => { if (isPlaying) { audioRef.current?.pause(); } else { const audio = audioRef.current; if (!audio) return; if (currentTrack && currentSrcRef.current !== currentTrack.src) { currentSrcRef.current = currentTrack.src; audio.src = currentTrack.src; audio.load(); } audio.play().catch(() => {}); } }, [isPlaying, currentTrack]); const next = useCallback(() => { if (!hasTrack) return; setCurrentIndex((prev) => (prev + 1) % playlist.length); }, [hasTrack, playlist.length]); const prev = useCallback(() => { if (!hasTrack) return; setCurrentIndex((prev) => (prev - 1 + playlist.length) % playlist.length); }, [hasTrack, playlist.length]); const seek = useCallback((ratio: number) => { const audio = audioRef.current; if (!audio || !audio.duration) return; audio.currentTime = ratio * audio.duration; }, []); const setVolume = useCallback((v: number) => { const clamped = Math.max(0, Math.min(1, v)); setVolumeState(clamped); if (audioRef.current) { audioRef.current.volume = clamped; if (clamped > 0) { audioRef.current.muted = false; setMuted(false); } } }, []); const toggleMute = useCallback(() => { const audio = audioRef.current; if (!audio) return; const next = !audio.muted; audio.muted = next; setMuted(next); }, []); const close = useCallback(() => setUserClosed(true), []); const open = useCallback(() => setUserClosed(false), []); const goTo = useCallback((index: number) => { if (index < 0 || index >= playlist.length) return; setCurrentIndex(index); }, [playlist.length]); // 경로 변경 시 새 플레이리스트 로드 const loadPlaylist = useCallback((tracks: Track[]) => { const audio = audioRef.current; if (audio) { audio.pause(); audio.removeAttribute("src"); } currentSrcRef.current = ""; autoPlayRef.current = true; setPlaylistState([...tracks]); setLoadKey((k) => k + 1); setCurrentIndex(0); setProgress(0); setCurrentTime(0); setDuration(0); setIsPlaying(false); isPlayingRef.current = false; }, []); const value: PlayerContextValue = { currentIndex, currentTrack, isPlaying, progress, currentTime, duration, volume, muted, userClosed, hasTrack, play, pause, toggle, next, prev, seek, setVolume, toggleMute, close, open, goTo, loadPlaylist, }; return ( {children} ); } // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- export function usePlayer(): PlayerContextValue { const ctx = useContext(PlayerContext); if (!ctx) throw new Error("usePlayer must be used inside "); return ctx; }