import { useEffect, useRef, useCallback } from 'react'; import { useAppDispatch, useAppSelector } from './useAppDispatch'; import { pause, resume, setPosition, setDuration, setVolume as setVolumeAction, } from '../store/slices/player'; import { nextTrack, prevTrack } from '../store/slices/queue'; import { play } from '../store/slices/player'; import { getStreamUrl, getCoverUrl } from '../api/endpoints/streaming'; let audioElement: HTMLAudioElement | null = null; function getAudio(): HTMLAudioElement { if (!audioElement) { audioElement = new Audio(); audioElement.preload = 'metadata'; } return audioElement; } export function useAudioPlayer() { const dispatch = useAppDispatch(); const player = useAppSelector((s) => s.player); const queue = useAppSelector((s) => s.queue); const accessToken = useAppSelector((s) => s.auth.accessToken); const isSetup = useRef(false); // `ended` is registered once below; read the latest loop flag through a ref // so the listener doesn't need to be re-bound on every queue change. const loopRef = useRef(queue.loop); loopRef.current = queue.loop; useEffect(() => { if (isSetup.current) return; isSetup.current = true; const audio = getAudio(); audio.addEventListener('timeupdate', () => { dispatch(setPosition(audio.currentTime)); }); audio.addEventListener('durationchange', () => { dispatch(setDuration(audio.duration || 0)); }); audio.addEventListener('ended', () => { if (loopRef.current) { audio.currentTime = 0; void audio.play(); } else { dispatch(nextTrack()); } }); audio.addEventListener('pause', () => { dispatch(pause()); }); audio.addEventListener('play', () => { dispatch(resume()); }); }, [dispatch]); useEffect(() => { if (!player.currentTrackId || !accessToken) return; const audio = getAudio(); const url = getStreamUrl(player.currentTrackId, accessToken); if (audio.src !== url) { audio.src = url; audio.load(); } if (player.isPlaying) { void audio.play(); } else { audio.pause(); } }, [player.currentTrackId, player.isPlaying, accessToken]); useEffect(() => { const audio = getAudio(); audio.volume = player.muted ? 0 : player.volume; }, [player.volume, player.muted]); // MediaSession: system media controls + metadata (lock screen, OS, headset keys) useEffect(() => { if (!('mediaSession' in navigator)) return; const entry = queue.entries[queue.currentIndex]; if (!entry) { navigator.mediaSession.metadata = null; return; } const artUrl = getCoverUrl(entry.albumArtUrl); navigator.mediaSession.metadata = new MediaMetadata({ title: entry.title, artist: entry.artistName, album: entry.albumTitle, artwork: artUrl ? [{ src: artUrl }] : [], }); }, [queue.entries, queue.currentIndex]); useEffect(() => { if (!('mediaSession' in navigator)) return; navigator.mediaSession.playbackState = player.isPlaying ? 'playing' : 'paused'; }, [player.isPlaying]); useEffect(() => { if (!('mediaSession' in navigator)) return; const ms = navigator.mediaSession; ms.setActionHandler('play', () => dispatch(resume())); ms.setActionHandler('pause', () => dispatch(pause())); ms.setActionHandler('previoustrack', () => dispatch(prevTrack())); ms.setActionHandler('nexttrack', () => dispatch(nextTrack())); ms.setActionHandler('seekto', (details) => { if (typeof details.seekTime === 'number') { getAudio().currentTime = details.seekTime; dispatch(setPosition(details.seekTime)); } }); return () => { ms.setActionHandler('play', null); ms.setActionHandler('pause', null); ms.setActionHandler('previoustrack', null); ms.setActionHandler('nexttrack', null); ms.setActionHandler('seekto', null); }; }, [dispatch]); useEffect(() => { const currentEntry = queue.entries[queue.currentIndex]; if (!currentEntry) return; if (currentEntry.trackId !== player.currentTrackId) { dispatch(play(currentEntry.trackId)); } }, [queue.currentIndex, queue.entries, player.currentTrackId, dispatch]); const seek = useCallback( (seconds: number) => { const audio = getAudio(); audio.currentTime = seconds; dispatch(setPosition(seconds)); }, [dispatch], ); const setPlayerVolume = useCallback( (vol: number) => { dispatch(setVolumeAction(vol)); }, [dispatch], ); const playNext = useCallback(() => { dispatch(nextTrack()); }, [dispatch]); const playPrev = useCallback(() => { dispatch(prevTrack()); }, [dispatch]); return { seek, setVolume: setPlayerVolume, playNext, playPrev }; }