import { useRef, useState, useCallback, useEffect } from "react"; import { setNewSong, setPlayingSong, setSongIndex, incSongIndex, decSongIndex, } from "../actions/currentSessionActions"; import "../styles/Player.css"; import PlayerControls from "./PlayerControls"; import PlayerSongListButton from "./PlayerSongListButton"; import { Slider } from "@material-ui/core"; import { calculateDurationTime, calculateCurrentTime, } from "../utils/player-utils"; import { setRecentSongsLocalStorage, getNextSong, getPreviousSong, } from "../utils/song-utils"; import { useDispatch, useSelector } from "react-redux"; // The Entire Bottom part where all the song controls are available function Player({ show }) { // playing 0 -> paused | 1 -> playing | -1 -> loading const [playing, setPlaying] = useState(0); const [continuousTime, setContinuousTime] = useState(0); const [displayCurrentTime, setDisplayCurrentTime] = useState("0:00"); const [displayDurationTime, setDisplayDurationTime] = useState("0:00"); const [currentSong, setCurrentSong] = useState(null); const audioRef = useRef(null); const { newSong, songIndex } = useSelector((state) => state.currentSession); const dispatch = useDispatch(); const isPlayingRef = useRef(false); const onTabCloseRef = useRef((e) => { if (isPlayingRef.current) { e.preventDefault(); e.returnValue = "You have unfinished changes!"; } }); useEffect(() => { const fn = onTabCloseRef.current; window.addEventListener("beforeunload", fn); return () => { window.removeEventListener("beforeunload", fn); }; }, []); useEffect(() => { if (currentSong) { document.title = `${currentSong.name} (${currentSong.artist}) | Octave`; dispatch(setPlayingSong(currentSong)); setRecentSongsLocalStorage(currentSong); } }, [currentSong, dispatch]); useEffect(() => { if (newSong) { dispatch(setSongIndex(0)); setCurrentSong(newSong); setPlaying(-1); dispatch(setNewSong(null)); } }, [dispatch, newSong, songIndex]); // playing 0 -> paused | 1 -> playing | -1 -> loading const playPauseSong = () => { if (!currentSong) { playNextSong(); return; } if (playing === 1) { audioRef.current.pause(); setPlaying(0); isPlayingRef.current = false; document.title = `Octave`; } else { audioRef.current.play(); setPlaying(1); isPlayingRef.current = true; document.title = `${currentSong?.name} (${currentSong?.artist}) | Octave`; } }; const playNextSong = useCallback(() => { const nextSong = getNextSong(songIndex); if (nextSong) { setCurrentSong(nextSong); setPlaying(-1); dispatch(incSongIndex()); } }, [songIndex, dispatch]); const playPreviousSong = useCallback(() => { const prevSong = getPreviousSong(songIndex); if (prevSong) { setCurrentSong(prevSong); dispatch(decSongIndex()); } }, [songIndex, dispatch]); const playSongByMediaSession = useCallback(async () => { await audioRef.current.play(); navigator.mediaSession.playbackState = "playing"; setPlaying(1); isPlayingRef.current = true; document.title = `${currentSong?.name} (${currentSong?.artist}) | Octave`; }, [currentSong]); const pauseSongByMediaSession = async () => { await audioRef.current.pause(); navigator.mediaSession.playbackState = "paused"; setPlaying(0); isPlayingRef.current = false; document.title = `Octave`; }; // MediaSession docs -> https://developer.mozilla.org/en-US/docs/Web/API/MediaSession const setupMediaSession = useCallback(() => { if ("mediaSession" in navigator) { navigator.mediaSession.metadata = new window.MediaMetadata({ title: currentSong.name, artist: currentSong.artist, }); navigator.mediaSession.setActionHandler("play", () => { playSongByMediaSession(); }); navigator.mediaSession.setActionHandler("pause", () => { pauseSongByMediaSession(); }); navigator.mediaSession.setActionHandler("previoustrack", () => { playPreviousSong(); }); navigator.mediaSession.setActionHandler("nexttrack", () => { playNextSong(); }); } }, [currentSong, playNextSong, playPreviousSong, playSongByMediaSession]); // When the audio element is rendered on the screen, this function gets executed const audioElementCallbackRef = useCallback( (node) => { if (!node) return; audioRef.current = node; audioRef.current.ontimeupdate = (event) => { const { duration, currentTime } = event.srcElement; let progressPercent = (currentTime / duration) * 100; // calculate current time of a song const currentRunningTime = calculateCurrentTime(currentTime); setContinuousTime(progressPercent); setDisplayCurrentTime(currentRunningTime); }; audioRef.current.onended = () => { setPlaying(0); isPlayingRef.current = false; document.title = `Octave`; playNextSong(); }; // can also use oncanplay audioRef.current.onloadeddata = async () => { await audioRef.current.play(); setPlaying(1); isPlayingRef.current = true; setupMediaSession(); const durationTime = calculateDurationTime(audioRef.current.duration); setDisplayDurationTime(durationTime); }; }, [audioRef, playNextSong, setupMediaSession] ); const songProgressChanged = useCallback( (event, value) => { if (!currentSong) return; const newProgressSeconds = (value / 100) * audioRef.current.duration; audioRef.current.currentTime = newProgressSeconds; setContinuousTime(newProgressSeconds); }, [audioRef, currentSong] ); if (!show) return null; return ( <div className="player"> <div className="player__progress"> <Slider color="secondary" value={continuousTime} onChangeCommitted={songProgressChanged} /> </div> <div className="player__main"> <div className="player__song"> {currentSong?.imageUrl && ( <img src={currentSong.imageUrl} alt="" className="song__infoImage" /> )} <div className="song__info"> <p className="song__infoTrack">{currentSong?.name}</p> <p className="song__infoArtist">{currentSong?.artist}</p> </div> </div> <PlayerControls playPauseSong={playPauseSong} playing={playing} playNextSong={playNextSong} playPreviousSong={playPreviousSong} /> <div className="player__left"> <PlayerSongListButton /> <div className="player__duration"> <span className="duration__current">{displayCurrentTime}</span> <span>/</span> <span className="duration__total">{displayDurationTime}</span> </div> </div> </div> <audio ref={audioElementCallbackRef} src={currentSong?.url}></audio> </div> ); } export default Player;