"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import "./Player.css"; import { Spinner } from "@/components/shared/Spinner/Spinner"; import SeekableProgressBar from "@/components/shared/ProgressBar/SeekableProgressBar"; import { getFileNameWithoutExtension, formatTime, getStorageKey, } from "./utils"; export default function Player({ videoUrl, title, type, episode = null, videoThumbnail=null }) { const router = useRouter(); 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(true); const overlayTimeout = useRef(null); const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, }); const playerVersion = "0.0.4 Alpha"; const seekTime = 5; useEffect(() => { if ('mediaSession' in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title: episode? episode: title, artwork: [ { src: videoThumbnail, sizes: '680x1000', type: 'image/png' } ] }); 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', () => { videoRef.current.pause(); videoRef.current.currentTime = 0; }); } }, [title, videoThumbnail]); useEffect(() => { const videoElement = videoRef.current; const savedData = localStorage.getItem(getStorageKey(type, title, episode)); if (savedData) { const { currentTime, duration } = JSON.parse(savedData); videoElement.currentTime = parseFloat(currentTime); } const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); const handleTimeUpdate = () => { const duration = videoElement.duration; const currentTime = videoElement.currentTime; setProgress((currentTime / duration) * 100); localStorage.setItem( getStorageKey(type, title, episode), JSON.stringify({ currentTime, duration }) ); updateBuffer(); // Update buffer on time update }; const handleWaiting = () => setIsBuffering(true); const handlePlaying = () => setIsBuffering(false); const handleProgress = () => updateBuffer(); // Update buffer on progress const handleLoadedData = () => { setIsBuffering(false); // Handle initial data load const duration = videoElement.duration; const currentTime = videoElement.currentTime; localStorage.setItem( getStorageKey(type, title, episode), JSON.stringify({ currentTime, duration }) ); }; const handleCanPlayThrough = () => setIsBuffering(false); // Handle when video can play through videoElement.addEventListener("play", handlePlay); videoElement.addEventListener("pause", handlePause); videoElement.addEventListener("timeupdate", handleTimeUpdate); videoElement.addEventListener("waiting", handleWaiting); videoElement.addEventListener("playing", handlePlaying); videoElement.addEventListener("progress", handleProgress); // Listen to progress events videoElement.addEventListener("loadeddata", handleLoadedData); // Listen for initial load videoElement.addEventListener("canplaythrough", handleCanPlayThrough); // Listen for full preload 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); // Clean up progress listener videoElement.removeEventListener("loadeddata", handleLoadedData); // Clean up initial load listener videoElement.removeEventListener("canplaythrough", handleCanPlayThrough); // Clean up full preload listener }; }, []); useEffect(() => { if (showControls) { if (overlayTimeout.current) { clearTimeout(overlayTimeout.current); } overlayTimeout.current = setTimeout(() => setShowControls(false), 3000); } return () => clearTimeout(overlayTimeout.current); }, [showControls]); useEffect(() => { const handleKeyDown = (event) => { const videoElement = videoRef.current; switch (event.key) { case " ": event.preventDefault(); togglePlayPause(); break; case "ArrowRight": handleFastForward(); break; case "ArrowLeft": handleRewind(); break; case "ArrowUp": changeVolume(0.1); break; case "ArrowDown": changeVolume(-0.1); break; case "m": toggleMute(); break; case "f": toggleFullscreen(); break; case "Home": videoElement.currentTime = 0; break; case "End": videoElement.currentTime = videoElement.duration; break; case "j": changePlaybackRate(-0.1); break; case "l": changePlaybackRate(0.1); break; case "k": resetPlaybackRate(); break; case "c": toggleCaptions(); break; case "s": toggleControls(); break; default: break; } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [isPlaying, volume, isMuted, isFullscreen]); const changePlaybackRate = (amount) => { const newRate = Math.min( 2, Math.max(0.5, videoRef.current.playbackRate + amount) ); videoRef.current.playbackRate = newRate; }; const resetPlaybackRate = () => { videoRef.current.playbackRate = 1; }; const toggleCaptions = () => { const tracks = videoRef.current.textTracks; for (let i = 0; i < tracks.length; i++) { tracks[i].mode = tracks[i].mode === "showing" ? "hidden" : "showing"; } }; const toggleControls = () => { setShowControls((prevShowControls) => !prevShowControls); }; const changeVolume = (amount) => { let newVolume = Math.min(1, Math.max(0, volume + amount)); setVolume(newVolume); videoRef.current.volume = newVolume; setIsMuted(newVolume === 0); }; 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 + seekTime ); } }; const handleRewind = () => { if (videoRef.current) { videoRef.current.currentTime = Math.max( 0, videoRef.current.currentTime - seekTime ); } }; const togglePlayPause = () => { if (isPlaying) { videoRef.current.pause(); videoRef.current.classList.remove("playing"); videoRef.current.classList.add("paused"); setShowControls(true); } else { videoRef.current.play(); videoRef.current.classList.remove("paused"); videoRef.current.classList.add("playing"); setShowControls(false); } }; const handleVolumeChange = (event) => { let volumeValue = parseFloat(event.target.value); // Ensure the volume is a finite number between 0 and 1 if (!isFinite(volumeValue) || volumeValue < 0) { volumeValue = 0; } else if (volumeValue > 1) { volumeValue = 1; } 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 handleSeek = (newProgress) => { videoRef.current.currentTime = (newProgress / 100) * videoRef.current.duration; setProgress(newProgress); }; const handleMouseMove = () => { setShowControls(true); }; 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); } }; const handleExitClick = () => { router.back(); }; return (
); }