diff --git a/frontend/src/components/MusicPlayer.tsx b/frontend/src/components/MusicPlayer.tsx index 716ab9a..4dd3c2c 100644 --- a/frontend/src/components/MusicPlayer.tsx +++ b/frontend/src/components/MusicPlayer.tsx @@ -17,74 +17,85 @@ export default function MusicPlayer() { const [activeId, setActiveId] = useState('lofi-girl'); const [volume, setVolume] = useState(0.3); const [started, setStarted] = useState(false); - const wrapperRef = useRef(null); + const [playerReady, setPlayerReady] = useState(false); const playerRef = useRef(null); - const apiReady = useRef(false); + const volumeRef = useRef(volume); + const activeIdRef = useRef(activeId); - // Load YouTube IFrame API on first user interaction - const startPlayer = useCallback(() => { - if (apiReady.current) return; - apiReady.current = true; + // Keep refs in sync so the API callback sees current values + useEffect(() => { volumeRef.current = volume; }, [volume]); + useEffect(() => { activeIdRef.current = activeId; }, [activeId]); + + // Load YouTube IFrame API + const loadAPI = useCallback(() => { + if (document.querySelector('script[src="https://www.youtube.com/iframe_api"]')) { + // Already loaded — just wait for YT to be ready + if ((window as any).YT?.Player) { + createPlayer(); + } + return; + } const tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; - const first = document.getElementsByTagName('script')[0]; - first.parentNode?.insertBefore(tag, first); + document.head.appendChild(tag); (window as any).onYouTubeIframeAPIReady = () => { - const active = LOFI_PLAYLISTS.find((p) => p.id === activeId); - if (!active) return; - - playerRef.current = new (window as any).YT.Player('kira-youtube-player', { - height: '0', - width: '0', - videoId: active.videoId, - playerVars: { - autoplay: 1, - controls: 0, - loop: 1, - playlist: active.videoId, - enablejsapi: 1, - origin: window.location.origin, - modestbranding: 1, - }, - events: { - onReady: (e: any) => { - e.target.setVolume(volume * 100); - e.target.playVideo(); - }, - }, - }); + createPlayer(); }; - }, [activeId, volume]); + }, []); + + const createPlayer = useCallback(() => { + const active = LOFI_PLAYLISTS.find((p) => p.id === activeIdRef.current); + if (!active || playerRef.current) return; + + playerRef.current = new (window as any).YT.Player('kira-youtube-player', { + height: '1', + width: '1', + videoId: active.videoId, + playerVars: { + autoplay: 1, + controls: 0, + loop: 1, + playlist: active.videoId, + enablejsapi: 1, + origin: window.location.origin, + modestbranding: 1, + fs: 0, + }, + events: { + onReady: (e: any) => { + e.target.setVolume(Math.round(volumeRef.current * 100)); + e.target.playVideo(); + setPlayerReady(true); + }, + }, + }); + }, []); - // Handle play button click (first user gesture) const handlePlay = () => { - if (!started) { - setStarted(true); - startPlayer(); - } + setStarted(true); + loadAPI(); }; - // Change station const changeStation = (id: string) => { setActiveId(id); - if (playerRef.current && (window as any).YT) { + if (playerRef.current && playerReady) { const active = LOFI_PLAYLISTS.find((p) => p.id === id); if (active) { playerRef.current.loadVideoById(active.videoId); - playerRef.current.setVolume(volume * 100); + playerRef.current.setVolume(Math.round(volume * 100)); playerRef.current.playVideo(); } } }; - // Volume + // Volume sync useEffect(() => { - if (playerRef.current) { - playerRef.current.setVolume(volume * 100); + if (playerRef.current && playerReady) { + playerRef.current.setVolume(Math.round(volume * 100)); } - }, [volume]); + }, [volume, playerReady]); return (
@@ -123,10 +134,12 @@ export default function MusicPlayer() { ))}
- {/* Hidden YouTube player container */} -
+ {/* YouTube player: NOT display:none (API won't init). Offscreen instead. */} +
- {/* Play button / status */}
{!started ? (
-
Vol {Math.round(volume * 100)}%
- {active && (
Playing {active} noise — body double approved ✨
)}