From 275ddc4ac8ec3484a2eba80877f9e19a36b86dcf Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 17 Jun 2025 18:21:59 +0300 Subject: [PATCH] added global player --- package-lock.json | 145 +++++++- package.json | 7 +- .../VideoPlayer/SubtitleManager.tsx | 67 ++-- src/components/VideoPlayer/VideoControls.tsx | 257 +++++++------- .../VideoPlayer/VideoControlsBar.tsx | 8 +- src/components/VideoPlayer/VideoPlayer.tsx | 94 ++++- .../VideoPlayer/useVideoPlayerController.tsx | 51 ++- src/context/GlobalProvider.tsx | 2 + src/hooks/useGlobalPipPlayer.tsx | 323 ++++++++++++++++++ src/state/pip.ts | 30 ++ 10 files changed, 791 insertions(+), 193 deletions(-) create mode 100644 src/hooks/useGlobalPipPlayer.tsx create mode 100644 src/state/pip.ts diff --git a/package-lock.json b/package-lock.json index d8aa078..33e931e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-hot-toast": "^2.5.2", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", + "react-rnd": "^10.5.2", "short-unique-id": "^5.2.0", "srt-webvtt": "^2.0.0", "ts-key-enum": "^3.0.13", @@ -38,6 +39,8 @@ "@types/react": "^19.0.10", "cpy-cli": "^5.0.0", "react": "^19.0.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", "tsup": "^8.4.0", "typescript": "^5.2.0" }, @@ -47,7 +50,9 @@ "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", "mediainfo.js": "^0.3.5", - "react": "^19.0.0" + "react": "^19.0.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" } }, "node_modules/@babel/code-frame": { @@ -1905,6 +1910,16 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3194,24 +3209,58 @@ } ] }, + "node_modules/re-resizable": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", + "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "peer": true, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.1.0" + } + }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/react-dropzone": { @@ -3276,6 +3325,67 @@ "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", "dev": true }, + "node_modules/react-rnd": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", + "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", + "license": "MIT", + "dependencies": { + "re-resizable": "6.11.2", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3427,16 +3537,23 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "peer": true + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 95d14fe..1458201 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-hot-toast": "^2.5.2", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", + "react-rnd": "^10.5.2", "short-unique-id": "^5.2.0", "srt-webvtt": "^2.0.0", "ts-key-enum": "^3.0.13", @@ -49,7 +50,9 @@ "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", "mediainfo.js": "^0.3.5", - "react": "^19.0.0" + "react": "^19.0.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@emotion/react": "^11.14.0", @@ -60,6 +63,8 @@ "@types/react": "^19.0.10", "cpy-cli": "^5.0.0", "react": "^19.0.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", "tsup": "^8.4.0", "typescript": "^5.2.0" }, diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx index 0f3e258..bc3cfc2 100644 --- a/src/components/VideoPlayer/SubtitleManager.tsx +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -173,13 +173,22 @@ const SubtitleManagerComponent = ({ getPublishedSubtitles, ]); - const handleClose = () => { + const ref = useRef(null) + console.log('isOpen', open) + + useEffect(()=> { + if(open){ + ref?.current?.focus() + } + }, [open]) + + const handleBlur = (e: React.FocusEvent) => { + if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish) { + console.log('handleBlur') close(); - setMode(1); - // setTitle(""); - // setDescription(""); - // setHasMetadata(false); - }; + setIsOpenPublish(false) + } +}; const publishHandler = async (subtitles: Subtitle[]) => { try { @@ -250,23 +259,20 @@ const SubtitleManagerComponent = ({ }; const theme = useTheme(); - + if(!open) return return ( <> - ))} */} - - + + + // { const [isOpen, setIsOpen] = useState(false); const btnRef = useRef(null); @@ -362,130 +363,13 @@ export const PlaybackRate = ({ fontSize: fontSizeSmall, padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig, }} - onClick={() => setIsOpen(true)} + onClick={() => openPlaybackMenu()} > - setIsOpen(false)} - slots={{ - transition: Fade, - }} - slotProps={{ - transition: { - timeout: 200, - }, - paper: { - sx: { - bgcolor: alpha("#181818", 0.98), - color: "white", - opacity: 0.9, - borderRadius: 2, - boxShadow: 5, - p: 1, - minWidth: 225, - height: 300, - overflow: "hidden", - display: "flex", - flexDirection: "column", - }, - }, - }} - anchorOrigin={{ - vertical: "top", - horizontal: "center", - }} - transformOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - > - - - - - - - Playback speed - - - - - - {speeds?.map((speed) => { - const isSelected = speed === playbackRate; - return ( - { - onSelect(speed) - setIsOpen(false) - }} - sx={{ - px: 2, - py: 1, - "&:hover": { - backgroundColor: "rgba(255, 255, 255, 0.1)", - }, - width: "100%", - justifyContent: "space-between", - }} - > - {speed} - {isSelected ? : } - - ); - })} - - + ); }; @@ -551,3 +435,138 @@ export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => { ); }; + +interface PlayBackMenuProps { + close: ()=> void + isOpen: boolean + onSelect: (speed: number)=> void; + playbackRate: number +} +export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMenuProps)=> { + const theme = useTheme() + const ref = useRef(null) + console.log('isOpen', isOpen) + + useEffect(()=> { + if(isOpen){ + ref?.current?.focus() + } + }, [isOpen]) + + const handleBlur = (e: React.FocusEvent) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + close(); + } +}; + if(!isOpen) return null + return ( + + + + + + + + + Playback speed + + + + + + {speeds?.map((speed) => { + const isSelected = speed === playbackRate; + return ( + { + onSelect(speed) + close() + }} + sx={{ + px: 2, + py: 1, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.1)", + }, + width: "100%", + justifyContent: "space-between", + }} + > + {speed} + {isSelected ? : } + + ); + })} + + + ) +} \ No newline at end of file diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 5f71340..ef5a3c2 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -40,9 +40,11 @@ interface VideoControlsBarProps { onSelectPlaybackRate: (rate: number)=> void; isMuted: boolean toggleMute: ()=> void + openPlaybackMenu: ()=> void + togglePictureInPicture: ()=> void } -export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager, onSelectPlaybackRate, isMuted, toggleMute}: VideoControlsBarProps) => { +export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager, onSelectPlaybackRate, isMuted, toggleMute, openPlaybackMenu, togglePictureInPicture}: VideoControlsBarProps) => { const showMobileControls = isScreenSmall && canPlay; @@ -100,7 +102,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in - + {/* */} - {/* */} + diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index 6fffcc7..5840ef2 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -4,6 +4,7 @@ import { RefObject, useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -28,6 +29,9 @@ import { import { base64ToBlobUrl } from "../../utils/base64"; import convert from "srt-webvtt"; import { TimelineActionsComponent } from "./TimelineActionsComponent"; +import { PlayBackMenu } from "./VideoControls"; +import { useGlobalPlayerStore } from "../../state/pip"; +import { useLocation } from "react-router-dom"; export async function srtBase64ToVttBlobUrl( base64Srt: string @@ -208,6 +212,9 @@ export const VideoPlayer = ({ const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false); const subtitleBtnRef = useRef(null); const [currentSubTrack, setCurrentSubTrack] = useState(null) + const location = useLocation(); + const locationRef = useRef(null) + const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false) const { reloadVideo, togglePlay, @@ -231,17 +238,27 @@ export const VideoPlayer = ({ percentLoaded, showControlsFullScreen, onSelectPlaybackRate, - seekTo + seekTo, + togglePictureInPicture } = useVideoPlayerController({ autoPlay, playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, - isMuted + isMuted, + videoRef }); + useEffect(()=> { + if(location){ + locationRef.current = location.pathname + + } + },[location]) + console.log('isFullscreen', isFullscreen) + const { getProgress } = useProgressStore(); const enterFullscreen = useCallback(() => { const ref = containerRef?.current as any; @@ -304,6 +321,10 @@ export const VideoPlayer = ({ if (!qortalVideoResource) return null; return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`; }, [qortalVideoResource]); +const videoLocationRef = useRef< null | string>(null) + useEffect(()=> { + videoLocationRef.current = videoLocation + }, [videoLocation]) useVideoPlayerHotKeys(hotkeyHandlers); const updateProgress = useCallback(() => { @@ -316,6 +337,15 @@ export const VideoPlayer = ({ setLocalProgress(currentTime); } }, [videoLocation]); + + useEffect(()=> { + if(videoLocation){ + const vidId = useGlobalPlayerStore.getState().videoId + if(vidId === videoLocation){ + togglePlay() + } + } + }, [videoLocation]) // useEffect(() => { // const ref = videoRef as React.RefObject; // if (!ref.current) return; @@ -442,6 +472,13 @@ export const VideoPlayer = ({ resetHideTimer(); }; + const closePlaybackMenu = useCallback(()=> { + setIsOpenPlaybackmenu(false) + }, []) + const openPlaybackMenu = useCallback(()=> { + setIsOpenPlaybackmenu(true) + }, []) + useEffect(() => { resetHideTimer(); // initial show return () => { @@ -593,6 +630,24 @@ export const VideoPlayer = ({ return JSON.stringify(qortalVideoResource); }, [qortalVideoResource]); + + const savedVideoRef = useRef(null); + + useEffect(()=> { + if(startPlay){ + useGlobalPlayerStore.getState().reset() + + } + }, [startPlay]) + +useLayoutEffect(() => { + // Save the video element while it's still mounted + const video = videoRef as any + if (video.current) { +savedVideoRef.current = video.current; + } +}, []); + useEffect(() => { if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return; @@ -630,7 +685,14 @@ export const VideoPlayer = ({ playerRef.current?.poster(""); playerRef.current?.playbackRate(playbackRate); playerRef.current?.volume(volume); + if(videoLocationRef.current){ + const savedProgress = getProgress(videoLocationRef.current); + if (typeof savedProgress === "number") { + playerRef.current?.currentTime(savedProgress); + } + } + playerRef.current?.play(); const tracksInfo = playerRef.current?.textTracks(); @@ -684,8 +746,30 @@ export const VideoPlayer = ({ console.error("useEffect start player", error); } return () => { + console.log('hello1002') + const video = savedVideoRef as any + const videoEl = video?.current!; + const player = playerRef.current; + + console.log('videohello', videoEl); + const isPlaying = !player?.paused(); + + if (videoEl && isPlaying && videoLocationRef.current) { + const current = player?.currentTime?.(); + const currentSource = player?.currentType(); + + useGlobalPlayerStore.getState().setVideoState({ + videoSrc: videoEl.src, + currentTime: current ?? 0, + isPlaying: true, + mode: 'floating', + videoId: videoLocationRef.current, + location: locationRef.current || "", + type: currentSource || 'video/mp4' + }); + } + canceled = true; - const player = playerRef.current; if (player && typeof player.dispose === "function") { try { @@ -753,6 +837,7 @@ export const VideoPlayer = ({ onVolumeChange={onVolumeChangeHandler} controls={false} /> + {/* */} {isReady && ( @@ -781,11 +866,12 @@ export const VideoPlayer = ({ onSelectPlaybackRate={onSelectPlaybackRate} isMuted={isMuted} toggleMute={toggleMute} + openPlaybackMenu={openPlaybackMenu} + togglePictureInPicture={togglePictureInPicture} /> )} {timelineActions && Array.isArray(timelineActions) && ( - )} { - const { autoPlay, playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, isMuted } = props; + const { autoPlay, videoRef , playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, isMuted } = props; const [isFullscreen, setIsFullscreen] = useState(false); const [showControlsFullScreen, setShowControlsFullScreen] = useState(false) @@ -39,7 +42,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => { const [startPlay, setStartPlay] = useState(false); const [startedFetch, setStartedFetch] = useState(false); const startedFetchRef = useRef(false); - + const navigate = useNavigate() const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore(); const { getProgress } = useProgressStore(); @@ -61,22 +64,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => { return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`; }, [qortalVideoResource]); - useEffect(() => { - if (videoLocation && isPlayerInitialized) { - try { - const ref = playerRef as any; - if (!ref.current) return; - const savedProgress = getProgress(videoLocation); - if (typeof savedProgress === "number") { - playerRef.current?.currentTime(savedProgress); - - } - } catch (error) { - console.error('line 74', error) - } - } - }, [videoLocation, getProgress, isPlayerInitialized]); const [playbackRate, _setLocalPlaybackRate] = useState( playbackSettings.playbackRate @@ -294,6 +282,33 @@ const togglePlay = useCallback(async () => { } }, [togglePlay, isReady]); +// videoRef?.current?.addEventListener("enterpictureinpicture", () => { +// setPipVideoPath(window.location.pathname); + +// }); + +// // when PiP ends (and you're on the wrong page), go back +// videoRef?.current?.addEventListener("leavepictureinpicture", () => { +// const { pipVideoPath } = usePipStore.getState(); +// if (pipVideoPath && window.location.pathname !== pipVideoPath) { +// navigate(pipVideoPath); +// } +// }); + + const togglePictureInPicture = async () => { + if (!videoRef.current) return; + const player = playerRef.current; + if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return; + + const current = player.currentTime(); + useGlobalPlayerStore.getState().setVideoState({ + videoSrc: videoRef.current.src, + currentTime: current, + isPlaying: true, + mode: 'floating', // or 'floating' + }); + }; + return { reloadVideo, @@ -314,6 +329,6 @@ const togglePlay = useCallback(async () => { isReady, resourceUrl, startPlay, - status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo + status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo, togglePictureInPicture }; }; diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index 67b6b74..a799961 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -14,6 +14,7 @@ import { usePersistentStore } from "../hooks/usePersistentStore"; import { IndexManager } from "../components/IndexManager/IndexManager"; import { useIndexes } from "../hooks/useIndexes"; import { useProgressStore } from "../state/video"; +import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; // ✅ Define Global Context Type interface GlobalContextType { @@ -80,6 +81,7 @@ export const GlobalProvider = ({ return ( + { + const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore(); + const [playing , setPlaying] = useState(false) + const [hasStarted, setHasStarted] = useState(false) + const playerRef = useRef(null); + const navigate = useNavigate() + const videoNode = useRef(null); + const { setProgress } = useProgressStore(); + + const updateProgress = useCallback(() => { + const player = playerRef?.current; + if (!player || typeof player?.currentTime !== "function") return; + + const currentTime = player.currentTime(); + console.log('videoId', videoId) + if (typeof currentTime === "number" && videoId && currentTime > 0.1) { + setProgress(videoId, currentTime); + } + }, [videoId]); + + const rndRef = useRef(null) + useEffect(() => { + if (!playerRef.current && videoNode.current) { + playerRef.current = videojs(videoNode.current, { autoplay: true, controls: false, + responsive: true, fluid: true }); + + // Resume playback if needed + playerRef.current.on('ready', () => { + if (videoSrc) { + + playerRef.current.src(videoSrc); + playerRef.current.currentTime(currentTime); + if (isPlaying) playerRef.current.play(); + } + }); + } + + return () => { + // optional: don't destroy, just hide + }; + }, []); + +useEffect(()=> { +if(!videoSrc){ + setHasStarted(false) +} +}, [videoSrc]) + +useEffect(() => { + const player = playerRef.current; + + if (!player) return; + +if (!videoSrc && player.src) { + // Only pause the player and unload the source without re-triggering playback + player.pause(); + + // Remove the video source safely + const tech = player.tech({ IWillNotUseThisInPlugins: true }); + if (tech && tech.el_) { + tech.setAttribute('src', ''); + setPlaying(false) + setHasStarted(false) + } + + // Optionally clear the poster and currentTime + player.poster(''); + player.currentTime(0); + + return; +} + + +if(videoSrc){ + // Set source and resume if needed + player.src({ src: videoSrc, type: type }); + player.currentTime(currentTime); + + if (isPlaying) { + const playPromise = player.play(); + + + if (playPromise?.catch) { + playPromise.catch((err: any) => { + console.warn('Unable to autoplay:', err); + }); + } + } else { + player.pause(); + } +} +}, [videoSrc, type, isPlaying, currentTime]); + + +// const onDragStart = () => { +// timer = Date.now(); +// isDragging.current = true; +// }; + +// const handleStopDrag = async () => { +// const time = Date.now(); +// if (timer && time - timer < 300) { +// isDragging.current = false; +// } else { +// isDragging.current = true; +// } +// }; +// const onDragStop = () => { +// handleStopDrag(); +// }; + +// const checkIfDrag = useCallback(() => { +// return isDragging.current; +// }, []); +const margin = 50; + + + const [height, setHeight] = useState(300) + const [width, setWidth] = useState(400) + + useEffect(() => { + + rndRef.current.updatePosition({ + x: window.innerWidth - 400 - margin, + y: window.innerHeight - 300 - margin, + width: 400, + height: 300 + }); + }, [videoSrc]); + + const [showControls, setShowControls] = useState(false) + + const handleMouseMove = () => { + setShowControls(true) + }; + + const handleMouseLeave = () => { + setShowControls(false); + }; + const startPlay = useCallback(() => { + try { + + const player = playerRef.current; + if (!player) return; + + + try { + player.play(); + } catch (err) { + console.warn('Play failed:', err); + } + + } catch (error) { + console.error('togglePlay', error) + } + }, []); + + const stopPlay = useCallback(() => { + const player = playerRef.current; + if (!player) return; + + try { + player.pause(); + } catch (err) { + console.warn('Play failed:', err); + } + + + }, []); + + const onPlayHandlerStart = useCallback(() => { + setPlaying(true) + setHasStarted(true) + }, [setPlaying]); + const onPlayHandlerStop = useCallback(() => { + setPlaying(false) + }, [setPlaying]); + + return ( + { + setWidth(ref.offsetWidth); + setHeight(ref.offsetHeight); + }} + +// default={{ +// x: 500, +// y: 500, +// width: 350, +// height: "auto", +// }} + // eslint-disable-next-line @typescript-eslint/no-empty-function + onDrag={() => {}} +> + {/*
*/} + + {/* {backgroundColor: showControls ? 'rgba(0,0,0,.5)' : 'unset'} */} + {showControls && ( + + + + {location && ( + navigate(location)}> + )} + {playing && ( + + )} + {!playing && ( + + )} + + + + + + )} + + + + {/*
*/} +
+ ); +}; diff --git a/src/state/pip.ts b/src/state/pip.ts new file mode 100644 index 0000000..a6609db --- /dev/null +++ b/src/state/pip.ts @@ -0,0 +1,30 @@ +import { create } from 'zustand'; + +type PlayerMode = 'embedded' | 'floating' | 'none'; + +interface GlobalPlayerState { + videoSrc: string | null; + videoId: string, + isPlaying: boolean; + currentTime: number; + location: string; + mode: PlayerMode; + setVideoState: (state: Partial) => void; + type: string, + reset: ()=> void; +} + const initialState = { + videoSrc: null, + videoId: "", + location: "", + isPlaying: false, + currentTime: 0, + type: 'video/mp4', + mode: 'embedded' as const, + }; +export const useGlobalPlayerStore = create((set) => ({ + ...initialState, + setVideoState: (state) => set((prev) => ({ ...prev, ...state })), + reset: () => set(initialState), + +}));