import { createContext, useEffect, useRef, useState } from "react"; export interface PlayerContextValue { playAudio: ( artist: string, title: string, audio: string | Blob | File, messageId?: string | null ) => void; playing: boolean; pause: () => void; resume: () => void; stop: () => void; setDuration: (duration: number) => void; duration: number; totalDuration: number; currentMessageId: string | null; lastMessageId: string | null; lastError: string | null; } export const PlayerContext = createContext(null); interface PlayerProviderProps { children: React.ReactNode; } export function PlayerProvider(props: PlayerProviderProps) { const audioRef = useRef(null); const objectUrlRef = useRef(null); const rafTimeUpdateRef = useRef(null); const isLoadingRef = useRef(false); const isSeekingRef = useRef(false); const durationRef = useRef(0); const totalDurationRef = useRef(0); const isPlayingRef = useRef(false); const [isPlaying, setIsPlaying] = useState(false); const [duration, setDurationState] = useState(0); const [totalDuration, setTotalDuration] = useState(0); const [lastError, setLastError] = useState(null); const [currentMessageId, setCurrentMessageId] = useState(null); const [lastMessageId, setLastMessageId] = useState(null); const commitPlaying = (next: boolean) => { if (isPlayingRef.current === next) return; isPlayingRef.current = next; setIsPlaying(next); }; const commitDuration = (next: number) => { const safe = Number.isFinite(next) && next >= 0 ? next : 0; if (Math.abs(safe - durationRef.current) < 0.033) return; durationRef.current = safe; setDurationState(safe); }; const commitTotalDuration = (next: number) => { const safe = Number.isFinite(next) && next > 0 ? next : 0; if (Math.abs(safe - totalDurationRef.current) < 0.05) return; totalDurationRef.current = safe; setTotalDuration(safe); }; const decodeMediaError = (err: MediaError | null) => { if (!err) return "Unknown media error"; switch (err.code) { case MediaError.MEDIA_ERR_ABORTED: return "Playback aborted"; case MediaError.MEDIA_ERR_NETWORK: return "Network error while loading audio"; case MediaError.MEDIA_ERR_DECODE: return "Audio decode error"; case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: return "Audio source is not supported"; default: return `Unknown media error (${err.code})`; } }; useEffect(() => { const audio = audioRef.current; if (!audio) return; const onPlay = () => { if (isLoadingRef.current) return; commitPlaying(true); }; const onPause = () => { if (isLoadingRef.current) return; commitPlaying(false); }; const onEnded = () => { commitPlaying(false); durationRef.current = 0; setDurationState(0); setCurrentMessageId(null); }; const onTimeUpdate = () => { if (isLoadingRef.current) return; if (isSeekingRef.current) return; if (rafTimeUpdateRef.current != null) return; rafTimeUpdateRef.current = requestAnimationFrame(() => { rafTimeUpdateRef.current = null; if (!isLoadingRef.current && !isSeekingRef.current) { commitDuration(audio.currentTime || 0); } }); }; const onLoadedMetadata = () => commitTotalDuration(audio.duration); const onDurationChange = () => commitTotalDuration(audio.duration); const onSeeked = () => { if (isSeekingRef.current) { isSeekingRef.current = false; if (!isLoadingRef.current) commitDuration(audio.currentTime || 0); return; } if (isLoadingRef.current) return; commitDuration(audio.currentTime || 0); }; const onCanPlay = () => { if (isLoadingRef.current) isLoadingRef.current = false; }; const onError = (_e: Event) => { const message = decodeMediaError(audio.error); setLastError(message); console.error("Audio playback error", { message, mediaError: audio.error, currentSrc: audio.currentSrc, readyState: audio.readyState, networkState: audio.networkState, }); }; audio.addEventListener("play", onPlay); audio.addEventListener("pause", onPause); audio.addEventListener("ended", onEnded); audio.addEventListener("timeupdate", onTimeUpdate); audio.addEventListener("loadedmetadata", onLoadedMetadata); audio.addEventListener("durationchange", onDurationChange); audio.addEventListener("seeked", onSeeked); audio.addEventListener("canplay", onCanPlay); audio.addEventListener("error", onError); return () => { audio.removeEventListener("play", onPlay); audio.removeEventListener("pause", onPause); audio.removeEventListener("ended", onEnded); audio.removeEventListener("timeupdate", onTimeUpdate); audio.removeEventListener("loadedmetadata", onLoadedMetadata); audio.removeEventListener("durationchange", onDurationChange); audio.removeEventListener("seeked", onSeeked); audio.removeEventListener("canplay", onCanPlay); audio.removeEventListener("error", onError); if (rafTimeUpdateRef.current != null) { cancelAnimationFrame(rafTimeUpdateRef.current); rafTimeUpdateRef.current = null; } }; }, []); useEffect(() => { return () => { if (objectUrlRef.current) { URL.revokeObjectURL(objectUrlRef.current); objectUrlRef.current = null; } }; }, []); const playAudio = ( artist: string, title: string, audio: string | Blob | File, messageId?: string | null ) => { const el = audioRef.current; if (!el) return; // чтобы не было warning о неиспользуемых args при строгих правилах void artist; void title; setLastError(null); if (objectUrlRef.current) { URL.revokeObjectURL(objectUrlRef.current); objectUrlRef.current = null; } const audioSrc = typeof audio === "string" ? audio : URL.createObjectURL(audio); if (typeof audio !== "string") { objectUrlRef.current = audioSrc; } isLoadingRef.current = true; isSeekingRef.current = false; el.src = audioSrc; durationRef.current = 0; const msgId = messageId ?? null; setCurrentMessageId(msgId); if (msgId) setLastMessageId(msgId); isPlayingRef.current = true; setIsPlaying(true); const prevDuration = durationRef.current; requestAnimationFrame(() => { if (durationRef.current === prevDuration) { setDurationState(0); } }); void el.play().catch((err) => { isLoadingRef.current = false; commitPlaying(false); setLastError(err instanceof Error ? err.message : "play() failed"); }); }; const pause = () => { const el = audioRef.current; if (!el) return; el.pause(); }; const resume = () => { const el = audioRef.current; if (!el) return; commitPlaying(true); void el.play().catch((err) => { commitPlaying(false); setLastError(err instanceof Error ? err.message : "resume() failed"); }); }; const stop = () => { const el = audioRef.current; if (!el) return; isLoadingRef.current = true; el.pause(); el.currentTime = 0; isLoadingRef.current = false; durationRef.current = 0; setDurationState(0); commitPlaying(false); setCurrentMessageId(null); }; const setDuration = (sec: number) => { const el = audioRef.current; if (!el) return; isSeekingRef.current = true; el.currentTime = Math.max(0, sec); commitDuration(el.currentTime || 0); }; return ( {props.children} ); }