From 98a51c0b5fdb15d4dee01985e913e5709bc5ace7 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 26 Jun 2025 22:40:00 +0300 Subject: [PATCH] fixes and update version --- package.json | 2 +- src/components/VideoPlayer/LoadingVideo.tsx | 104 ++--- src/components/VideoPlayer/MobileControls.tsx | 96 ++--- .../VideoPlayer/SubtitleManager.tsx | 233 +++++------ .../VideoPlayer/TimelineActionsComponent.tsx | 124 +++--- src/components/VideoPlayer/VideoControls.tsx | 359 ++++++++--------- .../VideoPlayer/VideoControlsBar.tsx | 199 ++++++---- src/components/VideoPlayer/VideoPlayer.tsx | 272 ++++--------- .../VideoPlayer/VideoPlayerParent.tsx | 94 +++++ .../VideoPlayer/useVideoPlayerController.tsx | 374 ++++++++---------- .../VideoPlayer/useVideoPlayerHotKeys.tsx | 8 +- src/context/GlobalProvider.tsx | 8 +- src/index.ts | 2 +- src/state/video.ts | 11 + src/utils/qortal.ts | 25 +- 15 files changed, 950 insertions(+), 961 deletions(-) create mode 100644 src/components/VideoPlayer/VideoPlayerParent.tsx diff --git a/package.json b/package.json index 92df552..e168cc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qapp-core", - "version": "1.0.34", + "version": "1.0.35", "description": "Qortal's core React library with global state, UI components, and utilities", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/components/VideoPlayer/LoadingVideo.tsx b/src/components/VideoPlayer/LoadingVideo.tsx index 78d77bd..cce83f7 100644 --- a/src/components/VideoPlayer/LoadingVideo.tsx +++ b/src/components/VideoPlayer/LoadingVideo.tsx @@ -1,31 +1,42 @@ -import { alpha, Box, Button, CircularProgress, IconButton, Typography } from "@mui/material"; +import { + alpha, + Box, + Button, + CircularProgress, + IconButton, + Typography, +} from "@mui/material"; import { PlayArrow } from "@mui/icons-material"; import { Status } from "../../state/publishes"; - interface LoadingVideoProps { - status: Status | null - percentLoaded: number - isReady: boolean - isLoading: boolean - togglePlay: ()=> void - startPlay: boolean, - downloadResource: ()=> void + status: Status | null; + percentLoaded: number; + isReady: boolean; + isLoading: boolean; + togglePlay: () => void; + startPlay: boolean; + downloadResource: () => void; } export const LoadingVideo = ({ - status, percentLoaded, isReady, isLoading, togglePlay, startPlay, downloadResource + status, + percentLoaded, + isReady, + isLoading, + togglePlay, + startPlay, + downloadResource, }: LoadingVideoProps) => { - const getDownloadProgress = (percentLoaded: number) => { const progress = percentLoaded; return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%"; }; - if(status === 'READY') return null + if (status === "READY") return null; return ( <> - {isLoading && status !== 'INITIAL' && ( + {isLoading && status !== "INITIAL" && ( {status !== "NOT_PUBLISHED" && status !== "FAILED_TO_DOWNLOAD" && ( - - + )} {status && ( - {status === "NOT_PUBLISHED" ? ( <>Video file was not published. Please inform the publisher! ) : status === "REFETCHING" ? ( <> - <> - {getDownloadProgress( - percentLoaded - )} - + <>{getDownloadProgress(percentLoaded)} <> Refetching in 25 seconds ) : status === "DOWNLOADED" ? ( <>Download Completed: building video... - ) : status === "FAILED_TO_DOWNLOAD" ? ( + ) : status === "FAILED_TO_DOWNLOAD" ? ( <>Unable to fetch video chunks from peers ) : ( - <> - {getDownloadProgress( - percentLoaded - )} - + <>{getDownloadProgress(percentLoaded)} )} )} - {status === 'FAILED_TO_DOWNLOAD' && ( - + {status === "FAILED_TO_DOWNLOAD" && ( + )} )} - {(status === 'INITIAL') && ( + {status === "INITIAL" && ( <> { togglePlay(); }} - sx={{ cursor: "pointer", - position:"absolute", - top:0, - left:0, - right:0, - bottom:0, - zIndex: 501, - background: 'rgba(0,0,0,0.3)', - padding: '0px', - borderRadius: "0px", - + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 501, + background: "rgba(0,0,0,0.3)", + padding: "0px", + borderRadius: "0px", }} > void; toggleFullscreen: () => void; setProgressRelative: (val: number) => void; - setLocalProgress: (val: number)=> void; - resetHideTimeout: ()=> void + setLocalProgress: (val: number) => void; + resetHideTimeout: () => void; } export const MobileControls = ({ showControlsMobile, @@ -37,7 +37,7 @@ export const MobileControls = ({ toggleFullscreen, setProgressRelative, setLocalProgress, - resetHideTimeout + resetHideTimeout, }: MobileControlsProps) => { return ( { e.stopPropagation(); openPlaybackMenu(); }} > - + color: "white", + }} + /> { e.stopPropagation(); @@ -126,7 +128,7 @@ export const MobileControls = ({ @@ -135,9 +137,9 @@ export const MobileControls = ({ sx={{ opacity: 1, zIndex: 2, - background: 'rgba(0,0,0,0.3)', - borderRadius: '50%', - padding: '10px' + background: "rgba(0,0,0,0.3)", + borderRadius: "50%", + padding: "10px", }} onClick={(e) => { e.stopPropagation(); @@ -147,7 +149,7 @@ export const MobileControls = ({ @@ -157,9 +159,9 @@ export const MobileControls = ({ sx={{ opacity: 1, zIndex: 2, - background: 'rgba(0,0,0,0.3)', - borderRadius: '50%', - padding: '10px' + background: "rgba(0,0,0,0.3)", + borderRadius: "50%", + padding: "10px", }} onClick={(e) => { e.stopPropagation(); @@ -169,7 +171,7 @@ export const MobileControls = ({ @@ -178,9 +180,9 @@ export const MobileControls = ({ sx={{ opacity: 1, zIndex: 2, - background: 'rgba(0,0,0,0.3)', - borderRadius: '50%', - padding: '10px' + background: "rgba(0,0,0,0.3)", + borderRadius: "50%", + padding: "10px", }} onClick={(e) => { e.stopPropagation(); @@ -190,7 +192,7 @@ export const MobileControls = ({ @@ -206,19 +208,21 @@ export const MobileControls = ({ { e.stopPropagation(); toggleFullscreen(); }} > - + - - + + void; open: boolean; onSelect: (subtitle: SubtitlePublishedData) => void; subtitleBtnRef: any; - currentSubTrack: null | string - setDrawerOpenSubtitles: (val: boolean)=> void - isFromDrawer: boolean - exitFullscreen: ()=> void + currentSubTrack: null | string; + setDrawerOpenSubtitles: (val: boolean) => void; + isFromDrawer: boolean; + exitFullscreen: () => void; } export interface Subtitle { language: string | null; @@ -113,7 +112,7 @@ const SubtitleManagerComponent = ({ currentSubTrack, setDrawerOpenSubtitles, isFromDrawer = false, - exitFullscreen + exitFullscreen, }: SubtitleManagerProps) => { const [mode, setMode] = useState(1); const [isOpenPublish, setIsOpenPublish] = useState(false); @@ -146,12 +145,13 @@ const SubtitleManagerComponent = ({ identifier: postIdSearch, name, limit: 0, - includeMetadata: true + includeMetadata: true, }; - const res = await lists.fetchResourcesResultsOnly( - searchParams + const res = await lists.fetchResourcesResultsOnly(searchParams); + lists.addList( + `subs-${videoId}`, + res?.filter((item) => !!item?.metadata?.title) || [] ); - lists.addList(`subs-${videoId}`, res?.filter((item)=> !!item?.metadata?.title) || []); } catch (error) { console.error(error); } finally { @@ -175,38 +175,42 @@ const SubtitleManagerComponent = ({ getPublishedSubtitles, ]); - const ref = useRef(null) + const ref = useRef(null); - useEffect(()=> { - if(open){ - ref?.current?.focus() + useEffect(() => { + if (open) { + ref?.current?.focus(); } - }, [open]) + }, [open]); - console.log('isFromDrawer', ) + console.log("isFromDrawer"); const handleBlur = (e: React.FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish && !isFromDrawer && open) { - console.log('hello close') - close(); - setIsOpenPublish(false) - } -}; + if ( + !e.currentTarget.contains(e.relatedTarget) && + !isOpenPublish && + !isFromDrawer && + open + ) { + console.log("hello close"); + close(); + setIsOpenPublish(false); + } + }; const publishHandler = async (subtitles: Subtitle[]) => { try { const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; - const name = auth?.name; if (!name) return; const resources: ResourceToPublish[] = []; const tempResources: { qortalMetadata: QortalMetadata; data: any }[] = []; for (const sub of subtitles) { - const identifier = await identifierOperations.buildLooseIdentifier( - ENTITY_SUBTITLE, - videoId - ); + const identifier = await identifierOperations.buildLooseIdentifier( + ENTITY_SUBTITLE, + videoId + ); const data = { subtitleData: sub.base64, language: sub.language, @@ -233,7 +237,7 @@ const SubtitleManagerComponent = ({ created: Date.now(), metadata: { title: sub.language || undefined, - } + }, }, data: data, }); @@ -260,33 +264,30 @@ const SubtitleManagerComponent = ({ }; const theme = useTheme(); - if(!open) return null + if (!open) return null; return ( <> { - setIsOpenPublish(true) - + setIsOpenPublish(true); }} > - - - ); }; - interface PublisherSubtitlesProps { publisherName: string; subtitles: any[]; @@ -450,13 +446,13 @@ const PublisherSubtitles = ({ {!currentSubTrack ? : } - {subtitles?.map((sub) => { + {subtitles?.map((sub, i) => { return ( ); })} @@ -725,6 +721,7 @@ const PublishSubtitles = ({ {mySubtitles?.map((sub, i) => { return ( void; currentSubtrack: null | string; } - const subtitlesStatus: Record = {} +const subtitlesStatus: Record = {}; const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { - const [selectedToDownload, setSelectedToDownload] = useState(null) - const [isReady, setIsReady] = useState(false) - const { resource, isLoading, error, refetch } = usePublish(2, "JSON", sub, true); + const [isReady, setIsReady] = useState(false); + const { resource, isLoading, error, refetch } = usePublish( + 2, + "JSON", + sub, + true + ); const isSelected = currentSubtrack === resource?.data?.language; - const [isGettingStatus, setIsGettingStatus] = useState(true) - // useEffect(()=> { - // if(resource?.data){ - // console.log('onselectdone') - // onSelect(resource?.data) - // } - // }, [isSelected, resource?.data]) - const getStatus = useCallback(async (service: Service, name: string, identifier: string)=> { - try { - if(subtitlesStatus[`${service}-${name}-${identifier}`]){ - setIsReady(true) - refetch() - return - } - - const response = await requestQueueGetStatus.enqueue( - (): Promise => { - return qortalRequest({ - action: 'GET_QDN_RESOURCE_STATUS', - identifier, - service, - name, - build: false - }) - } - ); - if(response?.status === 'READY'){ - setIsReady(true) - subtitlesStatus[`${service}-${name}-${identifier}`] = true - refetch() + const [isGettingStatus, setIsGettingStatus] = useState(true); - } - } catch (error) { - - } finally { - setIsGettingStatus(false) - } - }, []) + const getStatus = useCallback( + async (service: Service, name: string, identifier: string) => { + try { + if (subtitlesStatus[`${service}-${name}-${identifier}`]) { + setIsReady(true); + refetch(); + return; + } - useEffect(()=> { - if(sub?.service && sub?.name && sub?.identifier){ - getStatus(sub?.service, sub?.name, sub?.identifier) + const response = await requestQueueGetStatus.enqueue( + (): Promise => { + return qortalRequest({ + action: "GET_QDN_RESOURCE_STATUS", + identifier, + service, + name, + build: false, + }); + } + ); + if (response?.status === "READY") { + setIsReady(true); + subtitlesStatus[`${service}-${name}-${identifier}`] = true; + refetch(); + } + } catch (error) { + } finally { + setIsGettingStatus(false); + } + }, + [] + ); + + useEffect(() => { + if (sub?.service && sub?.name && sub?.identifier) { + getStatus(sub?.service, sub?.name, sub?.identifier); } - }, [sub?.identifier, sub?.name, sub?.service]) + }, [sub?.identifier, sub?.name, sub?.service]); return ( { - if(resource?.data){ - onSelect(isSelected ? null : resource?.data) - + if (resource?.data) { + onSelect(isSelected ? null : resource?.data); } else { - refetch() + refetch(); } }} sx={{ @@ -830,14 +826,23 @@ const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { justifyContent: "space-between", }} > - {isGettingStatus && } + {isGettingStatus && ( + + )} {!isGettingStatus && ( <> {sub?.metadata?.title} - {(!isLoading && !error && !resource?.data) ? : isLoading ? : isSelected ? : } + {!isLoading && !error && !resource?.data ? ( + + ) : isLoading ? ( + + ) : isSelected ? ( + + ) : ( + + )} )} - ); }; diff --git a/src/components/VideoPlayer/TimelineActionsComponent.tsx b/src/components/VideoPlayer/TimelineActionsComponent.tsx index 9de8002..7cc4941 100644 --- a/src/components/VideoPlayer/TimelineActionsComponent.tsx +++ b/src/components/VideoPlayer/TimelineActionsComponent.tsx @@ -1,41 +1,51 @@ -import React, { useCallback, useMemo, useState } from 'react' -import { TimelineAction } from './VideoPlayer' -import { alpha, Box, ButtonBase, Popover, Typography } from '@mui/material' +import React, { useCallback, useMemo, useState } from "react"; +import { TimelineAction } from "./VideoPlayer"; +import { alpha, ButtonBase, Typography } from "@mui/material"; interface TimelineActionsComponentProps { -timelineActions: TimelineAction[] -progress: number -containerRef: any -seekTo: (time: number)=> void -isVideoPlayerSmall: boolean - + timelineActions: TimelineAction[]; + progress: number; + containerRef: any; + seekTo: (time: number) => void; + isVideoPlayerSmall: boolean; } -const placementStyles: Record, React.CSSProperties> = { - 'TOP-RIGHT': { top: 16, right: 16 }, - 'TOP-LEFT': { top: 16, left: 16 }, - 'BOTTOM-LEFT': { bottom: 60, left: 16 }, - 'BOTTOM-RIGHT': { bottom: 60, right: 16 }, +const placementStyles: Record< + NonNullable, + React.CSSProperties +> = { + "TOP-RIGHT": { top: 16, right: 16 }, + "TOP-LEFT": { top: 16, left: 16 }, + "BOTTOM-LEFT": { bottom: 60, left: 16 }, + "BOTTOM-RIGHT": { bottom: 60, right: 16 }, }; -export const TimelineActionsComponent = ({timelineActions, progress, containerRef, seekTo, isVideoPlayerSmall}: TimelineActionsComponentProps) => { - const [isOpen, setIsOpen] = useState(true) +export const TimelineActionsComponent = ({ + timelineActions, + progress, + containerRef, + seekTo, + isVideoPlayerSmall, +}: TimelineActionsComponentProps) => { + const [isOpen, setIsOpen] = useState(true); - const handleClick = useCallback((action: TimelineAction)=> { - if(action?.type === 'SEEK'){ - if(!action?.seekToTime) return - seekTo(action.seekToTime) - } else if(action?.type === 'CUSTOM'){ - if(action.onClick){ - action.onClick() - } - } - },[]) + const handleClick = useCallback((action: TimelineAction) => { + if (action?.type === "SEEK") { + if (!action?.seekToTime) return; + seekTo(action.seekToTime); + } else if (action?.type === "CUSTOM") { + if (action.onClick) { + action.onClick(); + } + } + }, []); - // Find the current matching action(s) + // Find the current matching action(s) const activeActions = useMemo(() => { - return timelineActions.filter(action => { - return progress >= action.time && progress <= action.time + action.duration; + return timelineActions.filter((action) => { + return ( + progress >= action.time && progress <= action.time + action.duration + ); }); }, [timelineActions, progress]); @@ -44,32 +54,36 @@ export const TimelineActionsComponent = ({timelineActions, progress, containerRe if (!hasActive) return null; // Don’t render unless active return ( <> - {activeActions.map((action, index) => { - const placement = (action.placement ?? 'TOP-RIGHT') as keyof typeof placementStyles; + {activeActions?.map((action, index) => { + const placement = (action.placement ?? + "TOP-RIGHT") as keyof typeof placementStyles; return ( - - - handleClick(action)}> - {action.label} - - - - ) - } )} + + handleClick(action)} + > + {action.label} + + + ); + })} - ) -} + ); +}; diff --git a/src/components/VideoPlayer/VideoControls.tsx b/src/components/VideoPlayer/VideoControls.tsx index a2fa9cf..d85c86d 100644 --- a/src/components/VideoPlayer/VideoControls.tsx +++ b/src/components/VideoPlayer/VideoControls.tsx @@ -65,50 +65,50 @@ export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => { ); }; -export const ProgressSlider = ({ progress, setLocalProgress, duration, playerRef, resetHideTimeout, isVideoPlayerSmall }: any) => { +export const ProgressSlider = ({ + progress, + setLocalProgress, + duration, + playerRef, + resetHideTimeout, + isVideoPlayerSmall, +}: any) => { const sliderRef = useRef(null); const [isDragging, setIsDragging] = useState(false); -const [sliderValue, setSliderValue] = useState(0); // local slider value + const [sliderValue, setSliderValue] = useState(0); // local slider value const [hoverX, setHoverX] = useState(null); const [thumbnailUrl, setThumbnailUrl] = useState(null); const [showDuration, setShowDuration] = useState(0); -const showTimeFunc = (val: number, clientX: number) => { - const slider = sliderRef.current; - if (!slider) return; - console.log('time',val, duration) - const percent = val / duration; - const time = Math.min(Math.max(0, percent * duration), duration); + const showTimeFunc = (val: number, clientX: number) => { + const slider = sliderRef.current; + if (!slider) return; + const percent = val / duration; + const time = Math.min(Math.max(0, percent * duration), duration); - setHoverX(clientX); - setShowDuration(time); + setHoverX(clientX); + setShowDuration(time); - resetHideTimeout() - // Optionally debounce processing thumbnails - // debounceTimeoutRef.current = setTimeout(() => { - // debouncedExtract(time, clientX); - // }, THUMBNAIL_DEBOUNCE); -}; + resetHideTimeout(); + }; const onProgressChange = (e: any, value: number | number[]) => { - const clientX = 'touches' in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; -if(clientX && resetHideTimeout){ + const clientX = + "touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; + if (clientX && resetHideTimeout) { showTimeFunc(value as number, clientX); - - -} + } setIsDragging(true); setSliderValue(value as number); }; -const onChangeCommitted = (e: any, value: number | number[]) => { + const onChangeCommitted = (e: any, value: number | number[]) => { if (!playerRef.current) return; - setSliderValue(value as number); + setSliderValue(value as number); playerRef.current?.currentTime(value as number); - setIsDragging(false); - setLocalProgress(value) -handleMouseLeave() + setIsDragging(false); + setLocalProgress(value); + handleMouseLeave(); }; - const THUMBNAIL_DEBOUNCE = 500; const THUMBNAIL_MIN_DIFF = 10; @@ -128,10 +128,6 @@ handleMouseLeave() setShowDuration(time); if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); - - // debounceTimeoutRef.current = setTimeout(() => { - // debouncedExtract(time, e.clientX); - // }, THUMBNAIL_DEBOUNCE); }; const handleMouseLeave = () => { @@ -160,10 +156,8 @@ handleMouseLeave() console.log("thumbnailUrl", thumbnailUrl, hoverX); } - const handleClickCapture = (e: React.MouseEvent) => { - - e.stopPropagation(); - + const handleClickCapture = (e: React.MouseEvent) => { + e.stopPropagation(); }; return ( @@ -190,8 +184,7 @@ handleMouseLeave() onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} onClickCapture={handleClickCapture} - value={isDragging ? sliderValue : progress} // use local state if dragging - + value={isDragging ? sliderValue : progress} // use local state if dragging onChange={onProgressChange} onChangeCommitted={onChangeCommitted} min={0} @@ -232,8 +225,6 @@ handleMouseLeave() placement="top" disablePortal modifiers={[{ name: "offset", options: { offset: [-10, 0] } }]} - - > - {/* - - preview - */} {formatTime(showDuration)} @@ -290,8 +259,8 @@ export const VideoTime = ({ progress, isScreenSmall, duration }: any) => { placement="bottom" arrow disableHoverListener={isScreenSmall} - disableFocusListener={isScreenSmall} - disableTouchListener={isScreenSmall} + disableFocusListener={isScreenSmall} + disableTouchListener={isScreenSmall} > { ); }; -export const VolumeControl = ({ sliderWidth, onVolumeChange, volume , isMuted, toggleMute}: any) => { +export const VolumeControl = ({ + sliderWidth, + onVolumeChange, + volume, + isMuted, + toggleMute, +}: any) => { return ( { const [isOpen, setIsOpen] = useState(false); const btnRef = useRef(null); @@ -398,7 +373,7 @@ export const PlaybackRate = ({ arrow > - - ); }; @@ -478,136 +451,138 @@ export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => { }; interface PlayBackMenuProps { - close: ()=> void - isOpen: boolean - onSelect: (speed: number)=> void; - playbackRate: number - isFromDrawer: boolean + close: () => void; + isOpen: boolean; + onSelect: (speed: number) => void; + playbackRate: number; + isFromDrawer: boolean; } -export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawer}: PlayBackMenuProps)=> { - const theme = useTheme() - const ref = useRef(null) +export const PlayBackMenu = ({ + close, + onSelect, + isOpen, + playbackRate, + isFromDrawer, +}: PlayBackMenuProps) => { + const theme = useTheme(); + const ref = useRef(null); - useEffect(()=> { - if(isOpen){ - ref?.current?.focus() + useEffect(() => { + if (isOpen) { + ref?.current?.focus(); } - }, [isOpen]) + }, [isOpen]); const handleBlur = (e: React.FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget) && !isFromDrawer) { - close(); - } -}; - if(!isOpen) return null + if (!e.currentTarget.contains(e.relatedTarget) && !isFromDrawer) { + close(); + } + }; + if (!isOpen) return null; return ( - - + - - - + + + + + Playback speed + + + + + + {speeds?.map((speed) => { + const isSelected = speed === playbackRate; + return ( + { + onSelect(speed); + close(); }} - /> - - - - 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 ? : } - - ); - })} - + {speed} + {isSelected ? : } + + ); + })} - ) -} \ No newline at end of file + + ); +}; diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 2999c36..4766aec 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -2,8 +2,6 @@ import { Box, IconButton } from "@mui/material"; import { ControlsContainer } from "./VideoPlayer-styles"; import { FullscreenButton, - ObjectFitButton, - PictureInPictureButton, PlaybackRate, PlayButton, ProgressSlider, @@ -11,42 +9,67 @@ import { VideoTime, VolumeControl, } from "./VideoControls"; -import { Ref } from "react"; -import SubtitlesIcon from '@mui/icons-material/Subtitles'; +import SubtitlesIcon from "@mui/icons-material/Subtitles"; import { CustomFontTooltip } from "./CustomFontTooltip"; interface VideoControlsBarProps { - canPlay: boolean - isScreenSmall: boolean - controlsHeight?: string + canPlay: boolean; + isScreenSmall: boolean; + controlsHeight?: string; progress: number; - duration: number + duration: number; isPlaying: boolean; - togglePlay: ()=> void; - reloadVideo: ()=> void; - volume: number - onVolumeChange: (_: any, val: number)=> void - toggleFullscreen: ()=> void - extractFrames: (time: number)=> void + togglePlay: () => void; + reloadVideo: () => void; + volume: number; + onVolumeChange: (_: any, val: number) => void; + toggleFullscreen: () => void; showControls: boolean; showControlsFullScreen: boolean; isFullScreen: boolean; - playerRef: any - increaseSpeed: ()=> void - decreaseSpeed: ()=> void - playbackRate: number - openSubtitleManager: ()=> void - subtitleBtnRef: any - onSelectPlaybackRate: (rate: number)=> void; - isMuted: boolean - toggleMute: ()=> void - openPlaybackMenu: ()=> void - togglePictureInPicture: ()=> void - isVideoPlayerSmall: boolean - setLocalProgress: (val: number)=> void + playerRef: any; + increaseSpeed: () => void; + decreaseSpeed: () => void; + playbackRate: number; + openSubtitleManager: () => void; + subtitleBtnRef: any; + onSelectPlaybackRate: (rate: number) => void; + isMuted: boolean; + toggleMute: () => void; + openPlaybackMenu: () => void; + togglePictureInPicture: () => void; + isVideoPlayerSmall: boolean; + setLocalProgress: (val: number) => void; } -export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, 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, isVideoPlayerSmall}: VideoControlsBarProps) => { - +export const VideoControlsBar = ({ + subtitleBtnRef, + setLocalProgress, + showControls, + playbackRate, + increaseSpeed, + decreaseSpeed, + isFullScreen, + showControlsFullScreen, + reloadVideo, + onVolumeChange, + volume, + isPlaying, + canPlay, + isScreenSmall, + controlsHeight, + playerRef, + duration, + progress, + togglePlay, + toggleFullscreen, + openSubtitleManager, + onSelectPlaybackRate, + isMuted, + toggleMute, + openPlaybackMenu, + togglePictureInPicture, + isVideoPlayerSmall, +}: VideoControlsBarProps) => { const showMobileControls = isScreenSmall && canPlay; const controlGroupSX = { @@ -56,13 +79,13 @@ export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, showControls height: controlsHeight, }; - let additionalStyles: React.CSSProperties = {} - if(isFullScreen && showControlsFullScreen){ + let additionalStyles: React.CSSProperties = {}; + if (isFullScreen && showControlsFullScreen) { additionalStyles = { opacity: 1, - position: 'fixed', - bottom: 0 - } + position: "fixed", + bottom: 0, + }; } return ( @@ -70,58 +93,72 @@ export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, showControls style={{ padding: "0px", opacity: showControls ? 1 : 0, - pointerEvents: showControls ? 'auto' : 'none', - transition: 'opacity 0.4s ease-in-out', - width: '100%' - // ...additionalStyles - // height: controlsHeight, + pointerEvents: showControls ? "auto" : "none", + transition: "opacity 0.4s ease-in-out", + width: "100%", }} > - {showMobileControls ? ( - null - ) : canPlay ? ( - - - - {!isVideoPlayerSmall && ( - - - - + {showMobileControls ? null : canPlay ? ( + + + {!isVideoPlayerSmall && ( + + + + - + + + - - - - - - - {/* */} - + + {/* */} + + - - - - - {/* */} - - - - )} - + + + + {/* */} + + + + )} ) : null} diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index 70f2b4d..0897de5 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -1,9 +1,6 @@ import { - ReactEventHandler, - Ref, RefObject, useCallback, - useContext, useEffect, useLayoutEffect, useMemo, @@ -13,33 +10,24 @@ import { import { QortalGetMetadata } from "../../types/interfaces/resources"; import { VideoContainer, VideoElement } from "./VideoPlayer-styles"; import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys"; -import { useProgressStore, useVideoStore } from "../../state/video"; +import { + useIsPlaying, + useProgressStore, + useVideoStore, +} from "../../state/video"; import { useVideoPlayerController } from "./useVideoPlayerController"; import { LoadingVideo } from "./LoadingVideo"; import { VideoControlsBar } from "./VideoControlsBar"; import videojs from "video.js"; import "video.js/dist/video-js.css"; -import Player from "video.js/dist/types/player"; -import { - Subtitle, - SubtitleManager, - SubtitleManagerProps, - SubtitlePublishedData, -} from "./SubtitleManager"; +import { SubtitleManager, SubtitlePublishedData } from "./SubtitleManager"; import { base64ToBlobUrl } from "../../utils/base64"; import convert from "srt-webvtt"; import { TimelineActionsComponent } from "./TimelineActionsComponent"; import { PlayBackMenu } from "./VideoControls"; import { useGlobalPlayerStore } from "../../state/pip"; -import { - alpha, - Box, - ClickAwayListener, - Drawer, - List, - ListItem, -} from "@mui/material"; +import { alpha, ClickAwayListener, Drawer } from "@mui/material"; import { MobileControls } from "./MobileControls"; import { useLocation } from "react-router-dom"; @@ -84,14 +72,17 @@ export type TimelineAction = onClick: () => void; // ✅ Required for CUSTOM placement?: "TOP-RIGHT" | "TOP-LEFT" | "BOTTOM-LEFT" | "BOTTOM-RIGHT"; }; -interface VideoPlayerProps { +export interface VideoPlayerProps { qortalVideoResource: QortalGetMetadata; - videoRef: Ref; + videoRef: any; retryAttempts?: number; poster?: string; autoPlay?: boolean; onEnded?: (e: React.SyntheticEvent) => void; timelineActions?: TimelineAction[]; + playerRef: any; + locationRef: RefObject; + videoLocationRef: RefObject; } const videoStyles = { @@ -117,19 +108,20 @@ export const isTouchDevice = export const VideoPlayer = ({ videoRef, + playerRef, qortalVideoResource, retryAttempts, poster, autoPlay, onEnded, timelineActions, + locationRef, + videoLocationRef, }: VideoPlayerProps) => { const containerRef = useRef(null); const [videoObjectFit] = useState("contain"); - const [isPlaying, setIsPlaying] = useState(false); - + const { isPlaying, setIsPlaying } = useIsPlaying(); const [width, setWidth] = useState(0); - console.log("width", width); useEffect(() => { const observer = new ResizeObserver(([entry]) => { setWidth(entry.contentRect.width); @@ -145,12 +137,11 @@ export const VideoPlayer = ({ playbackRate: state.playbackSettings.playbackRate, }) ); - const playerRef = useRef(null); + // const playerRef = useRef(null); const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false); const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false); const [showControlsMobile2, setShowControlsMobile] = useState(false); const [isPlayerInitialized, setIsPlayerInitialized] = useState(false); - const [videoCodec, setVideoCodec] = useState(null); const [isMuted, setIsMuted] = useState(false); const { setProgress } = useProgressStore(); const [localProgress, setLocalProgress] = useState(0); @@ -160,9 +151,8 @@ export const VideoPlayer = ({ const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false); const subtitleBtnRef = useRef(null); const [currentSubTrack, setCurrentSubTrack] = useState(null); - const location = useLocation() + const location = useLocation(); - const locationRef = useRef(null); const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false); const isVideoPlayerSmall = width < 600 || isTouchDevice; const { @@ -176,36 +166,33 @@ export const VideoPlayer = ({ toggleObjectFit, controlsHeight, setProgressRelative, - toggleAlwaysShowControls, changeVolume, - startedFetch, isReady, resourceUrl, startPlay, setProgressAbsolute, - setAlwaysShowControls, status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate, seekTo, togglePictureInPicture, - downloadResource + downloadResource, } = useVideoPlayerController({ autoPlay, playerRef, qortalVideoResource, retryAttempts, - isPlayerInitialized, isMuted, videoRef, }); - const showControlsMobile = (showControlsMobile2 || !isPlaying) && isVideoPlayerSmall + const showControlsMobile = + (showControlsMobile2 || !isPlaying) && isVideoPlayerSmall; useEffect(() => { if (location) { - locationRef.current = location.pathname; + locationRef.current = location?.pathname; } }, [location]); @@ -243,10 +230,6 @@ export const VideoPlayer = ({ } }, []); - // const exitFullscreen = useCallback(() => { - // document?.exitFullscreen(); - // }, [isFullscreen]); - const exitFullscreen = useCallback(async () => { try { if (document.fullscreenElement) { @@ -275,8 +258,8 @@ export const VideoPlayer = ({ }, [isFullscreen]); const toggleFullscreen = useCallback(() => { - setShowControls(false) - setShowControlsMobile(false) + setShowControls(false); + setShowControlsMobile(false); isFullscreen ? exitFullscreen() : enterFullscreen(); }, [isFullscreen]); @@ -286,13 +269,11 @@ export const VideoPlayer = ({ togglePlay, setProgressRelative, toggleObjectFit, - toggleAlwaysShowControls, increaseSpeed, decreaseSpeed, changeVolume, toggleMute, setProgressAbsolute, - setAlwaysShowControls, toggleFullscreen, }), [ @@ -300,13 +281,11 @@ export const VideoPlayer = ({ togglePlay, setProgressRelative, toggleObjectFit, - toggleAlwaysShowControls, increaseSpeed, decreaseSpeed, changeVolume, toggleMute, setProgressAbsolute, - setAlwaysShowControls, toggleFullscreen, ] ); @@ -318,7 +297,7 @@ export const VideoPlayer = ({ const openSubtitleManager = useCallback(() => { if (isVideoPlayerSmall) { setDrawerOpenSubtitles(true); - return + return; } setIsOpenSubtitleManage(true); }, [isVideoPlayerSmall]); @@ -327,7 +306,6 @@ export const VideoPlayer = ({ if (!qortalVideoResource) return null; return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`; }, [qortalVideoResource]); - const videoLocationRef = useRef(null); useEffect(() => { videoLocationRef.current = videoLocation; }, [videoLocation]); @@ -352,15 +330,6 @@ export const VideoPlayer = ({ } } }, [videoLocation]); - // useEffect(() => { - // const ref = videoRef as React.RefObject; - // if (!ref.current) return; - // if (ref.current) { - // ref.current.volume = volume; - // } - // // Only run on mount - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, []); const onPlay = useCallback(() => { setIsPlaying(true); @@ -385,7 +354,6 @@ export const VideoPlayer = ({ const videoStylesContainer = useMemo(() => { return { cursor: "auto", - // aspectRatio: "16 / 9", ...videoStyles?.videoContainer, }; }, [showControls, isVideoPlayerSmall]); @@ -432,37 +400,6 @@ export const VideoPlayer = ({ }; }, [isPlayerInitialized]); - const canvasRef = useRef(null); - const videoRefForCanvas = useRef(null); - const extractFrames = useCallback((time: number): void => { - // const video = videoRefForCanvas?.current; - // const canvas: any = canvasRef.current; - // if (!video || !canvas) return null; - // // Avoid unnecessary resize if already correct - // if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { - // canvas.width = video.videoWidth; - // canvas.height = video.videoHeight; - // } - // const context = canvas.getContext("2d"); - // if (!context) return null; - // // If video is already near the correct time, don't seek again - // const threshold = 0.01; // 10ms threshold - // if (Math.abs(video.currentTime - time) > threshold) { - // await new Promise((resolve) => { - // const onSeeked = () => resolve(); - // video.addEventListener("seeked", onSeeked, { once: true }); - // video.currentTime = time; - // }); - // } - // context.drawImage(video, 0, 0, canvas.width, canvas.height); - // // Use a faster method for image export (optional tradeoff) - // const blob = await new Promise((resolve) => { - // canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); - // }); - // if (!blob) return null; - // return URL.createObjectURL(blob); - }, []); - const hideTimeout = useRef(null); const resetHideTimer = () => { @@ -476,7 +413,6 @@ export const VideoPlayer = ({ const handleMouseMove = () => { if (isVideoPlayerSmall) return; - console.log('going 222') resetHideTimer(); }; @@ -591,24 +527,6 @@ export const VideoPlayer = ({ true ); - // Remove all existing remote text tracks - // try { - // const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_ - // if (remoteTracks && remoteTracks?.length) { - // const toRemove: TextTrack[] = []; - // for (let i = 0; i < remoteTracks.length; i++) { - // const track = remoteTracks[i]; - // toRemove.push(track); - // } - // toRemove.forEach((track) => { - // console.log('removing track') - // playerRef.current?.removeRemoteTextTrack(track); - // }); - // } - // } catch (error) { - // console.log('error2', error) - // } - await new Promise((res) => { setTimeout(() => { res(null); @@ -660,12 +578,10 @@ export const VideoPlayer = ({ return; const resource = JSON.parse(videoLocactionStringified); - let canceled = false; try { const setupPlayer = async () => { const type = await getVideoMimeTypeFromUrl(resource); - if (canceled) return; const options = { autoplay: true, @@ -723,7 +639,6 @@ export const VideoPlayer = ({ setCurrentSubTrack(activeTrack.language || activeTrack.srclang); } else { setCurrentSubTrack(null); - console.log("No subtitle is currently showing"); } }; @@ -736,7 +651,6 @@ export const VideoPlayer = ({ playerRef.current?.on("error", () => { const error = playerRef.current?.error(); console.error("Video.js playback error:", error); - // Optional: display user-friendly message }); } }; @@ -745,39 +659,6 @@ export const VideoPlayer = ({ } catch (error) { console.error("useEffect start player", error); } - return () => { - const video = savedVideoRef as any; - const videoEl = video?.current!; - const player = playerRef.current; - - 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; - - if (player && typeof player.dispose === "function") { - try { - player.dispose(); - } catch (err) { - console.error("Error disposing Video.js player:", err); - } - playerRef.current = null; - } - }; }, [isReady, resourceUrl, startPlay, poster, videoLocactionStringified]); useEffect(() => { @@ -815,29 +696,22 @@ export const VideoPlayer = ({ if (!container) return; container.addEventListener("touchstart", handleInteraction); - // container.addEventListener('mousemove', handleInteraction); return () => { container.removeEventListener("touchstart", handleInteraction); - // container.removeEventListener('mousemove', handleInteraction); }; }, []); - const handleClickVideoElement = useCallback(()=> { - if(isVideoPlayerSmall){ - resetHideTimeout() - return + const handleClickVideoElement = useCallback(() => { + if (isVideoPlayerSmall) { + resetHideTimeout(); + return; } - console.log('sup') - togglePlay() - }, [isVideoPlayerSmall, togglePlay]) - - console.log("showControlsMobile", isVideoPlayerSmall); + togglePlay(); + }, [isVideoPlayerSmall, togglePlay]); return ( <> - {/* */} - {!isVideoPlayerSmall && ( + isFromDrawer={false} + close={closePlaybackMenu} + isOpen={isOpenPlaybackMenu} + onSelect={onSelectPlaybackRate} + playbackRate={playbackRate} + /> )} - {isReady && showControls && ( )} - setDrawerOpenSubtitles(false)}> - setDrawerOpenSubtitles(false)}> + - - - + }} + > + + + setDrawerOpenPlayback(false)}> ) => void; + timelineActions?: TimelineAction[]; +} +export const VideoPlayerParent = ({ + videoRef, + qortalVideoResource, + retryAttempts, + poster, + autoPlay, + onEnded, + timelineActions, +}: VideoPlayerParentProps) => { + const playerRef = useRef(null); + const locationRef = useRef(null); + const videoLocationRef = useRef(null); + const { isPlaying, setIsPlaying } = useIsPlaying(); + const isPlayingRef = useRef(false); + useEffect(() => { + isPlayingRef.current = isPlaying; + }, [isPlaying]); + + useEffect(() => { + return () => { + const player = playerRef.current; + + const isPlaying = isPlayingRef.current; + const currentSrc = player?.currentSrc(); + + if (currentSrc && isPlaying && videoLocationRef.current) { + const current = player?.currentTime?.(); + const currentSource = player?.currentType(); + + useGlobalPlayerStore.getState().setVideoState({ + videoSrc: currentSrc, + currentTime: current ?? 0, + isPlaying: true, + mode: "floating", + videoId: videoLocationRef.current, + location: locationRef.current || "", + type: currentSource || "video/mp4", + }); + } + }; + }, []); + useEffect(() => { + return () => { + const player = playerRef.current; + + setIsPlaying(false); + + if (player && typeof player.dispose === "function") { + try { + player.dispose(); + } catch (err) { + console.error("Error disposing Video.js player:", err); + } + playerRef.current = null; + } + }; + }, [ + qortalVideoResource?.service, + qortalVideoResource?.name, + qortalVideoResource?.identifier, + ]); + + return ( + + ); +}; diff --git a/src/components/VideoPlayer/useVideoPlayerController.tsx b/src/components/VideoPlayer/useVideoPlayerController.tsx index ef78de1..da19fb8 100644 --- a/src/components/VideoPlayer/useVideoPlayerController.tsx +++ b/src/components/VideoPlayer/useVideoPlayerController.tsx @@ -1,14 +1,5 @@ -import { - useState, - useEffect, - RefObject, - useMemo, - useCallback, - Ref, - useRef, - useImperativeHandle, -} from "react"; -import { useProgressStore, useVideoStore } from "../../state/video"; +import { useState, useEffect, useCallback, useRef } from "react"; +import { useVideoStore } from "../../state/video"; import { QortalGetMetadata } from "../../types/interfaces/resources"; import { useResourceStatus } from "../../hooks/useResourceStatus"; import useIdleTimeout from "../../common/useIdleTimeout"; @@ -24,81 +15,70 @@ interface UseVideoControls { autoPlay?: boolean; qortalVideoResource: QortalGetMetadata; retryAttempts?: number; - isPlayerInitialized: boolean - isMuted: boolean - videoRef: any + isMuted: boolean; + videoRef: any; } export const useVideoPlayerController = (props: UseVideoControls) => { - const { autoPlay, videoRef , playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, isMuted } = props; + const { + autoPlay, + videoRef, + playerRef, + qortalVideoResource, + retryAttempts, + isMuted, + } = props; const [isFullscreen, setIsFullscreen] = useState(false); - const [showControlsFullScreen, setShowControlsFullScreen] = useState(false) + const [showControlsFullScreen, setShowControlsFullScreen] = useState(false); const [videoObjectFit, setVideoObjectFit] = useState<"contain" | "fill">( "contain" ); - const [alwaysShowControls, setAlwaysShowControls] = useState(false); const [startPlay, setStartPlay] = useState(false); const [startedFetch, setStartedFetch] = useState(false); const startedFetchRef = useRef(false); - const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore(); - const { getProgress } = useProgressStore(); + const { playbackSettings, setPlaybackRate } = useVideoStore(); - const { isReady, resourceUrl, status, percentLoaded, downloadResource } = useResourceStatus({ - resource: !startedFetch ? null : qortalVideoResource, - retryAttempts, - }); - - const idleTime = 5000; // Time in milliseconds - useIdleTimeout({ - onIdle: () => (setShowControlsFullScreen(false)), - onActive: () => (setShowControlsFullScreen(true)), - idleTime, + const { isReady, resourceUrl, status, percentLoaded, downloadResource } = + useResourceStatus({ + resource: !startedFetch ? null : qortalVideoResource, + retryAttempts, }); - - const videoLocation = useMemo(() => { - if (!qortalVideoResource) return null; - return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`; - }, [qortalVideoResource]); - - - - const [playbackRate, _setLocalPlaybackRate] = useState( - playbackSettings.playbackRate - ); + const idleTime = 5000; // Time in milliseconds + useIdleTimeout({ + onIdle: () => setShowControlsFullScreen(false), + onActive: () => setShowControlsFullScreen(true), + idleTime, + }); const updatePlaybackRate = useCallback( - (newSpeed: number) => { - try { - const player = playerRef.current; - if (!player) return; + (newSpeed: number) => { + try { + const player = playerRef.current; + if (!player) return; - if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; - - const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed); - player.playbackRate(clampedSpeed); // ✅ Video.js API - - // _setLocalPlaybackRate(clampedSpeed); - // setPlaybackRate(clampedSpeed); - } catch (error) { - console.error('updatePlaybackRate', error) - } - }, - [setPlaybackRate, _setLocalPlaybackRate, minSpeed, maxSpeed] -); + if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; + const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed); + player.playbackRate(clampedSpeed); // ✅ Video.js API + } catch (error) { + console.error("updatePlaybackRate", error); + } + }, + [setPlaybackRate, minSpeed, maxSpeed] + ); const increaseSpeed = useCallback( (wrapOverflow = true) => { try { const changedSpeed = playbackSettings.playbackRate + speedChange; - const newSpeed = wrapOverflow - ? changedSpeed - : Math.min(changedSpeed, maxSpeed); - updatePlaybackRate(newSpeed); + const newSpeed = wrapOverflow + ? changedSpeed + : Math.min(changedSpeed, maxSpeed); + updatePlaybackRate(newSpeed); } catch (error) { - console.error('increaseSpeed', increaseSpeed) + console.error("increaseSpeed", increaseSpeed); } }, [updatePlaybackRate, playbackSettings.playbackRate] @@ -108,10 +88,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => { updatePlaybackRate(playbackSettings.playbackRate - speedChange); }, [updatePlaybackRate, playbackSettings.playbackRate]); - const toggleAlwaysShowControls = useCallback(() => { - setAlwaysShowControls((prev) => !prev); - }, [setAlwaysShowControls]); - useEffect(() => { const handleFullscreenChange = () => { setIsFullscreen(!!document.fullscreenElement); @@ -121,153 +97,152 @@ export const useVideoPlayerController = (props: UseVideoControls) => { document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); - const onVolumeChange = useCallback( - (_: any, value: number | number[]) => { - try { - const newVolume = value as number; + const onVolumeChange = useCallback((_: any, value: number | number[]) => { + try { + const newVolume = value as number; const ref = playerRef as any; if (!ref.current) return; if (ref.current) { playerRef.current?.volume(newVolume); - } - } catch (error) { - console.error('onVolumeChange', error) - } - }, - [] - ); + } catch (error) { + console.error("onVolumeChange", error); + } + }, []); const toggleMute = useCallback(() => { try { - const ref = playerRef as any; - if (!ref.current) return; - + const ref = playerRef as any; + if (!ref.current) return; - ref.current?.muted(!isMuted) + ref.current?.muted(!isMuted); } catch (error) { - console.error('toggleMute', toggleMute) + console.error("toggleMute", toggleMute); } }, [isMuted]); - const changeVolume = useCallback( - (delta: number) => { + const changeVolume = useCallback((delta: number) => { try { const player = playerRef.current; - if (!player || typeof player.volume !== 'function') return; + if (!player || typeof player.volume !== "function") return; - const currentVolume = player.volume(); // Get current volume (0–1) - let newVolume = Math.max(0, Math.min(currentVolume + delta, 1)); - newVolume = +newVolume.toFixed(2); // Round to 2 decimal places + const currentVolume = player.volume(); // Get current volume (0–1) + let newVolume = Math.max(0, Math.min(currentVolume + delta, 1)); + newVolume = +newVolume.toFixed(2); // Round to 2 decimal places - player.volume(newVolume); // Set new volume - player.muted(false); // Ensure it's unmuted + player.volume(newVolume); // Set new volume + player.muted(false); // Ensure it's unmuted } catch (error) { - console.error('changeVolume', error) + console.error("changeVolume", error); } - }, - [] -); + }, []); - + const setProgressRelative = useCallback((seconds: number) => { + try { + const player = playerRef.current; + if ( + !player || + typeof player.currentTime !== "function" || + typeof player.duration !== "function" + ) + return; -const setProgressRelative = useCallback((seconds: number) => { - try { - const player = playerRef.current; - if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return; + const current = player.currentTime(); + const duration = player.duration() || 100; + const newTime = Math.max(0, Math.min(current + seconds, duration)); - const current = player.currentTime(); - const duration = player.duration() || 100; - const newTime = Math.max(0, Math.min(current + seconds, duration)); + player.currentTime(newTime); + } catch (error) { + console.error("setProgressRelative", error); + } + }, []); - player.currentTime(newTime); - } catch (error) { - console.error('setProgressRelative', error) - } -}, []); + const setProgressAbsolute = useCallback((percent: number) => { + try { + const player = playerRef.current; + if ( + !player || + typeof player.duration !== "function" || + typeof player.currentTime !== "function" + ) + return; + const duration = player.duration(); + const clampedPercent = Math.min(100, Math.max(0, percent)); + const finalTime = (duration * clampedPercent) / 100; - const setProgressAbsolute = useCallback((percent: number) => { -try { - const player = playerRef.current; - if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return; + player.currentTime(finalTime); + } catch (error) { + console.error("setProgressAbsolute", error); + } + }, []); - const duration = player.duration(); - const clampedPercent = Math.min(100, Math.max(0, percent)); - const finalTime = (duration * clampedPercent) / 100; - - player.currentTime(finalTime); -} catch (error) { - console.error('setProgressAbsolute', error) -} -}, []); - - const seekTo = useCallback((time: number) => { -try { - const player = playerRef.current; - if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return; - - player.currentTime(time); -} catch (error) { - console.error('setProgressAbsolute', error) -} -}, []); + const seekTo = useCallback((time: number) => { + try { + const player = playerRef.current; + if ( + !player || + typeof player.duration !== "function" || + typeof player.currentTime !== "function" + ) + return; + player.currentTime(time); + } catch (error) { + console.error("setProgressAbsolute", error); + } + }, []); const toggleObjectFit = useCallback(() => { setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain"); }, [setVideoObjectFit]); -const togglePlay = useCallback(async () => { - - - try { - if (!startedFetchRef.current) { - setStartedFetch(true); - startedFetchRef.current = true; - setStartPlay(true); - return; - } - const player = playerRef.current; - if (!player) return; - if (isReady) { - if (player.paused()) { - try { - await player.play(); - } catch (err) { - console.warn('Play failed:', err); + const togglePlay = useCallback(async () => { + try { + if (!startedFetchRef.current) { + setStartedFetch(true); + startedFetchRef.current = true; + setStartPlay(true); + return; } - } else { - player.pause(); + const player = playerRef.current; + if (!player) return; + if (isReady) { + if (player.paused()) { + try { + await player.play(); + } catch (err) { + console.warn("Play failed:", err); + } + } else { + player.pause(); + } + } + } catch (error) { + console.error("togglePlay", error); } - } - } catch (error) { - console.error('togglePlay', error) - } -}, [setStartedFetch, isReady]); + }, [setStartedFetch, isReady]); + const reloadVideo = useCallback(async () => { + try { + const player = playerRef.current; + if (!player || !isReady || !resourceUrl) return; - const reloadVideo = useCallback(async () => { - try { - const player = playerRef.current; - if (!player || !isReady || !resourceUrl) return; + const currentTime = player.currentTime(); - const currentTime = player.currentTime(); - - player.src({ src: resourceUrl, type: 'video/mp4' }); // Adjust type if needed - player.load(); - - player.ready(() => { - player.currentTime(currentTime); - player.play().catch((err: any) => { - console.warn('Playback failed after reload:', err); - }); - }); - } catch (error) { - console.error(error) - } -}, [isReady, resourceUrl]); + player.src({ src: resourceUrl, type: "video/mp4" }); // Adjust type if needed + player.load(); + player.ready(() => { + player.currentTime(currentTime); + player.play().catch((err: any) => { + console.warn("Playback failed after reload:", err); + }); + }); + } catch (error) { + console.error(error); + } + }, [isReady, resourceUrl]); useEffect(() => { if (autoPlay) togglePlay(); @@ -279,34 +254,25 @@ 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 () => { + const togglePictureInPicture = async () => { if (!videoRef.current) return; - const player = playerRef.current; - if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') 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' - }); + const current = player.currentTime(); + useGlobalPlayerStore.getState().setVideoState({ + videoSrc: videoRef.current.src, + currentTime: current, + isPlaying: true, + mode: "floating", // or 'floating' + }); }; - return { reloadVideo, togglePlay, @@ -318,14 +284,18 @@ const togglePlay = useCallback(async () => { toggleObjectFit, controlsHeight, setProgressRelative, - toggleAlwaysShowControls, changeVolume, setProgressAbsolute, - setAlwaysShowControls, startedFetch, isReady, resourceUrl, startPlay, - status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo, togglePictureInPicture, downloadResource + status, + percentLoaded, + showControlsFullScreen, + onSelectPlaybackRate: updatePlaybackRate, + seekTo, + togglePictureInPicture, + downloadResource, }; }; diff --git a/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx b/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx index ff19e66..10914f9 100644 --- a/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx +++ b/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx @@ -3,10 +3,8 @@ import { useEffect, useCallback } from 'react'; interface UseVideoControls { reloadVideo: () => void; togglePlay: () => void; - setAlwaysShowControls: React.Dispatch>; setProgressRelative: (seconds: number) => void; toggleObjectFit: () => void; - toggleAlwaysShowControls: () => void; increaseSpeed: (wrapOverflow?: boolean) => void; decreaseSpeed: () => void; changeVolume: (delta: number) => void; @@ -21,7 +19,6 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => { togglePlay, setProgressRelative, toggleObjectFit, - toggleAlwaysShowControls, increaseSpeed, decreaseSpeed, changeVolume, @@ -51,9 +48,6 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => { case "f": toggleFullscreen(); break; - case "c": - toggleAlwaysShowControls(); - break; case "+": case ">": increaseSpeed(false); @@ -126,7 +120,7 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => { togglePlay, setProgressRelative, toggleObjectFit, - toggleAlwaysShowControls, + increaseSpeed, decreaseSpeed, changeVolume, diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index 32c31d5..2995e53 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -58,7 +58,6 @@ export const GlobalProvider = ({ // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); const isPublishing = useMultiplePublishStore((s)=> s.isPublishing); - const videoSrc = useGlobalPlayerStore((s)=> s.videoSrc); const appInfo = useAppInfo(config.appName, config?.publicSalt); const lists = useResources(); const identifierOperations = useIdentifiers( @@ -83,7 +82,7 @@ export const GlobalProvider = ({ [auth, lists, appInfo, identifierOperations, persistentOperations] ); const { clearOldProgress } = useProgressStore(); - + useEffect(() => { clearOldProgress(); }, []); @@ -91,7 +90,10 @@ export const GlobalProvider = ({ return ( - + + + + {isPublishing && ( diff --git a/src/index.ts b/src/index.ts index 55d1860..05b693b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ export { useModal } from './hooks/useModal'; export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls'; export {TimelineAction} from './components/VideoPlayer/VideoPlayer' export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys'; -export { VideoPlayer } from './components/VideoPlayer/VideoPlayer'; +export { VideoPlayerParent as VideoPlayer } from './components/VideoPlayer/VideoPlayerParent'; export { useListReturn } from './hooks/useListData'; import './index.css' export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events'; diff --git a/src/state/video.ts b/src/state/video.ts index ecf7326..814dba8 100644 --- a/src/state/video.ts +++ b/src/state/video.ts @@ -118,3 +118,14 @@ export const useProgressStore = create()( } ) ); + + +interface IsPlayingState { + isPlaying: boolean; + setIsPlaying: (value: boolean) => void; +} + +export const useIsPlaying = create((set) => ({ + isPlaying: false, + setIsPlaying: (value) => set({ isPlaying: value }), +})); \ No newline at end of file diff --git a/src/utils/qortal.ts b/src/utils/qortal.ts index da2b0cc..7802d1b 100644 --- a/src/utils/qortal.ts +++ b/src/utils/qortal.ts @@ -4,14 +4,19 @@ export const createAvatarLink = (qortalName: string)=> { const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); -export const createQortalLink = (type: 'APP' | 'WEBSITE', appName: string, path: string) => { +export const createQortalLink = ( + type: 'APP' | 'WEBSITE', + appName: string, + path: string +) => { + const encodedAppName = encodeURIComponent(appName); + let link = `qortal://${type}/${encodedAppName}`; - let link = 'qortal://' + type + '/' + appName - if(path && path.startsWith('/')){ - link = link + removeTrailingSlash(path) - } - if(path && !path.startsWith('/')){ - link = link + '/' + removeTrailingSlash(path) - } - return link - }; \ No newline at end of file + if (path) { + link += path.startsWith('/') + ? removeTrailingSlash(path) + : '/' + removeTrailingSlash(path); + } + + return link; +}; \ No newline at end of file