import { useCallback, useEffect, useId, useState } from 'react'; const audioInstances = new Map(); let currentPlayingId: string | null = null; const AUDIO_PLAY_EVENT = 'AUDIO_PLAY_EVENT'; const createAudioPlayEvent = (audioId: string) => new CustomEvent(AUDIO_PLAY_EVENT, { detail: { audioId }, }); interface UseAudioControlProps { url: string; } interface UseAudioControlReturn { isPlaying: boolean; progress: number; duration: number | undefined; isLoading: boolean; play: () => void; pause: () => void; stop: () => void; setProgress: (newProgress: number) => void; } function useAudioControl(props: UseAudioControlProps): UseAudioControlReturn { const { url } = props; const audioId = useId(); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [audio, setAudio] = useState(null); const [duration, setDuration] = useState(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const handleAudioPlay = (e: CustomEvent<{ audioId: string }>) => { if (e.detail.audioId !== audioId && isPlaying) { // if other audio start playing and current audio is playing then pause current audio audio?.pause(); setIsPlaying(false); } }; window.addEventListener(AUDIO_PLAY_EVENT, handleAudioPlay as EventListener); return () => { window.removeEventListener(AUDIO_PLAY_EVENT, handleAudioPlay as EventListener); }; }, [audioId, audio, isPlaying]); // initial audio instance or update url useEffect(() => { let audioInstance = audioInstances.get(audioId); // listen metadata loaded event const handleLoadedMetadata = () => { if (!audioInstance) { return; } if (audioInstance.duration === Infinity) { // Jump to a large time audioInstance.currentTime = 1e101; audioInstance.ontimeupdate = () => { if (!audioInstance) { return; } setDuration(audioInstance.duration); audioInstance.ontimeupdate = null; audioInstance.currentTime = 0; }; } else { setDuration(audioInstance.duration); } setIsLoading(false); }; const handleError = () => { console.error('Audio load error:', url); setIsLoading(false); }; // if instance not existed or url changed then create new instance if (!audioInstance || audioInstance.src !== url) { setIsLoading(true); setDuration(undefined); // if exist lod instance then clear first if (audioInstance) { audioInstance.pause(); audioInstance.src = ''; audioInstance.load(); } // create new instance audioInstance = new Audio(url); audioInstance.addEventListener('loadedmetadata', handleLoadedMetadata); audioInstance.addEventListener('error', handleError); audioInstances.set(audioId, audioInstance); setAudio(audioInstance); setIsPlaying(false); setProgress(0); } return () => { // remove when component unmount if (audioInstances.has(audioId)) { const instance = audioInstances.get(audioId)!; instance.removeEventListener('loadedmetadata', handleLoadedMetadata); instance.removeEventListener('error', handleError); instance.pause(); instance.src = ''; instance.load(); audioInstances.delete(audioId); } }; }, [audioId, url]); useEffect(() => { const handleTimeUpdate = () => { if (audio) { setProgress(audio.currentTime / audio.duration); } }; const handleEnded = () => { setIsPlaying(false); setProgress(0); currentPlayingId = null; }; if (audio) { audio.addEventListener('timeupdate', handleTimeUpdate); audio.addEventListener('ended', handleEnded); } return () => { audio?.removeEventListener('timeupdate', handleTimeUpdate); audio?.removeEventListener('ended', handleEnded); }; }, [audio]); const setAudioProgress = useCallback((newProgress: number) => { if (audio) { audio.currentTime = audio.duration * newProgress; setProgress(newProgress); } }, [audio]); const play = useCallback(() => { if (audio) { // notify other audio instance window.dispatchEvent(createAudioPlayEvent(audioId)); audio.play(); setIsPlaying(true); currentPlayingId = audioId; } }, [audio, audioId]); const pause = useCallback(() => { if (audio) { audio.pause(); setIsPlaying(false); setAudioProgress(0); if (currentPlayingId === audioId) { currentPlayingId = null; } } }, [audio, audioId, setAudioProgress]); const stop = useCallback(() => { if (audio) { audio.pause(); setIsPlaying(false); if (currentPlayingId === audioId) { currentPlayingId = null; } } }, [audio, audioId]); return { isPlaying, progress, duration, isLoading, play, pause, stop, setProgress: setAudioProgress, }; } export { useAudioControl };