|
"use client"; |
|
import { useEffect, useRef, useState } from "react"; |
|
import "./MoviePlayer.css"; |
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
|
import { |
|
faFastBackward, |
|
faFastForward, |
|
faPause, |
|
faPlay, |
|
faVolumeUp, |
|
faVolumeMute, |
|
faExpand, |
|
faDownload, |
|
faArrowLeft, |
|
faCaretLeft, |
|
faAngleLeft, |
|
} from "@fortawesome/free-solid-svg-icons"; |
|
import { Spinner } from "@/components/Spinner"; |
|
import SeekableProgressBar from "./SeekableProgressBar"; |
|
|
|
export default function MoviePlayer({ videoUrl, title }) { |
|
const videoRef = useRef(null); |
|
const [isPlaying, setIsPlaying] = useState(false); |
|
const [volume, setVolume] = useState(1); |
|
const [isMuted, setIsMuted] = useState(false); |
|
const [progress, setProgress] = useState(0); |
|
const [buffer, setBuffer] = useState(0); |
|
const [isFullscreen, setIsFullscreen] = useState(false); |
|
const [showControls, setShowControls] = useState(true); |
|
const [isBuffering, setIsBuffering] = useState(false); |
|
const overlayTimeout = useRef(null); |
|
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 }); |
|
const playerVersion = "0.0.2 Alpha"; |
|
|
|
useEffect(() => { |
|
const videoElement = videoRef.current; |
|
|
|
const handlePlay = () => setIsPlaying(true); |
|
const handlePause = () => setIsPlaying(false); |
|
const handleTimeUpdate = () => { |
|
setProgress((videoElement.currentTime / videoElement.duration) * 100); |
|
updateBuffer(); |
|
}; |
|
const handleWaiting = () => setIsBuffering(true); |
|
const handlePlaying = () => setIsBuffering(false); |
|
const handleProgress = () => updateBuffer(); |
|
|
|
videoElement.addEventListener("play", handlePlay); |
|
videoElement.addEventListener("pause", handlePause); |
|
videoElement.addEventListener("timeupdate", handleTimeUpdate); |
|
videoElement.addEventListener("waiting", handleWaiting); |
|
videoElement.addEventListener("playing", handlePlaying); |
|
videoElement.addEventListener("progress", handleProgress); |
|
|
|
return () => { |
|
videoElement.removeEventListener("play", handlePlay); |
|
videoElement.removeEventListener("pause", handlePause); |
|
videoElement.removeEventListener("timeupdate", handleTimeUpdate); |
|
videoElement.removeEventListener("waiting", handleWaiting); |
|
videoElement.removeEventListener("playing", handlePlaying); |
|
videoElement.removeEventListener("progress", handleProgress); |
|
}; |
|
}, []); |
|
|
|
useEffect(() => { |
|
if (showControls) { |
|
if (overlayTimeout.current) { |
|
clearTimeout(overlayTimeout.current); |
|
} |
|
overlayTimeout.current = setTimeout(() => setShowControls(false), 3000); |
|
} |
|
|
|
return () => clearTimeout(overlayTimeout.current); |
|
}, [showControls]); |
|
|
|
const handleContextMenu = (event) => { |
|
event.preventDefault(); |
|
setContextMenu({ |
|
visible: true, |
|
x: event.pageX, |
|
y: event.pageY, |
|
}); |
|
}; |
|
|
|
const hideContextMenu = () => { |
|
setContextMenu({ visible: false, x: 0, y: 0 }); |
|
}; |
|
|
|
useEffect(() => { |
|
window.addEventListener("click", hideContextMenu); |
|
return () => { |
|
window.removeEventListener("click", hideContextMenu); |
|
}; |
|
}, []); |
|
|
|
const handleFastForward = () => { |
|
if (videoRef.current) { |
|
videoRef.current.currentTime = Math.min( |
|
videoRef.current.duration, |
|
videoRef.current.currentTime + 10 |
|
); |
|
} |
|
}; |
|
|
|
const handleRewind = () => { |
|
if (videoRef.current) { |
|
videoRef.current.currentTime = Math.max( |
|
0, |
|
videoRef.current.currentTime - 10 |
|
); |
|
} |
|
}; |
|
|
|
const togglePlayPause = () => { |
|
if (isPlaying) { |
|
videoRef.current.pause(); |
|
setShowControls(true); |
|
} else { |
|
videoRef.current.play(); |
|
setShowControls(false); |
|
} |
|
}; |
|
|
|
const handleVolumeChange = (event) => { |
|
const volumeValue = event.target.value; |
|
setVolume(volumeValue); |
|
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 handleSeek = (newProgress) => { |
|
videoRef.current.currentTime = (newProgress / 100) * videoRef.current.duration; |
|
setProgress(newProgress); |
|
}; |
|
|
|
const handleMouseMove = () => { |
|
setShowControls(true); |
|
}; |
|
|
|
const formatTime = (seconds) => { |
|
if (isNaN(seconds)) { |
|
return '00:00:00'; |
|
} |
|
const wholeSeconds = Math.floor(seconds); |
|
const hours = Math.floor(wholeSeconds / 3600); |
|
const minutes = Math.floor((wholeSeconds % 3600) / 60); |
|
const secs = wholeSeconds % 60; |
|
|
|
const formattedHours = String(hours).padStart(2, '0'); |
|
const formattedMinutes = String(minutes).padStart(2, '0'); |
|
const formattedSeconds = String(secs).padStart(2, '0'); |
|
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; |
|
}; |
|
|
|
const updateBuffer = () => { |
|
const videoElement = videoRef.current; |
|
if (videoElement.buffered.length > 0) { |
|
const bufferEnd = videoElement.buffered.end(videoElement.buffered.length - 1); |
|
const bufferValue = (bufferEnd / videoElement.duration) * 100; |
|
setBuffer(bufferValue); |
|
} |
|
}; |
|
|
|
return ( |
|
<div className="video-player-container" onMouseMove={handleMouseMove} onContextMenu={handleContextMenu}> |
|
<div className={`player-overlay ${showControls ? "show" : "hide"}`}> |
|
<h2 className="video-title"><div className="control-btn"><FontAwesomeIcon icon={faAngleLeft} size="lg"/></div>{title}</h2> |
|
<div className="controls"> |
|
<div className="player-controls-top"> |
|
<label className="current-time">{formatTime(videoRef?.current?.currentTime)}</label> |
|
<SeekableProgressBar progress={progress} buffer={buffer} onSeek={handleSeek} /> |
|
<label className="duration">{formatTime(videoRef?.current?.duration)}</label> |
|
</div> |
|
<div className="player-controls-down"> |
|
<div className="player-controls-left"> |
|
<button |
|
onClick={handleRewind} |
|
className="control-btn" |
|
disabled={!isPlaying} |
|
> |
|
<FontAwesomeIcon icon={faFastBackward} size="xl" /> |
|
</button> |
|
<button onClick={togglePlayPause} className="play-pause-btn"> |
|
{isPlaying ? ( |
|
<FontAwesomeIcon icon={faPause} size="xl" /> |
|
) : ( |
|
<FontAwesomeIcon icon={faPlay} size="xl" /> |
|
)} |
|
</button> |
|
<button |
|
onClick={handleFastForward} |
|
className="control-btn" |
|
disabled={!isPlaying} |
|
> |
|
<FontAwesomeIcon icon={faFastForward} size="xl" /> |
|
</button> |
|
</div> |
|
<div className="player-controls-right"> |
|
<button onClick={toggleMute} className="control-btn"> |
|
<FontAwesomeIcon |
|
icon={isMuted ? faVolumeMute : faVolumeUp} |
|
size="xl" |
|
/> |
|
</button> |
|
<input |
|
type="range" |
|
className="volume-control" |
|
min="0" |
|
max="1" |
|
step="0.01" |
|
value={volume} |
|
onChange={handleVolumeChange} |
|
/> |
|
<button onClick={toggleFullscreen} className="control-btn"> |
|
<FontAwesomeIcon icon={faExpand} size="xl" /> |
|
</button> |
|
{videoUrl && ( |
|
<a href={videoUrl} download className="control-btn"> |
|
<FontAwesomeIcon icon={faDownload} size="xl" /> |
|
</a> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<video |
|
ref={videoRef} |
|
className="video-element" |
|
controls={false} |
|
preload="auto" |
|
src={videoUrl} |
|
autoPlay={true} |
|
> |
|
<track |
|
kind="subtitles" |
|
label="English" |
|
srcLang="en" |
|
src="path/to/your/subtitles.vtt" // Replace with actual subtitles URL |
|
/> |
|
Your browser does not support the video tag. |
|
</video> |
|
{isBuffering && <div className="buffering-indicator"><div className="postion-fix"><Spinner/></div></div>} |
|
{contextMenu.visible && ( |
|
<div |
|
className="context-menu" |
|
style={{ left: contextMenu.x, top: contextMenu.y }} |
|
> |
|
<ul> |
|
<li>Player Version: {playerVersion}</li> |
|
</ul> |
|
</div> |
|
)} |
|
</div> |
|
); |
|
} |
|
|