ChandimaPrabath's picture
player patch
b16295e
"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 (
<div
className="video-player-container"
onMouseMove={handleMouseMove}
onContextMenu={handleContextMenu}
>
<div className="player-indication-overlay"></div>
<div className={`player-overlay ${showControls ? "show" : "hide"}`}>
<div className="video-title">
<div className="control-btn player-exit" onClick={handleExitClick}>
<span className="material-symbols-outlined medium">
keyboard_tab_rtl
</span>
</div>
<label className="video-title-text">
{episode ? getFileNameWithoutExtension(episode) : title}
</label>
</div>
<div className="player-controls-center">
<button onClick={handleRewind} className="control-btn">
<label className="rewind-label-left">{seekTime}s</label>
<span className="material-symbols-outlined large">rotate_left</span>
</button>
<button onClick={togglePlayPause} className="play-pause-btn">
{isPlaying ? (
<span className="material-symbols-outlined large">pause</span>
) : (
<span className="material-symbols-outlined large">
play_arrow
</span>
)}
</button>
<button onClick={handleFastForward} className="control-btn">
<span className="material-symbols-outlined large">
rotate_right
</span>
<label className="rewind-label-right">{seekTime}s</label>
</button>
</div>
<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={toggleMute} className="control-btn volumn-btn">
{isMuted ? (
<span className="material-symbols-outlined small">
volume_off
</span>
) : (
<span className="material-symbols-outlined small">
volume_up
</span>
)}
</button>
<input
type="range"
className="volume-control"
min="0"
max="1"
step="0.01"
value={volume}
onChange={handleVolumeChange}
/>
<button
onClick={handleRewind}
className="previous-btn control-btn"
>
<span className="material-symbols-outlined small">
skip_previous
</span>
</button>
<button
onClick={handleFastForward}
className="next-btn control-btn"
>
<span className="material-symbols-outlined small">
skip_next
</span>
</button>
{isPlaying ? (
<button className="playlist-btn control-btn">
<span className="material-symbols-outlined small">
featured_play_list
</span>
</button>
) : (
<button className="playlist-btn control-btn" disabled={true}>
<span className="material-symbols-outlined small">
featured_play_list
</span>
</button>
)}
</div>
<div className="player-controls-right">
{isPlaying ? (
<button className="cc-btn control-btn">
<span className="material-symbols-outlined small">
closed_caption
</span>
</button>
) : (
<button className="cc-btn control-btn" disabled={true}>
<span className="material-symbols-outlined small">
closed_caption_disabled
</span>
</button>
)}
<button onClick={toggleFullscreen} className="control-btn">
{isFullscreen ? (
<span className="material-symbols-outlined small">
fullscreen_exit
</span>
) : (
<span className="material-symbols-outlined small">
fullscreen
</span>
)}
</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}
src={videoUrl}
autoPlay={true}
>
{/* <track
kind="subtitles"
label="English"
srcLang="en"
src="/My.Spy.The.Eternal.City.(2024).WEB.vtt"
default
/> */}
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>
);
}