import { createContext, useContext, useRef, useState, useCallback, useEffect } from "react"; import { getTrackStreamUrl } from "./api"; const MusicPlayerCtx = createContext(null); export const MusicPlayerProvider = ({ children }) => { const audioRef = useRef(new Audio()); const [queue, setQueue] = useState([]); const [index, setIndex] = useState(-1); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState({ position: 0, duration: 0 }); const [outputDevices, setOutputDevices] = useState([]); const [sinkId, setSinkId] = useState("default"); const sinkSupported = typeof audioRef.current.setSinkId === "function"; const current = index >= 0 ? queue[index] : null; const refreshOutputDevices = useCallback(async () => { if (!navigator.mediaDevices?.enumerateDevices) return; try { const devices = await navigator.mediaDevices.enumerateDevices(); const outs = devices.filter((d) => d.kind === "audiooutput"); setOutputDevices(outs.map((d, i) => ({ deviceId: d.deviceId, label: d.label || `Output ${i + 1}` }))); } catch { /* enumeration unsupported/denied — picker just won't populate */ } }, []); const selectOutput = useCallback(async (deviceId) => { if (!sinkSupported) return; try { await audioRef.current.setSinkId(deviceId); setSinkId(deviceId); } catch { /* device may have disappeared (e.g. BT speaker turned off) — keep current output */ } }, [sinkSupported]); const playAt = useCallback((newQueue, startIndex) => { setQueue(newQueue); setIndex(startIndex); }, []); useEffect(() => { const audio = audioRef.current; if (!current) { audio.pause(); return; } audio.src = getTrackStreamUrl(current); audio.play().catch(() => {}); // isPlaying reflects reality via the native play/pause listeners below }, [current]); // isPlaying always mirrors the audio element's own state, so it stays correct through anything // that changes playback outside togglePlay — a stall, an OS media-key pause, autoplay being blocked. useEffect(() => { const audio = audioRef.current; const onTime = () => setProgress({ position: audio.currentTime, duration: audio.duration || 0 }); const onEnded = () => { if (index >= 0 && index < queue.length - 1) setIndex(index + 1); }; const onPlay = () => setIsPlaying(true); const onPause = () => setIsPlaying(false); audio.addEventListener("timeupdate", onTime); audio.addEventListener("ended", onEnded); audio.addEventListener("play", onPlay); audio.addEventListener("pause", onPause); return () => { audio.removeEventListener("timeupdate", onTime); audio.removeEventListener("ended", onEnded); audio.removeEventListener("play", onPlay); audio.removeEventListener("pause", onPause); }; }, [index, queue]); const togglePlay = useCallback(() => { const audio = audioRef.current; if (!current) return; if (audio.paused) audio.play().catch(() => {}); else audio.pause(); }, [current]); const next = useCallback(() => { if (index >= 0 && index < queue.length - 1) setIndex(index + 1); }, [index, queue]); const prev = useCallback(() => { if (index > 0) setIndex(index - 1); }, [index]); const seek = useCallback((seconds) => { audioRef.current.currentTime = seconds; }, []); return ( 0, outputDevices, sinkId, sinkSupported, refreshOutputDevices, selectOutput, }}> {children} ); }; export const useMusicPlayer = () => useContext(MusicPlayerCtx);