UNPKG

@streamspark/react-video-player

Version:

A fully-featured, YouTube-like video player built completely from scratch using React and TypeScript.

1,032 lines (990 loc) 33.9 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ var defaultAttributes = { xmlns: "http://www.w3.org/2000/svg", width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }; /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().trim(); const createLucideIcon = (iconName, iconNode) => { const Component = react.forwardRef( ({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, ...rest }, ref) => { return react.createElement( "svg", { ref, ...defaultAttributes, width: size, height: size, stroke: color, strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth, className: ["lucide", `lucide-${toKebabCase(iconName)}`, className].join(" "), ...rest }, [ ...iconNode.map(([tag, attrs]) => react.createElement(tag, attrs)), ...Array.isArray(children) ? children : [children] ] ); } ); Component.displayName = `${iconName}`; return Component; }; /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Maximize = createLucideIcon("Maximize", [ ["path", { d: "M8 3H5a2 2 0 0 0-2 2v3", key: "1dcmit" }], ["path", { d: "M21 8V5a2 2 0 0 0-2-2h-3", key: "1e4gt3" }], ["path", { d: "M3 16v3a2 2 0 0 0 2 2h3", key: "wsl5sc" }], ["path", { d: "M16 21h3a2 2 0 0 0 2-2v-3", key: "18trek" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Minimize = createLucideIcon("Minimize", [ ["path", { d: "M8 3v3a2 2 0 0 1-2 2H3", key: "hohbtr" }], ["path", { d: "M21 8h-3a2 2 0 0 1-2-2V3", key: "5jw1f3" }], ["path", { d: "M3 16h3a2 2 0 0 1 2 2v3", key: "198tvr" }], ["path", { d: "M16 21v-3a2 2 0 0 1 2-2h3", key: "ph8mxp" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Pause = createLucideIcon("Pause", [ ["rect", { width: "4", height: "16", x: "6", y: "4", key: "iffhe4" }], ["rect", { width: "4", height: "16", x: "14", y: "4", key: "sjin7j" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Play = createLucideIcon("Play", [ ["polygon", { points: "5 3 19 12 5 21 5 3", key: "191637" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Settings = createLucideIcon("Settings", [ [ "path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z", key: "1qme2f" } ], ["circle", { cx: "12", cy: "12", r: "3", key: "1v7zrd" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Type = createLucideIcon("Type", [ ["polyline", { points: "4 7 4 4 20 4 20 7", key: "1nosan" }], ["line", { x1: "9", x2: "15", y1: "20", y2: "20", key: "swin9y" }], ["line", { x1: "12", x2: "12", y1: "4", y2: "20", key: "1tx1rr" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Volume1 = createLucideIcon("Volume1", [ ["polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5", key: "16drj5" }], ["path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07", key: "ltjumu" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Volume2 = createLucideIcon("Volume2", [ ["polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5", key: "16drj5" }], ["path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07", key: "ltjumu" }], ["path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14", key: "1kegas" }] ]); /** * @license lucide-react v0.344.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const VolumeX = createLucideIcon("VolumeX", [ ["polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5", key: "16drj5" }], ["line", { x1: "22", x2: "16", y1: "9", y2: "15", key: "1ewh16" }], ["line", { x1: "16", x2: "22", y1: "9", y2: "15", key: "5ykzw1" }] ]); const SeekBar = ({ currentTime, duration, buffered, onSeek, formatTime }) => { const [isDragging, setIsDragging] = react.useState(false); const [hoverTime, setHoverTime] = react.useState(0); const [showPreview, setShowPreview] = react.useState(false); const seekBarRef = react.useRef(null); const progress = duration > 0 ? currentTime / duration * 100 : 0; const handleMouseDown = (e) => { setIsDragging(true); handleSeek(e); }; const handleMouseMove = (e) => { if (!seekBarRef.current) return; const rect = seekBarRef.current.getBoundingClientRect(); const percentage = Math.max(0, Math.min(100, (e.clientX - rect.left) / rect.width * 100)); const time = percentage / 100 * duration; setHoverTime(time); setShowPreview(true); if (isDragging) { onSeek(time); } }; const handleMouseUp = (e) => { if (isDragging) { handleSeek(e); setIsDragging(false); } }; const handleMouseLeave = () => { setShowPreview(false); setIsDragging(false); }; const handleSeek = (e) => { if (!seekBarRef.current) return; const rect = seekBarRef.current.getBoundingClientRect(); const percentage = Math.max(0, Math.min(100, (e.clientX - rect.left) / rect.width * 100)); const time = percentage / 100 * duration; onSeek(time); }; return /* @__PURE__ */ jsxRuntime.jsxs( "div", { className: "rvp-seek-bar-container", ref: seekBarRef, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, role: "slider", "aria-label": "Seek", "aria-valuemin": 0, "aria-valuemax": duration, "aria-valuenow": currentTime, tabIndex: 0, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-seek-bar", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-seek-bar-background", children: [ /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-seek-bar-buffered", style: { width: `${buffered}%` } } ), /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-seek-bar-progress", style: { width: `${progress}%` } } ), /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-seek-bar-thumb", style: { left: `${progress}%` } } ) ] }) }), showPreview && /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-seek-preview", style: { left: `${hoverTime / duration * 100}%`, transform: "translateX(-50%)" }, children: formatTime(hoverTime) } ) ] } ); }; const Volume = ({ volume, isMuted, onVolumeChange, onMute }) => { const [isDragging, setIsDragging] = react.useState(false); const [showSlider, setShowSlider] = react.useState(false); const sliderRef = react.useRef(null); const getVolumeIcon = () => { if (isMuted || volume === 0) return /* @__PURE__ */ jsxRuntime.jsx(VolumeX, { size: 20 }); if (volume < 0.5) return /* @__PURE__ */ jsxRuntime.jsx(Volume1, { size: 20 }); return /* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 20 }); }; const handleMouseDown = (e) => { e.preventDefault(); setIsDragging(true); handleVolumeChange(e); }; const handleMouseMove = (e) => { if (isDragging) { handleVolumeChange(e); } }; const handleMouseUp = () => { setIsDragging(false); }; const handleVolumeChange = (e) => { if (!sliderRef.current) return; const rect = sliderRef.current.getBoundingClientRect(); const percentage = Math.max(0, Math.min(100, (rect.bottom - e.clientY) / rect.height * 100)); const newVolume = percentage / 100; onVolumeChange(newVolume); }; return /* @__PURE__ */ jsxRuntime.jsxs( "div", { className: "rvp-volume-control", onMouseEnter: () => setShowSlider(true), onMouseLeave: () => { if (!isDragging) setShowSlider(false); }, children: [ /* @__PURE__ */ jsxRuntime.jsx( "button", { className: "rvp-control-btn rvp-volume-btn", onClick: onMute, "aria-label": isMuted ? "Unmute" : "Mute", children: getVolumeIcon() } ), /* @__PURE__ */ jsxRuntime.jsx("div", { className: `rvp-volume-slider ${showSlider ? "rvp-show" : ""}`, children: /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-volume-slider-track", ref: sliderRef, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, role: "slider", "aria-label": "Volume", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round((isMuted ? 0 : volume) * 100), tabIndex: 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-volume-slider-background", children: [ /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-volume-slider-fill", style: { height: `${isMuted ? 0 : volume * 100}%` } } ), /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "rvp-volume-slider-thumb", style: { bottom: `${isMuted ? 0 : volume * 100}%` } } ) ] }) } ) }) ] } ); }; const PLAYBACK_RATES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; const PlaybackSpeed = ({ playbackRate, onPlaybackRateChange }) => { const [showMenu, setShowMenu] = react.useState(false); return /* @__PURE__ */ jsxRuntime.jsxs( "div", { className: "rvp-playback-speed", onMouseEnter: () => setShowMenu(true), onMouseLeave: () => setShowMenu(false), children: [ /* @__PURE__ */ jsxRuntime.jsxs( "button", { className: "rvp-control-btn rvp-speed-btn", "aria-label": `Playback speed: ${playbackRate}x`, children: [ /* @__PURE__ */ jsxRuntime.jsx(Settings, { size: 20 }), /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rvp-speed-label", children: [ playbackRate, "x" ] }) ] } ), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rvp-speed-menu ${showMenu ? "rvp-show" : ""}`, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-speed-menu-title", children: "Playback Speed" }), PLAYBACK_RATES.map((rate) => /* @__PURE__ */ jsxRuntime.jsxs( "button", { className: `rvp-speed-option ${rate === playbackRate ? "rvp-active" : ""}`, onClick: () => onPlaybackRateChange(rate), "aria-label": `Set speed to ${rate}x`, children: [ rate, "x ", rate === 1 && "(Normal)" ] }, rate )) ] }) ] } ); }; const Controls = ({ isPlaying, currentTime, duration, volume, isMuted, playbackRate, isFullscreen, captionsEnabled, buffered, hasCaptions, onPlay, onSeek, onVolumeChange, onMute, onPlaybackRateChange, onFullscreen, onToggleCaptions, formatTime }) => { return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-video-controls", children: [ /* @__PURE__ */ jsxRuntime.jsx( SeekBar, { currentTime, duration, buffered, onSeek, formatTime } ), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-controls-bar", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-controls-left", children: [ /* @__PURE__ */ jsxRuntime.jsx( "button", { className: "rvp-control-btn rvp-play-btn", onClick: onPlay, "aria-label": isPlaying ? "Pause" : "Play", children: isPlaying ? /* @__PURE__ */ jsxRuntime.jsx(Pause, { size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(Play, { size: 24 }) } ), /* @__PURE__ */ jsxRuntime.jsx( Volume, { volume, isMuted, onVolumeChange, onMute } ), /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-time-display", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [ formatTime(currentTime), " / ", formatTime(duration) ] }) }) ] }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-controls-right", children: [ hasCaptions && /* @__PURE__ */ jsxRuntime.jsx( "button", { className: `rvp-control-btn ${captionsEnabled ? "rvp-active" : ""}`, onClick: onToggleCaptions, title: "Toggle Captions", "aria-label": "Toggle Captions", children: /* @__PURE__ */ jsxRuntime.jsx(Type, { size: 20 }) } ), /* @__PURE__ */ jsxRuntime.jsx( PlaybackSpeed, { playbackRate, onPlaybackRateChange } ), /* @__PURE__ */ jsxRuntime.jsx( "button", { className: "rvp-control-btn", onClick: onFullscreen, "aria-label": isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen", children: isFullscreen ? /* @__PURE__ */ jsxRuntime.jsx(Minimize, { size: 20 }) : /* @__PURE__ */ jsxRuntime.jsx(Maximize, { size: 20 }) } ) ] }) ] }) ] }); }; const LoadingSpinner = () => { return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-video-loading", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-loading-spinner", "aria-label": "Loading video" }) }); }; const useVideo = (videoRef, containerRef, options) => { const [isPlaying, setIsPlaying] = react.useState(false); const [currentTime, setCurrentTime] = react.useState(0); const [duration, setDuration] = react.useState(0); const [volume, setVolume] = react.useState(1); const [isMuted, setIsMuted] = react.useState(false); const [playbackRate, setPlaybackRate] = react.useState(1); const [isFullscreen, setIsFullscreen] = react.useState(false); const [captionsEnabled, setCaptionsEnabled] = react.useState(false); const [isLoading, setIsLoading] = react.useState(true); const [buffered, setBuffered] = react.useState(0); const lastVolumeRef = react.useRef(1); const lastCurrentTimeRef = react.useRef(0); const togglePlay = react.useCallback(() => { if (!videoRef.current) return; if (isPlaying) { videoRef.current.pause(); } else { videoRef.current.play().catch((error) => { console.error("Error playing video:", error); options?.onError?.(error.message); }); } options?.onPlayPauseFeedback?.(); }, [isPlaying, videoRef, options]); const seek = react.useCallback((time) => { if (!videoRef.current) return; const clampedTime = Math.max(0, Math.min(duration, time)); const timeDiff = clampedTime - lastCurrentTimeRef.current; videoRef.current.currentTime = clampedTime; setCurrentTime(clampedTime); lastCurrentTimeRef.current = clampedTime; options?.onSeek?.(clampedTime); if (Math.abs(timeDiff) >= 5) { options?.onSeekFeedback?.(timeDiff); } }, [videoRef, duration, options]); const setVolumeValue = react.useCallback((newVolume) => { if (!videoRef.current) return; const clampedVolume = Math.max(0, Math.min(1, newVolume)); videoRef.current.volume = clampedVolume; setVolume(clampedVolume); if (clampedVolume > 0) { lastVolumeRef.current = clampedVolume; if (isMuted) { setIsMuted(false); videoRef.current.muted = false; } } options?.onVolumeChange?.(clampedVolume); options?.onVolumeFeedback?.(clampedVolume, isMuted && clampedVolume === 0); }, [videoRef, isMuted, options]); const toggleMute = react.useCallback(() => { if (!videoRef.current) return; const newMutedState = !isMuted; setIsMuted(newMutedState); videoRef.current.muted = newMutedState; if (newMutedState) { lastVolumeRef.current = volume; } else { const restoreVolume = lastVolumeRef.current || 0.5; videoRef.current.volume = restoreVolume; setVolume(restoreVolume); } options?.onVolumeFeedback?.(newMutedState ? 0 : volume, newMutedState); }, [isMuted, videoRef, volume, options]); const setPlaybackRateValue = react.useCallback((rate) => { if (!videoRef.current) return; const clampedRate = Math.max(0.25, Math.min(2, rate)); videoRef.current.playbackRate = clampedRate; setPlaybackRate(clampedRate); }, [videoRef]); const toggleFullscreen = react.useCallback(() => { if (!containerRef.current) return; if (!document.fullscreenElement) { containerRef.current.requestFullscreen().catch((error) => { console.error("Error entering fullscreen:", error); options?.onError?.(error.message); }); } else { document.exitFullscreen().catch((error) => { console.error("Error exiting fullscreen:", error); options?.onError?.(error.message); }); } }, [containerRef, options]); const toggleCaptions = react.useCallback(() => { if (!videoRef.current) return; const textTracks = videoRef.current.textTracks; if (textTracks.length > 0) { const track = textTracks[0]; track.mode = captionsEnabled ? "hidden" : "showing"; setCaptionsEnabled(!captionsEnabled); } }, [captionsEnabled, videoRef]); const formatTime = react.useCallback((time) => { if (isNaN(time)) return "0:00"; const hours = Math.floor(time / 3600); const minutes = Math.floor(time % 3600 / 60); const seconds = Math.floor(time % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; } return `${minutes}:${seconds.toString().padStart(2, "0")}`; }, []); react.useEffect(() => { const handleKeyDown = (e) => { if (!videoRef.current || !containerRef.current) return; const isPlayerFocused = containerRef.current.contains(document.activeElement) || document.activeElement === containerRef.current || document.activeElement === videoRef.current; if (!isPlayerFocused) return; const handledKeys = [ "Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "KeyM", "KeyF", "KeyK", "KeyJ", "KeyL", "Comma", "Period", "Home", "End" ]; if (handledKeys.includes(e.code)) { e.preventDefault(); } const currentVideoTime = videoRef.current.currentTime; switch (e.code) { case "Space": case "KeyK": togglePlay(); break; case "ArrowLeft": case "KeyJ": const newTimeLeft = Math.max(0, currentVideoTime - 10); seek(newTimeLeft); break; case "ArrowRight": case "KeyL": const newTimeRight = Math.min(duration, currentVideoTime + 10); seek(newTimeRight); break; case "ArrowUp": const newVolumeUp = Math.min(1, volume + 0.1); setVolumeValue(newVolumeUp); break; case "ArrowDown": const newVolumeDown = Math.max(0, volume - 0.1); setVolumeValue(newVolumeDown); break; case "KeyM": toggleMute(); break; case "KeyF": toggleFullscreen(); break; case "Comma": if (e.shiftKey) { setPlaybackRateValue(Math.max(0.25, playbackRate - 0.25)); } break; case "Period": if (e.shiftKey) { setPlaybackRateValue(Math.min(2, playbackRate + 0.25)); } break; case "Home": seek(0); break; case "End": seek(duration); break; } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [ togglePlay, seek, currentTime, duration, setVolumeValue, volume, toggleMute, toggleFullscreen, setPlaybackRateValue, playbackRate, videoRef, containerRef ]); react.useEffect(() => { const video = videoRef.current; if (!video) return; const handlePlay = () => { setIsPlaying(true); setIsLoading(false); options?.onPlay?.(); }; const handlePause = () => { setIsPlaying(false); options?.onPause?.(); }; const handleTimeUpdate = () => { const currentTime2 = video.currentTime; setCurrentTime(currentTime2); lastCurrentTimeRef.current = currentTime2; options?.onTimeUpdate?.(currentTime2); }; const handleDurationChange = () => { setDuration(video.duration || 0); }; const handleVolumeChange = () => { setVolume(video.volume); setIsMuted(video.muted); }; const handleLoadStart = () => { setIsLoading(true); }; const handleCanPlay = () => { setIsLoading(false); }; const handleProgress = () => { if (video.buffered.length > 0) { const bufferedEnd = video.buffered.end(video.buffered.length - 1); const duration2 = video.duration; if (duration2 > 0) { setBuffered(bufferedEnd / duration2 * 100); } } }; const handleEnded = () => { setIsPlaying(false); options?.onEnded?.(); }; const handleError = () => { setIsLoading(false); options?.onError?.("Video failed to load"); }; video.addEventListener("play", handlePlay); video.addEventListener("pause", handlePause); video.addEventListener("timeupdate", handleTimeUpdate); video.addEventListener("durationchange", handleDurationChange); video.addEventListener("volumechange", handleVolumeChange); video.addEventListener("loadstart", handleLoadStart); video.addEventListener("canplay", handleCanPlay); video.addEventListener("progress", handleProgress); video.addEventListener("ended", handleEnded); video.addEventListener("error", handleError); return () => { video.removeEventListener("play", handlePlay); video.removeEventListener("pause", handlePause); video.removeEventListener("timeupdate", handleTimeUpdate); video.removeEventListener("durationchange", handleDurationChange); video.removeEventListener("volumechange", handleVolumeChange); video.removeEventListener("loadstart", handleLoadStart); video.removeEventListener("canplay", handleCanPlay); video.removeEventListener("progress", handleProgress); video.removeEventListener("ended", handleEnded); video.removeEventListener("error", handleError); }; }, [videoRef, options]); react.useEffect(() => { const handleFullscreenChange = () => { setIsFullscreen(!!document.fullscreenElement); }; document.addEventListener("fullscreenchange", handleFullscreenChange); return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); return { // State isPlaying, currentTime, duration, volume, isMuted, playbackRate, isFullscreen, captionsEnabled, isLoading, buffered, // Controls togglePlay, seek, setVolume: setVolumeValue, toggleMute, setPlaybackRate: setPlaybackRateValue, toggleFullscreen, toggleCaptions, formatTime }; }; const VideoPlayer = ({ src, poster, captions, title, theme = "dark", autoplay = false, loop = false, muted = false, controls = true, width = "100%", height = "auto", className = "", style = {}, onPlay, onPause, onTimeUpdate, onVolumeChange, onSeek, onEnded, onError }) => { const videoRef = react.useRef(null); const containerRef = react.useRef(null); const controlsTimeoutRef = react.useRef(null); const [showControls, setShowControls] = react.useState(true); const [showPlayPause, setShowPlayPause] = react.useState(false); const [showSeekFeedback, setShowSeekFeedback] = react.useState(false); const [seekFeedbackText, setSeekFeedbackText] = react.useState(""); const [showVolumeFeedback, setShowVolumeFeedback] = react.useState(false); const [volumeFeedbackText, setVolumeFeedbackText] = react.useState(""); const videoState = useVideo(videoRef, containerRef, { onPlay, onPause, onTimeUpdate, onVolumeChange, onSeek, onEnded, onError, onSeekFeedback: (timeDiff) => { if (Math.abs(timeDiff) >= 5) { setSeekFeedbackText(timeDiff > 0 ? `+${Math.round(timeDiff)}s` : `${Math.round(timeDiff)}s`); setShowSeekFeedback(true); setTimeout(() => setShowSeekFeedback(false), 1e3); } }, onVolumeFeedback: (volume2, isMuted2) => { setVolumeFeedbackText(isMuted2 ? "Muted" : `${Math.round(volume2 * 100)}%`); setShowVolumeFeedback(true); setTimeout(() => setShowVolumeFeedback(false), 1e3); }, onPlayPauseFeedback: () => { setShowPlayPause(true); setTimeout(() => setShowPlayPause(false), 800); } }); const { isPlaying, currentTime, duration, volume, isMuted, playbackRate, isFullscreen, captionsEnabled, isLoading, buffered, togglePlay, seek, setVolume, toggleMute, setPlaybackRate, toggleFullscreen, toggleCaptions, formatTime } = videoState; const clearControlsTimeout = () => { if (controlsTimeoutRef.current) { clearTimeout(controlsTimeoutRef.current); } }; const showControlsTemporarily = () => { setShowControls(true); clearControlsTimeout(); controlsTimeoutRef.current = setTimeout(() => { if (isPlaying) { setShowControls(false); } }, 3e3); }; const handleMouseMove = () => { if (!controls) return; showControlsTemporarily(); }; const handleMouseLeave = () => { if (!controls) return; if (isPlaying) { clearControlsTimeout(); controlsTimeoutRef.current = setTimeout(() => { setShowControls(false); }, 1e3); } }; react.useEffect(() => { return () => { clearControlsTimeout(); }; }, []); react.useEffect(() => { if (!isPlaying || !controls) setShowControls(true); }, [isPlaying, controls]); react.useEffect(() => { if (videoRef.current && muted) videoRef.current.muted = true; }, [muted]); react.useEffect(() => { containerRef.current?.focus(); }, []); react.useEffect(() => { const handleKeyDown = (e) => { if (e.code === "Space" && document.activeElement === containerRef.current) { e.preventDefault(); togglePlay(); showControlsTemporarily(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isPlaying]); return /* @__PURE__ */ jsxRuntime.jsxs( "div", { ref: containerRef, className: `rvp-video-player rvp-${theme} ${className}`, style: { width, height, ...style }, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onClick: () => containerRef.current?.focus(), tabIndex: 0, role: "application", "aria-label": title || "Video Player", children: [ /* @__PURE__ */ jsxRuntime.jsxs( "video", { ref: videoRef, src, poster, autoPlay: autoplay, loop, muted, preload: "metadata", className: "rvp-video", "aria-label": title, onClick: togglePlay, children: [ captions && /* @__PURE__ */ jsxRuntime.jsx( "track", { kind: "subtitles", src: captions, srcLang: "en", label: "English", default: captionsEnabled } ), "Your browser does not support the video tag." ] } ), isLoading && /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, {}), showPlayPause && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-feedback-overlay rvp-play-pause-feedback", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-feedback-icon", children: isPlaying ? /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "80", height: "80", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "80", height: "80", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" }) }) }) }), showSeekFeedback && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-feedback-overlay rvp-seek-feedback", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-feedback-text", children: seekFeedbackText }) }), showVolumeFeedback && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-feedback-overlay rvp-volume-feedback", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rvp-feedback-text", children: [ /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", style: { marginRight: "8px" }, children: volumeFeedbackText === "Muted" ? /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" }) : /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" }) }), volumeFeedbackText ] }) }), controls && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rvp-video-overlay ${showControls ? "rvp-show" : ""}`, children: [ title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rvp-video-title", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { children: title }) }), /* @__PURE__ */ jsxRuntime.jsx( Controls, { isPlaying, currentTime, duration, volume, isMuted, playbackRate, isFullscreen, captionsEnabled, buffered, hasCaptions: !!captions, onPlay: togglePlay, onSeek: seek, onVolumeChange: setVolume, onMute: toggleMute, onPlaybackRateChange: setPlaybackRate, onFullscreen: toggleFullscreen, onToggleCaptions: toggleCaptions, formatTime } ) ] }) ] } ); }; exports.VideoPlayer = VideoPlayer; exports.useVideo = useVideo; //# sourceMappingURL=index.js.map