Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useRef } from "react"; | |
| import "./MusicPlayer.css"; | |
| import { useMusicPlayer } from "@/context/MusicPlayerContext"; | |
| import { IoPlayOutline, IoPauseOutline } from "react-icons/io5"; | |
| import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io"; | |
| import { | |
| TbRewindBackward5, | |
| TbRewindForward5, | |
| TbPlayerStop, | |
| TbVolumeOff, | |
| TbVolume, | |
| TbPlayerSkipBack, | |
| TbPlayerSkipForward, | |
| TbPlaylist, | |
| TbPlaylistOff, | |
| } from "react-icons/tb"; | |
| import { RiFullscreenLine, RiFullscreenExitLine } from "react-icons/ri"; | |
| import { formatTime } from "./utils"; | |
| export default function MusicPlayer() { | |
| const { | |
| videoRef, | |
| isPlayerVisible, | |
| src, | |
| isPlayerMaximized, | |
| togglePlayerSize, | |
| setIsPlayerVisible, | |
| initializePlayer, | |
| title, | |
| setNowPlaying, | |
| nowPlaying, | |
| didDestroy, | |
| setDidDestroy, | |
| playNext, | |
| playPrevious, | |
| canPlayPrevious, | |
| canPlayNext, | |
| isPlayEueOpen, | |
| setIsPlayEueOpen, | |
| } = useMusicPlayer(); | |
| const [currentSrc, setCurrentSrc] = useState(src); | |
| const [currentTime, setCurrentTime] = useState(0); | |
| const [duration, setDuration] = useState(0); | |
| const [progress, setProgress] = useState(0); | |
| const [bufferProgress, setBufferProgress] = useState(0); | |
| const [isPlaying, setIsPlaying] = useState(false); | |
| const [showControls, setShowControls] = useState(true); | |
| const [isBuffering, setIsBuffering] = useState(true); | |
| const [volume, setVolume] = useState(1); | |
| const [isMuted, setIsMuted] = useState(false); | |
| const [isFullscreen, setIsFullscreen] = useState(false); | |
| const overlayTimeout = useRef(null); | |
| const seekTime = 5; | |
| const poster = "https://dlcdnwebimgs.asus.com/gain/4BB18AEF-347E-4DB6-B78C-C0FFE1F20385/w750/h470" | |
| // Event Handlers | |
| const handleTimeUpdate = (videoElement) => { | |
| if (videoElement) { | |
| setCurrentTime(videoElement.currentTime); | |
| setProgress((videoElement.currentTime / videoElement.duration) * 100); | |
| } | |
| }; | |
| const handleLoadedMetadata = (videoElement) => { | |
| if (videoElement) { | |
| setDuration(videoElement.duration); | |
| setProgress(0); | |
| setCurrentTime(0); | |
| videoElement.play(); | |
| } | |
| }; | |
| const handleProgress = (videoElement) => { | |
| if (videoElement && videoElement.buffered.length > 0) { | |
| const bufferEnd = videoElement.buffered.end( | |
| videoElement.buffered.length - 1 | |
| ); | |
| const bufferValue = (bufferEnd / videoElement.duration) * 100; | |
| setBufferProgress(bufferValue); | |
| } | |
| }; | |
| const handlePlay = () => { | |
| setIsPlaying(true); | |
| setNowPlaying(title); | |
| }; | |
| const handlePause = () => { | |
| setIsPlaying(false); | |
| setNowPlaying(""); | |
| }; | |
| const attachEventListeners = (videoElement) => { | |
| if (videoElement) { | |
| videoElement.addEventListener("timeupdate", () => | |
| handleTimeUpdate(videoElement) | |
| ); | |
| videoElement.addEventListener("loadedmetadata", () => | |
| handleLoadedMetadata(videoElement) | |
| ); | |
| videoElement.addEventListener("progress", () => | |
| handleProgress(videoElement) | |
| ); | |
| videoElement.addEventListener("play", handlePlay); | |
| videoElement.addEventListener("pause", handlePause); | |
| } | |
| }; | |
| const detachEventListeners = (videoElement) => { | |
| if (videoElement) { | |
| videoElement.removeEventListener("timeupdate", () => | |
| handleTimeUpdate(videoElement) | |
| ); | |
| videoElement.removeEventListener("loadedmetadata", () => | |
| handleLoadedMetadata(videoElement) | |
| ); | |
| videoElement.removeEventListener("progress", () => | |
| handleProgress(videoElement) | |
| ); | |
| videoElement.removeEventListener("play", handlePlay); | |
| videoElement.removeEventListener("pause", handlePause); | |
| } | |
| }; | |
| useEffect(() => { | |
| const videoElement = videoRef.current; | |
| // Attach event listeners | |
| attachEventListeners(videoElement); | |
| return () => { | |
| // Detach event listeners | |
| detachEventListeners(videoElement); | |
| }; | |
| }, [videoRef, currentSrc, nowPlaying, didDestroy]); | |
| useEffect(() => { | |
| if (src !== currentSrc) { | |
| setCurrentSrc(src); | |
| } | |
| }, [src, currentSrc]); | |
| useEffect(() => { | |
| if ('mediaSession' in navigator) { | |
| navigator.mediaSession.metadata = new MediaMetadata({ | |
| title: title, | |
| artwork: [ | |
| { src: poster } // Ensure 'poster' is defined in your component | |
| ] | |
| }); | |
| navigator.mediaSession.setActionHandler('play', () => { | |
| videoRef.current.play(); | |
| }); | |
| navigator.mediaSession.setActionHandler('pause', () => { | |
| videoRef.current.pause(); | |
| }); | |
| navigator.mediaSession.setActionHandler('seekbackward', (details) => { | |
| videoRef.current.currentTime = Math.max(videoRef.current.currentTime - (details.seekOffset || 10), 0); | |
| }); | |
| navigator.mediaSession.setActionHandler('seekforward', (details) => { | |
| videoRef.current.currentTime = Math.min(videoRef.current.currentTime + (details.seekOffset || 10), videoRef.current.duration); | |
| }); | |
| navigator.mediaSession.setActionHandler('stop', () => { | |
| destroyPlayer(); | |
| }); | |
| // New action handlers for previous and next | |
| navigator.mediaSession.setActionHandler('previoustrack', () => { | |
| playPrevious(); // Assuming playPrevious is a function that plays the previous track | |
| }); | |
| navigator.mediaSession.setActionHandler('nexttrack', () => { | |
| playNext(); // Assuming playNext is a function that plays the next track | |
| }); | |
| } | |
| // Cleanup function to reset media session when component unmounts | |
| return () => { | |
| if ('mediaSession' in navigator) { | |
| navigator.mediaSession.metadata = null; | |
| navigator.mediaSession.setActionHandler('play', null); | |
| navigator.mediaSession.setActionHandler('pause', null); | |
| navigator.mediaSession.setActionHandler('seekbackward', null); | |
| navigator.mediaSession.setActionHandler('seekforward', null); | |
| navigator.mediaSession.setActionHandler('stop', null); | |
| navigator.mediaSession.setActionHandler('previoustrack', null); | |
| navigator.mediaSession.setActionHandler('nexttrack', null); | |
| } | |
| }; | |
| }, [title, poster, playPrevious, playNext]); | |
| useEffect(() => { | |
| if (showControls) { | |
| if (overlayTimeout.current) { | |
| clearTimeout(overlayTimeout.current); | |
| } | |
| overlayTimeout.current = setTimeout(() => setShowControls(false), 3000); | |
| } | |
| return () => clearTimeout(overlayTimeout.current); | |
| }, [showControls]); | |
| const togglePlayPause = () => { | |
| if (videoRef.current) { | |
| if (isPlaying) { | |
| videoRef.current.pause(); | |
| } else { | |
| videoRef.current.play(); | |
| } | |
| setIsPlaying(!isPlaying); | |
| } | |
| }; | |
| const handleVolumeChange = (event) => { | |
| let volumeValue = parseFloat(event.target.value); | |
| // Ensure the volume is a finite number between 0 and 1 | |
| volumeValue = Math.max(0, Math.min(1, volumeValue)); | |
| setVolume(volumeValue); | |
| if (videoRef.current) { | |
| videoRef.current.volume = volumeValue; | |
| } | |
| setIsMuted(volumeValue === 0); | |
| }; | |
| const toggleMute = () => { | |
| if (isMuted) { | |
| videoRef.current.volume = volume; | |
| setIsMuted(false); | |
| } else { | |
| videoRef.current.volume = 0; | |
| setIsMuted(true); | |
| } | |
| }; | |
| const toggleFullscreen = () => { | |
| const doc = window.document; | |
| const docEl = doc.documentElement; | |
| const requestFullscreen = | |
| docEl.requestFullscreen || | |
| docEl.mozRequestFullScreen || | |
| docEl.webkitRequestFullscreen || | |
| docEl.msRequestFullscreen; | |
| const exitFullscreen = | |
| doc.exitFullscreen || | |
| doc.mozCancelFullScreen || | |
| docEl.webkitExitFullscreen || | |
| doc.msExitFullscreen; | |
| if (!isFullscreen) { | |
| requestFullscreen.call(docEl); | |
| } else { | |
| exitFullscreen.call(doc); | |
| } | |
| setIsFullscreen(!isFullscreen); | |
| }; | |
| const destroyPlayer = () => { | |
| if (videoRef.current) { | |
| videoRef.current.pause(); | |
| videoRef.current.currentTime = 0; | |
| videoRef.current.removeAttribute("src"); // Clear the source | |
| videoRef.current.load(); // Reload to reset duration/currentTime | |
| setNowPlaying(""); | |
| setCurrentSrc("/reset"); | |
| setCurrentTime(0); | |
| setDuration(0); | |
| setProgress(0); | |
| setBufferProgress(0); | |
| setIsPlaying(false); | |
| setIsPlayerVisible(false); | |
| setDidDestroy(true); | |
| console.log("setting didDestroy to true"); | |
| } | |
| }; | |
| const handleProgressClick = (e) => { | |
| const progressBar = e.currentTarget; | |
| const rect = progressBar.getBoundingClientRect(); | |
| const clickX = e.clientX - rect.left; | |
| const newTime = (clickX / rect.width) * duration; | |
| if (videoRef.current) { | |
| videoRef.current.currentTime = newTime; | |
| setCurrentTime(newTime); | |
| } | |
| }; | |
| const handleFastForward = () => { | |
| if (videoRef.current) { | |
| videoRef.current.currentTime = Math.min( | |
| videoRef.current.duration, | |
| videoRef.current.currentTime + seekTime | |
| ); | |
| } | |
| }; | |
| const handleRewind = () => { | |
| if (videoRef.current) { | |
| videoRef.current.currentTime = Math.max( | |
| 0, | |
| videoRef.current.currentTime - seekTime | |
| ); | |
| } | |
| }; | |
| const handleMouseMove = () => { | |
| setShowControls(true); | |
| }; | |
| const handleIsPlayQueueOpen = () =>{ | |
| setIsPlayEueOpen(!isPlayEueOpen); | |
| }; | |
| if (!isPlayerVisible || !currentSrc) return null; | |
| return ( | |
| <div | |
| className={ | |
| isPlayerMaximized | |
| ? "player-maximized relative" | |
| : "player-minimized relative" | |
| } | |
| onMouseMove={handleMouseMove} | |
| > | |
| {isPlayerMaximized && ( | |
| <div className={`player-controls ${showControls ? "show" : "hide"}`}> | |
| <div className="player-controls-top"> | |
| <div className="name-container"> | |
| <label className="music-title">{title}</label> | |
| <button className="player-max-button" onClick={togglePlayerSize}> | |
| <IoIosArrowDown /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="player-controls-center"> | |
| <button className="player-max-button" onClick={handleRewind}> | |
| <TbRewindBackward5 /> | |
| </button> | |
| <button className="player-max-button" onClick={togglePlayPause}> | |
| {isPlaying ? <IoPauseOutline /> : <IoPlayOutline />} | |
| </button> | |
| <button className="player-max-button" onClick={handleFastForward}> | |
| <TbRewindForward5 /> | |
| </button> | |
| </div> | |
| <div className="player-controls-bottom"> | |
| <div className="player-control-bottom-top"> | |
| <label className="current-time">{formatTime(currentTime)}</label> | |
| <div | |
| className="progress-bar-container" | |
| onClick={handleProgressClick} | |
| > | |
| <div | |
| className="buffer-bar" | |
| style={{ width: `${bufferProgress}%` }} | |
| /> | |
| <div | |
| className="progress-bar" | |
| style={{ width: `${progress}%` }} | |
| /> | |
| </div> | |
| <label className="duration">{formatTime(duration)}</label> | |
| </div> | |
| <div className="player-controls-down"> | |
| <div className="player-controls-left"> | |
| <button | |
| onClick={toggleMute} | |
| className="player-min-button volumn-btn" | |
| > | |
| {isMuted ? <TbVolumeOff /> : <TbVolume />} | |
| </button> | |
| <input | |
| type="range" | |
| className="volume-control" | |
| min="0" | |
| max="1" | |
| step="0.01" | |
| value={volume} | |
| onChange={handleVolumeChange} | |
| /> | |
| <button | |
| onClick={playPrevious} | |
| className="previous-btn player-min-button" | |
| disabled={!canPlayPrevious} | |
| > | |
| <TbPlayerSkipBack /> | |
| </button> | |
| <button | |
| onClick={playNext} | |
| className="next-btn player-min-button" | |
| disabled={!canPlayNext} | |
| > | |
| <TbPlayerSkipForward /> | |
| </button> | |
| <button className="player-min-button" onClick={destroyPlayer}> | |
| <TbPlayerStop /> | |
| </button> | |
| {isPlayEueOpen ? ( | |
| <button className="playlist-btn player-min-button" onClick={handleIsPlayQueueOpen}> | |
| <TbPlaylist /> | |
| </button> | |
| ) : ( | |
| <button | |
| className="playlist-btn player-min-button" | |
| onClick={handleIsPlayQueueOpen} | |
| > | |
| <TbPlaylistOff /> | |
| </button> | |
| )} | |
| </div> | |
| <div className="player-controls-right"> | |
| <button | |
| onClick={toggleFullscreen} | |
| className="player-min-button" | |
| > | |
| {isFullscreen ? ( | |
| <RiFullscreenExitLine /> | |
| ) : ( | |
| <RiFullscreenLine /> | |
| )} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="player-top"> | |
| <video | |
| ref={videoRef} | |
| src={currentSrc} | |
| poster={poster} | |
| preload="metadata" | |
| className="video-element" | |
| autoPlay | |
| onClick={isPlayerMaximized ? null : togglePlayerSize} | |
| /> | |
| {!isPlayerMaximized && ( | |
| <div className="player-controls-mini"> | |
| <div className="player-mini-control-top"> | |
| <label className="music-title player-mini">{title}</label> | |
| <button className="player-min-button" onClick={togglePlayerSize}> | |
| <IoIosArrowUp /> | |
| </button> | |
| </div> | |
| <div className="player-mini-control-center"> | |
| <button | |
| onClick={playPrevious} | |
| className="previous-btn player-min-button" | |
| disabled={!canPlayPrevious} | |
| > | |
| <TbPlayerSkipBack /> | |
| </button> | |
| <button className="player-min-button" onClick={handleRewind}> | |
| <TbRewindBackward5 /> | |
| </button> | |
| <button className="player-min-button" onClick={togglePlayPause}> | |
| {isPlaying ? <IoPauseOutline /> : <IoPlayOutline />} | |
| </button> | |
| <button className="player-min-button" onClick={handleFastForward}> | |
| <TbRewindForward5 /> | |
| </button> | |
| <button | |
| onClick={playNext} | |
| className="next-btn player-min-button" | |
| disabled={!canPlayNext} | |
| > | |
| <TbPlayerSkipForward /> | |
| </button> | |
| <button className="player-min-button" onClick={destroyPlayer}> | |
| <TbPlayerStop /> | |
| </button> | |
| </div> | |
| <div className="player-mini-volume"> | |
| <button | |
| onClick={toggleMute} | |
| className="player-min-button volumn-btn" | |
| > | |
| {isMuted ? <TbVolumeOff /> : <TbVolume />} | |
| </button> | |
| <input | |
| type="range" | |
| className="volume-control" | |
| min="0" | |
| max="1" | |
| step="0.01" | |
| value={volume} | |
| onChange={handleVolumeChange} | |
| /> | |
| {isPlayEueOpen ? ( | |
| <button className="playlist-btn player-min-button" onClick={handleIsPlayQueueOpen}> | |
| <TbPlaylist /> | |
| </button> | |
| ) : ( | |
| <button | |
| className="playlist-btn player-min-button" | |
| onClick={handleIsPlayQueueOpen} | |
| > | |
| <TbPlaylistOff /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {!isPlayerMaximized && ( | |
| <div className="player-mini-control-bottom"> | |
| <label className="current-time">{formatTime(currentTime)}</label> | |
| <div | |
| className="progress-bar-container player-mini" | |
| onClick={handleProgressClick} | |
| > | |
| <div | |
| className="buffer-bar" | |
| style={{ width: `${bufferProgress}%` }} | |
| /> | |
| <div className="progress-bar" style={{ width: `${progress}%` }} /> | |
| </div> | |
| <label className="duration">{formatTime(duration)}</label> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |