fixes and update version

This commit is contained in:
PhilReact 2025-06-26 22:40:00 +03:00
parent 205da3ca24
commit 98a51c0b5f
15 changed files with 950 additions and 961 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.34", "version": "1.0.35",
"description": "Qortal's core React library with global state, UI components, and utilities", "description": "Qortal's core React library with global state, UI components, and utilities",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.mjs", "module": "dist/index.mjs",

View File

@ -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 { PlayArrow } from "@mui/icons-material";
import { Status } from "../../state/publishes"; import { Status } from "../../state/publishes";
interface LoadingVideoProps { interface LoadingVideoProps {
status: Status | null status: Status | null;
percentLoaded: number percentLoaded: number;
isReady: boolean isReady: boolean;
isLoading: boolean isLoading: boolean;
togglePlay: ()=> void togglePlay: () => void;
startPlay: boolean, startPlay: boolean;
downloadResource: ()=> void downloadResource: () => void;
} }
export const LoadingVideo = ({ export const LoadingVideo = ({
status, percentLoaded, isReady, isLoading, togglePlay, startPlay, downloadResource status,
percentLoaded,
isReady,
isLoading,
togglePlay,
startPlay,
downloadResource,
}: LoadingVideoProps) => { }: LoadingVideoProps) => {
const getDownloadProgress = (percentLoaded: number) => { const getDownloadProgress = (percentLoaded: number) => {
const progress = percentLoaded; const progress = percentLoaded;
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%"; return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
}; };
if(status === 'READY') return null if (status === "READY") return null;
return ( return (
<> <>
{isLoading && status !== 'INITIAL' && ( {isLoading && status !== "INITIAL" && (
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
@ -36,7 +47,7 @@ export const LoadingVideo = ({
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
zIndex={500} zIndex={500}
bgcolor={alpha('#000000', !startPlay ? 0 : 0.95)} bgcolor={alpha("#000000", !startPlay ? 0 : 0.95)}
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -45,32 +56,27 @@ export const LoadingVideo = ({
}} }}
> >
{status !== "NOT_PUBLISHED" && status !== "FAILED_TO_DOWNLOAD" && ( {status !== "NOT_PUBLISHED" && status !== "FAILED_TO_DOWNLOAD" && (
<CircularProgress sx={{ <CircularProgress
color: 'white' sx={{
}} /> color: "white",
}}
/>
)} )}
{status && ( {status && (
<Typography <Typography
component="div" component="div"
sx={{ sx={{
color: "white", color: "white",
fontSize: "15px", fontSize: "15px",
textAlign: "center", textAlign: "center",
fontFamily: "sans-serif" fontFamily: "sans-serif",
}} }}
> >
{status === "NOT_PUBLISHED" ? ( {status === "NOT_PUBLISHED" ? (
<>Video file was not published. Please inform the publisher!</> <>Video file was not published. Please inform the publisher!</>
) : status === "REFETCHING" ? ( ) : status === "REFETCHING" ? (
<> <>
<> <>{getDownloadProgress(percentLoaded)}</>
{getDownloadProgress(
percentLoaded
)}
</>
<> Refetching in 25 seconds</> <> Refetching in 25 seconds</>
</> </>
@ -79,31 +85,30 @@ export const LoadingVideo = ({
) : status === "FAILED_TO_DOWNLOAD" ? ( ) : status === "FAILED_TO_DOWNLOAD" ? (
<>Unable to fetch video chunks from peers</> <>Unable to fetch video chunks from peers</>
) : ( ) : (
<> <>{getDownloadProgress(percentLoaded)}</>
{getDownloadProgress(
percentLoaded
)}
</>
)} )}
</Typography> </Typography>
)} )}
{status === 'FAILED_TO_DOWNLOAD' && ( {status === "FAILED_TO_DOWNLOAD" && (
<Button variant="outlined" onClick={downloadResource} sx={{ <Button
color: 'white' variant="outlined"
}}>Try again</Button> onClick={downloadResource}
sx={{
color: "white",
}}
>
Try again
</Button>
)} )}
</Box> </Box>
)} )}
{(status === 'INITIAL') && ( {status === "INITIAL" && (
<> <>
<IconButton <IconButton
onClick={() => { onClick={() => {
togglePlay(); togglePlay();
}} }}
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
position: "absolute", position: "absolute",
@ -112,10 +117,9 @@ export const LoadingVideo = ({
right: 0, right: 0,
bottom: 0, bottom: 0,
zIndex: 501, zIndex: 501,
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
padding: '0px', padding: "0px",
borderRadius: "0px", borderRadius: "0px",
}} }}
> >
<PlayArrow <PlayArrow

View File

@ -22,7 +22,7 @@ interface MobileControlsProps {
toggleFullscreen: () => void; toggleFullscreen: () => void;
setProgressRelative: (val: number) => void; setProgressRelative: (val: number) => void;
setLocalProgress: (val: number) => void; setLocalProgress: (val: number) => void;
resetHideTimeout: ()=> void resetHideTimeout: () => void;
} }
export const MobileControls = ({ export const MobileControls = ({
showControlsMobile, showControlsMobile,
@ -37,7 +37,7 @@ export const MobileControls = ({
toggleFullscreen, toggleFullscreen,
setProgressRelative, setProgressRelative,
setLocalProgress, setLocalProgress,
resetHideTimeout resetHideTimeout,
}: MobileControlsProps) => { }: MobileControlsProps) => {
return ( return (
<Box <Box
@ -70,33 +70,35 @@ export const MobileControls = ({
openSubtitleManager(); openSubtitleManager();
}} }}
sx={{ sx={{
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '7px' padding: "7px",
}} }}
> >
<SubtitlesIcon <SubtitlesIcon
sx={{ sx={{
fontSize: "24px", fontSize: "24px",
color: 'white' color: "white",
}} }}
/> />
</IconButton> </IconButton>
<IconButton <IconButton
sx={{ sx={{
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '7px' padding: "7px",
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
openPlaybackMenu(); openPlaybackMenu();
}} }}
> >
<SlowMotionVideoIcon sx={{ <SlowMotionVideoIcon
sx={{
fontSize: "24px", fontSize: "24px",
color: 'white' color: "white",
}}/> }}
/>
</IconButton> </IconButton>
</Box> </Box>
<Box <Box
@ -106,17 +108,17 @@ export const MobileControls = ({
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
gap: "50px", gap: "50px",
display: 'flex', display: "flex",
alignItems: 'center' alignItems: "center",
}} }}
> >
<IconButton <IconButton
sx={{ sx={{
opacity: 1, opacity: 1,
zIndex: 2, zIndex: 2,
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '10px' padding: "10px",
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -126,7 +128,7 @@ export const MobileControls = ({
<Replay10Icon <Replay10Icon
sx={{ sx={{
fontSize: "36px", fontSize: "36px",
color: 'white' color: "white",
}} }}
/> />
</IconButton> </IconButton>
@ -135,9 +137,9 @@ export const MobileControls = ({
sx={{ sx={{
opacity: 1, opacity: 1,
zIndex: 2, zIndex: 2,
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '10px' padding: "10px",
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -147,7 +149,7 @@ export const MobileControls = ({
<PauseIcon <PauseIcon
sx={{ sx={{
fontSize: "36px", fontSize: "36px",
color: 'white' color: "white",
}} }}
/> />
</IconButton> </IconButton>
@ -157,9 +159,9 @@ export const MobileControls = ({
sx={{ sx={{
opacity: 1, opacity: 1,
zIndex: 2, zIndex: 2,
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '10px' padding: "10px",
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -169,7 +171,7 @@ export const MobileControls = ({
<PlayArrowIcon <PlayArrowIcon
sx={{ sx={{
fontSize: "36px", fontSize: "36px",
color: 'white' color: "white",
}} }}
/> />
</IconButton> </IconButton>
@ -178,9 +180,9 @@ export const MobileControls = ({
sx={{ sx={{
opacity: 1, opacity: 1,
zIndex: 2, zIndex: 2,
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '10px' padding: "10px",
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -190,7 +192,7 @@ export const MobileControls = ({
<Forward10Icon <Forward10Icon
sx={{ sx={{
fontSize: "36px", fontSize: "36px",
color: 'white' color: "white",
}} }}
/> />
</IconButton> </IconButton>
@ -206,19 +208,21 @@ export const MobileControls = ({
<IconButton <IconButton
sx={{ sx={{
fontSize: "24px", fontSize: "24px",
background: 'rgba(0,0,0,0.3)', background: "rgba(0,0,0,0.3)",
borderRadius: '50%', borderRadius: "50%",
padding: '7px' padding: "7px",
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
toggleFullscreen(); toggleFullscreen();
}} }}
> >
<Fullscreen sx={{ <Fullscreen
color: 'white', sx={{
color: "white",
fontSize: "24px", fontSize: "24px",
}} /> }}
/>
</IconButton> </IconButton>
</Box> </Box>
<Box <Box
@ -226,13 +230,15 @@ export const MobileControls = ({
width: "100%", width: "100%",
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
display: 'flex', display: "flex",
flexDirection: 'column' flexDirection: "column",
}}
>
<Box
sx={{
padding: "0px 10px",
}} }}
> >
<Box sx={{
padding: '0px 10px'
}}>
<VideoTime isScreenSmall progress={progress} duration={duration} /> <VideoTime isScreenSmall progress={progress} duration={duration} />
</Box> </Box>
<ProgressSlider <ProgressSlider

View File

@ -42,8 +42,8 @@ import { useGlobal } from "../../context/GlobalProvider";
import { ENTITY_SUBTITLE, SERVICE_SUBTITLE } from "./video-player-constants"; import { ENTITY_SUBTITLE, SERVICE_SUBTITLE } from "./video-player-constants";
import ISO6391, { LanguageCode } from "iso-639-1"; import ISO6391, { LanguageCode } from "iso-639-1";
import LanguageSelect from "./LanguageSelect"; import LanguageSelect from "./LanguageSelect";
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from "@mui/icons-material/Download";
import DownloadingIcon from '@mui/icons-material/Downloading'; import DownloadingIcon from "@mui/icons-material/Downloading";
import { import {
useDropzone, useDropzone,
DropzoneRootProps, DropzoneRootProps,
@ -64,17 +64,16 @@ import { RequestQueueWithPromise } from "../../utils/queue";
export const requestQueueGetStatus = new RequestQueueWithPromise(1); export const requestQueueGetStatus = new RequestQueueWithPromise(1);
export interface SubtitleManagerProps { export interface SubtitleManagerProps {
qortalMetadata: QortalGetMetadata; qortalMetadata: QortalGetMetadata;
close: () => void; close: () => void;
open: boolean; open: boolean;
onSelect: (subtitle: SubtitlePublishedData) => void; onSelect: (subtitle: SubtitlePublishedData) => void;
subtitleBtnRef: any; subtitleBtnRef: any;
currentSubTrack: null | string currentSubTrack: null | string;
setDrawerOpenSubtitles: (val: boolean)=> void setDrawerOpenSubtitles: (val: boolean) => void;
isFromDrawer: boolean isFromDrawer: boolean;
exitFullscreen: ()=> void exitFullscreen: () => void;
} }
export interface Subtitle { export interface Subtitle {
language: string | null; language: string | null;
@ -113,7 +112,7 @@ const SubtitleManagerComponent = ({
currentSubTrack, currentSubTrack,
setDrawerOpenSubtitles, setDrawerOpenSubtitles,
isFromDrawer = false, isFromDrawer = false,
exitFullscreen exitFullscreen,
}: SubtitleManagerProps) => { }: SubtitleManagerProps) => {
const [mode, setMode] = useState(1); const [mode, setMode] = useState(1);
const [isOpenPublish, setIsOpenPublish] = useState(false); const [isOpenPublish, setIsOpenPublish] = useState(false);
@ -146,12 +145,13 @@ const SubtitleManagerComponent = ({
identifier: postIdSearch, identifier: postIdSearch,
name, name,
limit: 0, limit: 0,
includeMetadata: true includeMetadata: true,
}; };
const res = await lists.fetchResourcesResultsOnly( const res = await lists.fetchResourcesResultsOnly(searchParams);
searchParams lists.addList(
`subs-${videoId}`,
res?.filter((item) => !!item?.metadata?.title) || []
); );
lists.addList(`subs-${videoId}`, res?.filter((item)=> !!item?.metadata?.title) || []);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
@ -175,21 +175,26 @@ const SubtitleManagerComponent = ({
getPublishedSubtitles, getPublishedSubtitles,
]); ]);
const ref = useRef<any>(null) const ref = useRef<any>(null);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
ref?.current?.focus() ref?.current?.focus();
} }
}, [open]) }, [open]);
console.log('isFromDrawer', ) console.log("isFromDrawer");
const handleBlur = (e: React.FocusEvent) => { const handleBlur = (e: React.FocusEvent) => {
if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish && !isFromDrawer && open) { if (
console.log('hello close') !e.currentTarget.contains(e.relatedTarget) &&
!isOpenPublish &&
!isFromDrawer &&
open
) {
console.log("hello close");
close(); close();
setIsOpenPublish(false) setIsOpenPublish(false);
} }
}; };
@ -197,7 +202,6 @@ const SubtitleManagerComponent = ({
try { try {
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
const name = auth?.name; const name = auth?.name;
if (!name) return; if (!name) return;
const resources: ResourceToPublish[] = []; const resources: ResourceToPublish[] = [];
@ -233,7 +237,7 @@ const SubtitleManagerComponent = ({
created: Date.now(), created: Date.now(),
metadata: { metadata: {
title: sub.language || undefined, title: sub.language || undefined,
} },
}, },
data: data, data: data,
}); });
@ -260,7 +264,7 @@ const SubtitleManagerComponent = ({
}; };
const theme = useTheme(); const theme = useTheme();
if(!open) return null if (!open) return null;
return ( return (
<> <>
<Box <Box
@ -268,16 +272,14 @@ const SubtitleManagerComponent = ({
tabIndex={-1} tabIndex={-1}
onBlur={handleBlur} onBlur={handleBlur}
bgcolor={alpha("#181818", 0.98)} bgcolor={alpha("#181818", 0.98)}
sx={{
sx={ position: isFromDrawer ? "relative" : "absolute",
{ bottom: isFromDrawer ? "unset" : 60,
position: isFromDrawer ? 'relative' : 'absolute', right: isFromDrawer ? "unset" : 5,
bottom: isFromDrawer ? 'unset' : 60,
right: isFromDrawer ? 'unset' : 5,
color: "white", color: "white",
opacity: 0.9, opacity: 0.9,
borderRadius: 2, borderRadius: 2,
boxShadow: isFromDrawer ? 'unset' : 5, boxShadow: isFromDrawer ? "unset" : 5,
p: 1, p: 1,
minWidth: 225, minWidth: 225,
height: 300, height: 300,
@ -285,8 +287,7 @@ const SubtitleManagerComponent = ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
zIndex: 10, zIndex: 10,
} }}
}
> >
<Box <Box
sx={{ sx={{
@ -318,8 +319,7 @@ const SubtitleManagerComponent = ({
marginLeft: "auto", marginLeft: "auto",
}} }}
onClick={() => { onClick={() => {
setIsOpenPublish(true) setIsOpenPublish(true);
}} }}
> >
<ModeEditIcon <ModeEditIcon
@ -399,8 +399,6 @@ const SubtitleManagerComponent = ({
Load community subs Load community subs
</Button> </Button>
</Box> </Box>
</Box> </Box>
<PublishSubtitles <PublishSubtitles
isOpen={isOpenPublish} isOpen={isOpenPublish}
@ -408,12 +406,10 @@ const SubtitleManagerComponent = ({
publishHandler={publishHandler} publishHandler={publishHandler}
mySubtitles={mySubtitles} mySubtitles={mySubtitles}
/> />
</> </>
); );
}; };
interface PublisherSubtitlesProps { interface PublisherSubtitlesProps {
publisherName: string; publisherName: string;
subtitles: any[]; subtitles: any[];
@ -450,13 +446,13 @@ const PublisherSubtitles = ({
{!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />} {!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase> </ButtonBase>
{subtitles?.map((sub) => { {subtitles?.map((sub, i) => {
return ( return (
<Subtitle <Subtitle
currentSubtrack={currentSubTrack} currentSubtrack={currentSubTrack}
onSelect={onSelect} onSelect={onSelect}
sub={sub} sub={sub}
key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`} key={i}
/> />
); );
})} })}
@ -725,6 +721,7 @@ const PublishSubtitles = ({
{mySubtitles?.map((sub, i) => { {mySubtitles?.map((sub, i) => {
return ( return (
<Card <Card
key={i}
sx={{ sx={{
padding: "10px", padding: "10px",
width: "500px", width: "500px",
@ -758,66 +755,65 @@ interface SubProps {
onSelect: (subtitle: Subtitle) => void; onSelect: (subtitle: Subtitle) => void;
currentSubtrack: null | string; currentSubtrack: null | string;
} }
const subtitlesStatus: Record<string, boolean> = {} const subtitlesStatus: Record<string, boolean> = {};
const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => {
const [selectedToDownload, setSelectedToDownload] = useState<null | QortalGetMetadata>(null) const [isReady, setIsReady] = useState(false);
const [isReady, setIsReady] = useState(false) const { resource, isLoading, error, refetch } = usePublish(
const { resource, isLoading, error, refetch } = usePublish(2, "JSON", sub, true); 2,
"JSON",
sub,
true
);
const isSelected = currentSubtrack === resource?.data?.language; const isSelected = currentSubtrack === resource?.data?.language;
const [isGettingStatus, setIsGettingStatus] = useState(true) const [isGettingStatus, setIsGettingStatus] = useState(true);
// useEffect(()=> {
// if(resource?.data){ const getStatus = useCallback(
// console.log('onselectdone') async (service: Service, name: string, identifier: string) => {
// onSelect(resource?.data)
// }
// }, [isSelected, resource?.data])
const getStatus = useCallback(async (service: Service, name: string, identifier: string)=> {
try { try {
if (subtitlesStatus[`${service}-${name}-${identifier}`]) { if (subtitlesStatus[`${service}-${name}-${identifier}`]) {
setIsReady(true) setIsReady(true);
refetch() refetch();
return return;
} }
const response = await requestQueueGetStatus.enqueue( const response = await requestQueueGetStatus.enqueue(
(): Promise<string> => { (): Promise<string> => {
return qortalRequest({ return qortalRequest({
action: 'GET_QDN_RESOURCE_STATUS', action: "GET_QDN_RESOURCE_STATUS",
identifier, identifier,
service, service,
name, name,
build: false build: false,
}) });
} }
); );
if(response?.status === 'READY'){ if (response?.status === "READY") {
setIsReady(true) setIsReady(true);
subtitlesStatus[`${service}-${name}-${identifier}`] = true subtitlesStatus[`${service}-${name}-${identifier}`] = true;
refetch() refetch();
} }
} catch (error) { } catch (error) {
} finally { } finally {
setIsGettingStatus(false) setIsGettingStatus(false);
} }
}, []) },
[]
);
useEffect(() => { useEffect(() => {
if (sub?.service && sub?.name && sub?.identifier) { if (sub?.service && sub?.name && sub?.identifier) {
getStatus(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 ( return (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
if (resource?.data) { if (resource?.data) {
onSelect(isSelected ? null : resource?.data) onSelect(isSelected ? null : resource?.data);
} else { } else {
refetch() refetch();
} }
}} }}
sx={{ sx={{
@ -830,14 +826,23 @@ const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => {
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
{isGettingStatus && <Skeleton variant="text" sx={{ fontSize: "1.25rem", width: '100%' }} />} {isGettingStatus && (
<Skeleton variant="text" sx={{ fontSize: "1.25rem", width: "100%" }} />
)}
{!isGettingStatus && ( {!isGettingStatus && (
<> <>
<Typography>{sub?.metadata?.title}</Typography> <Typography>{sub?.metadata?.title}</Typography>
{(!isLoading && !error && !resource?.data) ? <DownloadIcon /> : isLoading ? <DownloadingIcon /> : isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />} {!isLoading && !error && !resource?.data ? (
<DownloadIcon />
) : isLoading ? (
<DownloadingIcon />
) : isSelected ? (
<CheckIcon />
) : (
<ArrowForwardIosIcon />
)}
</> </>
)} )}
</ButtonBase> </ButtonBase>
); );
}; };

View File

@ -1,41 +1,51 @@
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from "react";
import { TimelineAction } from './VideoPlayer' import { TimelineAction } from "./VideoPlayer";
import { alpha, Box, ButtonBase, Popover, Typography } from '@mui/material' import { alpha, ButtonBase, Typography } from "@mui/material";
interface TimelineActionsComponentProps { interface TimelineActionsComponentProps {
timelineActions: TimelineAction[] timelineActions: TimelineAction[];
progress: number progress: number;
containerRef: any containerRef: any;
seekTo: (time: number)=> void seekTo: (time: number) => void;
isVideoPlayerSmall: boolean isVideoPlayerSmall: boolean;
} }
const placementStyles: Record<NonNullable<TimelineAction['placement']>, React.CSSProperties> = { const placementStyles: Record<
'TOP-RIGHT': { top: 16, right: 16 }, NonNullable<TimelineAction["placement"]>,
'TOP-LEFT': { top: 16, left: 16 }, React.CSSProperties
'BOTTOM-LEFT': { bottom: 60, left: 16 }, > = {
'BOTTOM-RIGHT': { bottom: 60, right: 16 }, "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) => { export const TimelineActionsComponent = ({
const [isOpen, setIsOpen] = useState(true) timelineActions,
progress,
containerRef,
seekTo,
isVideoPlayerSmall,
}: TimelineActionsComponentProps) => {
const [isOpen, setIsOpen] = useState(true);
const handleClick = useCallback((action: TimelineAction) => { const handleClick = useCallback((action: TimelineAction) => {
if(action?.type === 'SEEK'){ if (action?.type === "SEEK") {
if(!action?.seekToTime) return if (!action?.seekToTime) return;
seekTo(action.seekToTime) seekTo(action.seekToTime);
} else if(action?.type === 'CUSTOM'){ } else if (action?.type === "CUSTOM") {
if (action.onClick) { if (action.onClick) {
action.onClick() action.onClick();
} }
} }
},[]) }, []);
// Find the current matching action(s) // Find the current matching action(s)
const activeActions = useMemo(() => { const activeActions = useMemo(() => {
return timelineActions.filter(action => { return timelineActions.filter((action) => {
return progress >= action.time && progress <= action.time + action.duration; return (
progress >= action.time && progress <= action.time + action.duration
);
}); });
}, [timelineActions, progress]); }, [timelineActions, progress]);
@ -44,32 +54,36 @@ export const TimelineActionsComponent = ({timelineActions, progress, containerRe
if (!hasActive) return null; // Dont render unless active if (!hasActive) return null; // Dont render unless active
return ( return (
<> <>
{activeActions.map((action, index) => { {activeActions?.map((action, index) => {
const placement = (action.placement ?? 'TOP-RIGHT') as keyof typeof placementStyles; const placement = (action.placement ??
"TOP-RIGHT") as keyof typeof placementStyles;
return ( return (
<ButtonBase <ButtonBase
key={index}
sx={{ sx={{
position: 'absolute', position: "absolute",
bgcolor: alpha("#181818", 0.95), bgcolor: alpha("#181818", 0.95),
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
boxShadow: 3, boxShadow: 3,
zIndex: 10, zIndex: 10,
outline: '1px solid white', outline: "1px solid white",
...placementStyles[placement || 'TOP-RIGHT'], ...placementStyles[placement || "TOP-RIGHT"],
}} }}
> >
<Typography
<Typography key={index} sx={{ key={index}
fontSize: isVideoPlayerSmall ? '16px' : '18px' sx={{
}} onClick={()=> handleClick(action)}> fontSize: isVideoPlayerSmall ? "16px" : "18px",
}}
onClick={() => handleClick(action)}
>
{action.label} {action.label}
</Typography> </Typography>
</ButtonBase> </ButtonBase>
) );
})} })}
</> </>
) );
} };

View File

@ -65,7 +65,14 @@ 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 sliderRef = useRef(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [sliderValue, setSliderValue] = useState(0); // local slider value const [sliderValue, setSliderValue] = useState(0); // local slider value
@ -76,25 +83,19 @@ const [sliderValue, setSliderValue] = useState(0); // local slider value
const showTimeFunc = (val: number, clientX: number) => { const showTimeFunc = (val: number, clientX: number) => {
const slider = sliderRef.current; const slider = sliderRef.current;
if (!slider) return; if (!slider) return;
console.log('time',val, duration)
const percent = val / duration; const percent = val / duration;
const time = Math.min(Math.max(0, percent * duration), duration); const time = Math.min(Math.max(0, percent * duration), duration);
setHoverX(clientX); setHoverX(clientX);
setShowDuration(time); setShowDuration(time);
resetHideTimeout() resetHideTimeout();
// Optionally debounce processing thumbnails
// debounceTimeoutRef.current = setTimeout(() => {
// debouncedExtract(time, clientX);
// }, THUMBNAIL_DEBOUNCE);
}; };
const onProgressChange = (e: any, value: number | number[]) => { const onProgressChange = (e: any, value: number | number[]) => {
const clientX = 'touches' in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX; const clientX =
"touches" in e ? e.touches[0].clientX : (e as React.MouseEvent).clientX;
if (clientX && resetHideTimeout) { if (clientX && resetHideTimeout) {
showTimeFunc(value as number, clientX); showTimeFunc(value as number, clientX);
} }
setIsDragging(true); setIsDragging(true);
setSliderValue(value as number); setSliderValue(value as number);
@ -104,11 +105,10 @@ const onChangeCommitted = (e: any, value: number | number[]) => {
setSliderValue(value as number); setSliderValue(value as number);
playerRef.current?.currentTime(value as number); playerRef.current?.currentTime(value as number);
setIsDragging(false); setIsDragging(false);
setLocalProgress(value) setLocalProgress(value);
handleMouseLeave() handleMouseLeave();
}; };
const THUMBNAIL_DEBOUNCE = 500; const THUMBNAIL_DEBOUNCE = 500;
const THUMBNAIL_MIN_DIFF = 10; const THUMBNAIL_MIN_DIFF = 10;
@ -128,10 +128,6 @@ handleMouseLeave()
setShowDuration(time); setShowDuration(time);
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
// debounceTimeoutRef.current = setTimeout(() => {
// debouncedExtract(time, e.clientX);
// }, THUMBNAIL_DEBOUNCE);
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
@ -161,9 +157,7 @@ handleMouseLeave()
} }
const handleClickCapture = (e: React.MouseEvent) => { const handleClickCapture = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
}; };
return ( return (
@ -191,7 +185,6 @@ handleMouseLeave()
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClickCapture={handleClickCapture} onClickCapture={handleClickCapture}
value={isDragging ? sliderValue : progress} // use local state if dragging value={isDragging ? sliderValue : progress} // use local state if dragging
onChange={onProgressChange} onChange={onProgressChange}
onChangeCommitted={onChangeCommitted} onChangeCommitted={onChangeCommitted}
min={0} min={0}
@ -232,8 +225,6 @@ handleMouseLeave()
placement="top" placement="top"
disablePortal disablePortal
modifiers={[{ name: "offset", options: { offset: [-10, 0] } }]} modifiers={[{ name: "offset", options: { offset: [-10, 0] } }]}
> >
<Box <Box
sx={{ sx={{
@ -241,37 +232,15 @@ handleMouseLeave()
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
bgcolor: alpha("#181818", 0.75), bgcolor: alpha("#181818", 0.75),
padding: '5px', padding: "5px",
borderRadius: '5px' borderRadius: "5px",
}} }}
> >
{/* <Box
sx={{
width: 250,
height: 125,
backgroundColor: "black",
border: "1px solid white",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: '7px',
background: '#444444',
padding: '2px'
}}
>
<img
src={thumbnailUrl}
alt="preview"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</Box> */}
<Typography <Typography
sx={{ sx={{
fontSize: "0.8rom", fontSize: "0.8rom",
textShadow: "0 0 5px rgba(0, 0, 0, 0.7)", textShadow: "0 0 5px rgba(0, 0, 0, 0.7)",
fontFamily: "sans-serif" fontFamily: "sans-serif",
}} }}
> >
{formatTime(showDuration)} {formatTime(showDuration)}
@ -359,7 +328,13 @@ const VolumeSlider = ({ width, volume, onVolumeChange }: any) => {
); );
}; };
export const VolumeControl = ({ sliderWidth, onVolumeChange, volume , isMuted, toggleMute}: any) => { export const VolumeControl = ({
sliderWidth,
onVolumeChange,
volume,
isMuted,
toggleMute,
}: any) => {
return ( return (
<Box <Box
sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }} sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }}
@ -381,7 +356,7 @@ export const PlaybackRate = ({
increaseSpeed, increaseSpeed,
isScreenSmall, isScreenSmall,
onSelect, onSelect,
openPlaybackMenu openPlaybackMenu,
}: any) => { }: any) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const btnRef = useRef(null); const btnRef = useRef(null);
@ -409,8 +384,6 @@ export const PlaybackRate = ({
<SlowMotionVideoIcon /> <SlowMotionVideoIcon />
</IconButton> </IconButton>
</CustomFontTooltip> </CustomFontTooltip>
</> </>
); );
}; };
@ -478,45 +451,48 @@ export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
}; };
interface PlayBackMenuProps { interface PlayBackMenuProps {
close: ()=> void close: () => void;
isOpen: boolean isOpen: boolean;
onSelect: (speed: number) => void; onSelect: (speed: number) => void;
playbackRate: number playbackRate: number;
isFromDrawer: boolean isFromDrawer: boolean;
} }
export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawer}: PlayBackMenuProps)=> { export const PlayBackMenu = ({
const theme = useTheme() close,
const ref = useRef<any>(null) onSelect,
isOpen,
playbackRate,
isFromDrawer,
}: PlayBackMenuProps) => {
const theme = useTheme();
const ref = useRef<any>(null);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
ref?.current?.focus() ref?.current?.focus();
} }
}, [isOpen]) }, [isOpen]);
const handleBlur = (e: React.FocusEvent) => { const handleBlur = (e: React.FocusEvent) => {
if (!e.currentTarget.contains(e.relatedTarget) && !isFromDrawer) { if (!e.currentTarget.contains(e.relatedTarget) && !isFromDrawer) {
close(); close();
} }
}; };
if(!isOpen) return null if (!isOpen) return null;
return ( return (
<Box <Box
ref={ref} ref={ref}
tabIndex={-1} tabIndex={-1}
onBlur={handleBlur} onBlur={handleBlur}
bgcolor={alpha("#181818", 0.98)} bgcolor={alpha("#181818", 0.98)}
sx={{
sx={ position: isFromDrawer ? "relative" : "absolute",
{ bottom: isFromDrawer ? "relative" : 60,
position: isFromDrawer ? 'relative' : 'absolute', right: isFromDrawer ? "relative" : 5,
bottom: isFromDrawer ? 'relative' : 60,
right:isFromDrawer ? 'relative' : 5,
color: "white", color: "white",
opacity: 0.9, opacity: 0.9,
borderRadius: 2, borderRadius: 2,
boxShadow: isFromDrawer ? 'relative' : 5, boxShadow: isFromDrawer ? "relative" : 5,
p: 1, p: 1,
minWidth: 225, minWidth: 225,
height: 300, height: 300,
@ -524,8 +500,7 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawe
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
zIndex: 10, zIndex: 10,
} }}
}
> >
<Box <Box
sx={{ sx={{
@ -589,8 +564,8 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawe
disabled={isSelected} disabled={isSelected}
key={speed} key={speed}
onClick={(e) => { onClick={(e) => {
onSelect(speed) onSelect(speed);
close() close();
}} }}
sx={{ sx={{
px: 2, px: 2,
@ -609,5 +584,5 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawe
})} })}
</Box> </Box>
</Box> </Box>
) );
} };

View File

@ -2,8 +2,6 @@ import { Box, IconButton } from "@mui/material";
import { ControlsContainer } from "./VideoPlayer-styles"; import { ControlsContainer } from "./VideoPlayer-styles";
import { import {
FullscreenButton, FullscreenButton,
ObjectFitButton,
PictureInPictureButton,
PlaybackRate, PlaybackRate,
PlayButton, PlayButton,
ProgressSlider, ProgressSlider,
@ -11,42 +9,67 @@ import {
VideoTime, VideoTime,
VolumeControl, VolumeControl,
} from "./VideoControls"; } from "./VideoControls";
import { Ref } from "react"; import SubtitlesIcon from "@mui/icons-material/Subtitles";
import SubtitlesIcon from '@mui/icons-material/Subtitles';
import { CustomFontTooltip } from "./CustomFontTooltip"; import { CustomFontTooltip } from "./CustomFontTooltip";
interface VideoControlsBarProps { interface VideoControlsBarProps {
canPlay: boolean canPlay: boolean;
isScreenSmall: boolean isScreenSmall: boolean;
controlsHeight?: string controlsHeight?: string;
progress: number; progress: number;
duration: number duration: number;
isPlaying: boolean; isPlaying: boolean;
togglePlay: () => void; togglePlay: () => void;
reloadVideo: () => void; reloadVideo: () => void;
volume: number volume: number;
onVolumeChange: (_: any, val: number)=> void onVolumeChange: (_: any, val: number) => void;
toggleFullscreen: ()=> void toggleFullscreen: () => void;
extractFrames: (time: number)=> void
showControls: boolean; showControls: boolean;
showControlsFullScreen: boolean; showControlsFullScreen: boolean;
isFullScreen: boolean; isFullScreen: boolean;
playerRef: any playerRef: any;
increaseSpeed: ()=> void increaseSpeed: () => void;
decreaseSpeed: ()=> void decreaseSpeed: () => void;
playbackRate: number playbackRate: number;
openSubtitleManager: ()=> void openSubtitleManager: () => void;
subtitleBtnRef: any subtitleBtnRef: any;
onSelectPlaybackRate: (rate: number) => void; onSelectPlaybackRate: (rate: number) => void;
isMuted: boolean isMuted: boolean;
toggleMute: ()=> void toggleMute: () => void;
openPlaybackMenu: ()=> void openPlaybackMenu: () => void;
togglePictureInPicture: ()=> void togglePictureInPicture: () => void;
isVideoPlayerSmall: boolean isVideoPlayerSmall: boolean;
setLocalProgress: (val: number)=> void 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 showMobileControls = isScreenSmall && canPlay;
const controlGroupSX = { const controlGroupSX = {
@ -56,13 +79,13 @@ export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, showControls
height: controlsHeight, height: controlsHeight,
}; };
let additionalStyles: React.CSSProperties = {} let additionalStyles: React.CSSProperties = {};
if (isFullScreen && showControlsFullScreen) { if (isFullScreen && showControlsFullScreen) {
additionalStyles = { additionalStyles = {
opacity: 1, opacity: 1,
position: 'fixed', position: "fixed",
bottom: 0 bottom: 0,
} };
} }
return ( return (
@ -70,50 +93,65 @@ export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, showControls
style={{ style={{
padding: "0px", padding: "0px",
opacity: showControls ? 1 : 0, opacity: showControls ? 1 : 0,
pointerEvents: showControls ? 'auto' : 'none', pointerEvents: showControls ? "auto" : "none",
transition: 'opacity 0.4s ease-in-out', transition: "opacity 0.4s ease-in-out",
width: '100%' width: "100%",
// ...additionalStyles
// height: controlsHeight,
}} }}
> >
{showMobileControls ? ( {showMobileControls ? null : canPlay ? (
null <Box
) : canPlay ? ( sx={{
<Box sx={{ display: "flex",
display: 'flex', flexDirection: "column",
flexDirection: 'column', width: "100%",
width: '100%' }}
}}> >
<ProgressSlider
<ProgressSlider setLocalProgress={setLocalProgress} playerRef={playerRef} progress={progress} duration={duration} /> setLocalProgress={setLocalProgress}
playerRef={playerRef}
progress={progress}
duration={duration}
/>
{!isVideoPlayerSmall && ( {!isVideoPlayerSmall && (
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex' width: "100%",
}}> display: "flex",
}}
>
<Box sx={controlGroupSX}> <Box sx={controlGroupSX}>
<PlayButton isPlaying={isPlaying} togglePlay={togglePlay} /> <PlayButton isPlaying={isPlaying} togglePlay={togglePlay} />
<ReloadButton reloadVideo={reloadVideo} /> <ReloadButton reloadVideo={reloadVideo} />
<VolumeControl
onVolumeChange={onVolumeChange}
<VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} isMuted={isMuted} toggleMute={toggleMute} /> volume={volume}
sliderWidth={"100px"}
isMuted={isMuted}
toggleMute={toggleMute}
/>
<VideoTime progress={progress} duration={duration} /> <VideoTime progress={progress} duration={duration} />
</Box> </Box>
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}> <Box sx={{ ...controlGroupSX, marginLeft: "auto" }}>
<PlaybackRate openPlaybackMenu={openPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} /> <PlaybackRate
openPlaybackMenu={openPlaybackMenu}
onSelect={onSelectPlaybackRate}
playbackRate={playbackRate}
increaseSpeed={increaseSpeed}
decreaseSpeed={decreaseSpeed}
/>
{/* <ObjectFitButton /> */} {/* <ObjectFitButton /> */}
<CustomFontTooltip <CustomFontTooltip title="Subtitles" placement="bottom" arrow>
title="Subtitles" <IconButton
placement="bottom" ref={subtitleBtnRef}
arrow onClick={openSubtitleManager}
> >
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}> <SubtitlesIcon
<SubtitlesIcon sx={{ sx={{
color: "white", color: "white",
}} /> }}
/>
</IconButton> </IconButton>
</CustomFontTooltip> </CustomFontTooltip>
{/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */} {/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */}
@ -121,7 +159,6 @@ export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, showControls
</Box> </Box>
</Box> </Box>
)} )}
</Box> </Box>
) : null} ) : null}
</ControlsContainer> </ControlsContainer>

View File

@ -1,9 +1,6 @@
import { import {
ReactEventHandler,
Ref,
RefObject, RefObject,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
@ -13,33 +10,24 @@ import {
import { QortalGetMetadata } from "../../types/interfaces/resources"; import { QortalGetMetadata } from "../../types/interfaces/resources";
import { VideoContainer, VideoElement } from "./VideoPlayer-styles"; import { VideoContainer, VideoElement } from "./VideoPlayer-styles";
import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys"; import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys";
import { useProgressStore, useVideoStore } from "../../state/video"; import {
useIsPlaying,
useProgressStore,
useVideoStore,
} from "../../state/video";
import { useVideoPlayerController } from "./useVideoPlayerController"; import { useVideoPlayerController } from "./useVideoPlayerController";
import { LoadingVideo } from "./LoadingVideo"; import { LoadingVideo } from "./LoadingVideo";
import { VideoControlsBar } from "./VideoControlsBar"; import { VideoControlsBar } from "./VideoControlsBar";
import videojs from "video.js"; import videojs from "video.js";
import "video.js/dist/video-js.css"; import "video.js/dist/video-js.css";
import Player from "video.js/dist/types/player"; import { SubtitleManager, SubtitlePublishedData } from "./SubtitleManager";
import {
Subtitle,
SubtitleManager,
SubtitleManagerProps,
SubtitlePublishedData,
} from "./SubtitleManager";
import { base64ToBlobUrl } from "../../utils/base64"; import { base64ToBlobUrl } from "../../utils/base64";
import convert from "srt-webvtt"; import convert from "srt-webvtt";
import { TimelineActionsComponent } from "./TimelineActionsComponent"; import { TimelineActionsComponent } from "./TimelineActionsComponent";
import { PlayBackMenu } from "./VideoControls"; import { PlayBackMenu } from "./VideoControls";
import { useGlobalPlayerStore } from "../../state/pip"; import { useGlobalPlayerStore } from "../../state/pip";
import { import { alpha, ClickAwayListener, Drawer } from "@mui/material";
alpha,
Box,
ClickAwayListener,
Drawer,
List,
ListItem,
} from "@mui/material";
import { MobileControls } from "./MobileControls"; import { MobileControls } from "./MobileControls";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@ -84,14 +72,17 @@ export type TimelineAction =
onClick: () => void; // ✅ Required for CUSTOM onClick: () => void; // ✅ Required for CUSTOM
placement?: "TOP-RIGHT" | "TOP-LEFT" | "BOTTOM-LEFT" | "BOTTOM-RIGHT"; placement?: "TOP-RIGHT" | "TOP-LEFT" | "BOTTOM-LEFT" | "BOTTOM-RIGHT";
}; };
interface VideoPlayerProps { export interface VideoPlayerProps {
qortalVideoResource: QortalGetMetadata; qortalVideoResource: QortalGetMetadata;
videoRef: Ref<HTMLVideoElement>; videoRef: any;
retryAttempts?: number; retryAttempts?: number;
poster?: string; poster?: string;
autoPlay?: boolean; autoPlay?: boolean;
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void; onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
timelineActions?: TimelineAction[]; timelineActions?: TimelineAction[];
playerRef: any;
locationRef: RefObject<string | null>;
videoLocationRef: RefObject<string | null>;
} }
const videoStyles = { const videoStyles = {
@ -117,19 +108,20 @@ export const isTouchDevice =
export const VideoPlayer = ({ export const VideoPlayer = ({
videoRef, videoRef,
playerRef,
qortalVideoResource, qortalVideoResource,
retryAttempts, retryAttempts,
poster, poster,
autoPlay, autoPlay,
onEnded, onEnded,
timelineActions, timelineActions,
locationRef,
videoLocationRef,
}: VideoPlayerProps) => { }: VideoPlayerProps) => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("contain"); const [videoObjectFit] = useState<StretchVideoType>("contain");
const [isPlaying, setIsPlaying] = useState(false); const { isPlaying, setIsPlaying } = useIsPlaying();
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
console.log("width", width);
useEffect(() => { useEffect(() => {
const observer = new ResizeObserver(([entry]) => { const observer = new ResizeObserver(([entry]) => {
setWidth(entry.contentRect.width); setWidth(entry.contentRect.width);
@ -145,12 +137,11 @@ export const VideoPlayer = ({
playbackRate: state.playbackSettings.playbackRate, playbackRate: state.playbackSettings.playbackRate,
}) })
); );
const playerRef = useRef<Player | null>(null); // const playerRef = useRef<Player | null>(null);
const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false); const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false);
const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false); const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false);
const [showControlsMobile2, setShowControlsMobile] = useState(false); const [showControlsMobile2, setShowControlsMobile] = useState(false);
const [isPlayerInitialized, setIsPlayerInitialized] = useState(false); const [isPlayerInitialized, setIsPlayerInitialized] = useState(false);
const [videoCodec, setVideoCodec] = useState<null | false | string>(null);
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
const { setProgress } = useProgressStore(); const { setProgress } = useProgressStore();
const [localProgress, setLocalProgress] = useState(0); const [localProgress, setLocalProgress] = useState(0);
@ -160,9 +151,8 @@ export const VideoPlayer = ({
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false); const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
const subtitleBtnRef = useRef(null); const subtitleBtnRef = useRef(null);
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null); const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null);
const location = useLocation() const location = useLocation();
const locationRef = useRef<string | null>(null);
const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false); const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false);
const isVideoPlayerSmall = width < 600 || isTouchDevice; const isVideoPlayerSmall = width < 600 || isTouchDevice;
const { const {
@ -176,36 +166,33 @@ export const VideoPlayer = ({
toggleObjectFit, toggleObjectFit,
controlsHeight, controlsHeight,
setProgressRelative, setProgressRelative,
toggleAlwaysShowControls,
changeVolume, changeVolume,
startedFetch,
isReady, isReady,
resourceUrl, resourceUrl,
startPlay, startPlay,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls,
status, status,
percentLoaded, percentLoaded,
showControlsFullScreen, showControlsFullScreen,
onSelectPlaybackRate, onSelectPlaybackRate,
seekTo, seekTo,
togglePictureInPicture, togglePictureInPicture,
downloadResource downloadResource,
} = useVideoPlayerController({ } = useVideoPlayerController({
autoPlay, autoPlay,
playerRef, playerRef,
qortalVideoResource, qortalVideoResource,
retryAttempts, retryAttempts,
isPlayerInitialized,
isMuted, isMuted,
videoRef, videoRef,
}); });
const showControlsMobile = (showControlsMobile2 || !isPlaying) && isVideoPlayerSmall const showControlsMobile =
(showControlsMobile2 || !isPlaying) && isVideoPlayerSmall;
useEffect(() => { useEffect(() => {
if (location) { if (location) {
locationRef.current = location.pathname; locationRef.current = location?.pathname;
} }
}, [location]); }, [location]);
@ -243,10 +230,6 @@ export const VideoPlayer = ({
} }
}, []); }, []);
// const exitFullscreen = useCallback(() => {
// document?.exitFullscreen();
// }, [isFullscreen]);
const exitFullscreen = useCallback(async () => { const exitFullscreen = useCallback(async () => {
try { try {
if (document.fullscreenElement) { if (document.fullscreenElement) {
@ -275,8 +258,8 @@ export const VideoPlayer = ({
}, [isFullscreen]); }, [isFullscreen]);
const toggleFullscreen = useCallback(() => { const toggleFullscreen = useCallback(() => {
setShowControls(false) setShowControls(false);
setShowControlsMobile(false) setShowControlsMobile(false);
isFullscreen ? exitFullscreen() : enterFullscreen(); isFullscreen ? exitFullscreen() : enterFullscreen();
}, [isFullscreen]); }, [isFullscreen]);
@ -286,13 +269,11 @@ export const VideoPlayer = ({
togglePlay, togglePlay,
setProgressRelative, setProgressRelative,
toggleObjectFit, toggleObjectFit,
toggleAlwaysShowControls,
increaseSpeed, increaseSpeed,
decreaseSpeed, decreaseSpeed,
changeVolume, changeVolume,
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls,
toggleFullscreen, toggleFullscreen,
}), }),
[ [
@ -300,13 +281,11 @@ export const VideoPlayer = ({
togglePlay, togglePlay,
setProgressRelative, setProgressRelative,
toggleObjectFit, toggleObjectFit,
toggleAlwaysShowControls,
increaseSpeed, increaseSpeed,
decreaseSpeed, decreaseSpeed,
changeVolume, changeVolume,
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls,
toggleFullscreen, toggleFullscreen,
] ]
); );
@ -318,7 +297,7 @@ export const VideoPlayer = ({
const openSubtitleManager = useCallback(() => { const openSubtitleManager = useCallback(() => {
if (isVideoPlayerSmall) { if (isVideoPlayerSmall) {
setDrawerOpenSubtitles(true); setDrawerOpenSubtitles(true);
return return;
} }
setIsOpenSubtitleManage(true); setIsOpenSubtitleManage(true);
}, [isVideoPlayerSmall]); }, [isVideoPlayerSmall]);
@ -327,7 +306,6 @@ export const VideoPlayer = ({
if (!qortalVideoResource) return null; if (!qortalVideoResource) return null;
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`; return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
}, [qortalVideoResource]); }, [qortalVideoResource]);
const videoLocationRef = useRef<null | string>(null);
useEffect(() => { useEffect(() => {
videoLocationRef.current = videoLocation; videoLocationRef.current = videoLocation;
}, [videoLocation]); }, [videoLocation]);
@ -352,15 +330,6 @@ export const VideoPlayer = ({
} }
} }
}, [videoLocation]); }, [videoLocation]);
// useEffect(() => {
// const ref = videoRef as React.RefObject<HTMLVideoElement>;
// 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(() => { const onPlay = useCallback(() => {
setIsPlaying(true); setIsPlaying(true);
@ -385,7 +354,6 @@ export const VideoPlayer = ({
const videoStylesContainer = useMemo(() => { const videoStylesContainer = useMemo(() => {
return { return {
cursor: "auto", cursor: "auto",
// aspectRatio: "16 / 9",
...videoStyles?.videoContainer, ...videoStyles?.videoContainer,
}; };
}, [showControls, isVideoPlayerSmall]); }, [showControls, isVideoPlayerSmall]);
@ -432,37 +400,6 @@ export const VideoPlayer = ({
}; };
}, [isPlayerInitialized]); }, [isPlayerInitialized]);
const canvasRef = useRef(null);
const videoRefForCanvas = useRef<any>(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<void>((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<Blob | null>((resolve) => {
// canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7);
// });
// if (!blob) return null;
// return URL.createObjectURL(blob);
}, []);
const hideTimeout = useRef<any>(null); const hideTimeout = useRef<any>(null);
const resetHideTimer = () => { const resetHideTimer = () => {
@ -476,7 +413,6 @@ export const VideoPlayer = ({
const handleMouseMove = () => { const handleMouseMove = () => {
if (isVideoPlayerSmall) return; if (isVideoPlayerSmall) return;
console.log('going 222')
resetHideTimer(); resetHideTimer();
}; };
@ -591,24 +527,6 @@ export const VideoPlayer = ({
true 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) => { await new Promise((res) => {
setTimeout(() => { setTimeout(() => {
res(null); res(null);
@ -660,12 +578,10 @@ export const VideoPlayer = ({
return; return;
const resource = JSON.parse(videoLocactionStringified); const resource = JSON.parse(videoLocactionStringified);
let canceled = false;
try { try {
const setupPlayer = async () => { const setupPlayer = async () => {
const type = await getVideoMimeTypeFromUrl(resource); const type = await getVideoMimeTypeFromUrl(resource);
if (canceled) return;
const options = { const options = {
autoplay: true, autoplay: true,
@ -723,7 +639,6 @@ export const VideoPlayer = ({
setCurrentSubTrack(activeTrack.language || activeTrack.srclang); setCurrentSubTrack(activeTrack.language || activeTrack.srclang);
} else { } else {
setCurrentSubTrack(null); setCurrentSubTrack(null);
console.log("No subtitle is currently showing");
} }
}; };
@ -736,7 +651,6 @@ export const VideoPlayer = ({
playerRef.current?.on("error", () => { playerRef.current?.on("error", () => {
const error = playerRef.current?.error(); const error = playerRef.current?.error();
console.error("Video.js playback error:", error); console.error("Video.js playback error:", error);
// Optional: display user-friendly message
}); });
} }
}; };
@ -745,39 +659,6 @@ export const VideoPlayer = ({
} catch (error) { } catch (error) {
console.error("useEffect start player", 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]); }, [isReady, resourceUrl, startPlay, poster, videoLocactionStringified]);
useEffect(() => { useEffect(() => {
@ -815,29 +696,22 @@ export const VideoPlayer = ({
if (!container) return; if (!container) return;
container.addEventListener("touchstart", handleInteraction); container.addEventListener("touchstart", handleInteraction);
// container.addEventListener('mousemove', handleInteraction);
return () => { return () => {
container.removeEventListener("touchstart", handleInteraction); container.removeEventListener("touchstart", handleInteraction);
// container.removeEventListener('mousemove', handleInteraction);
}; };
}, []); }, []);
const handleClickVideoElement = useCallback(() => { const handleClickVideoElement = useCallback(() => {
if (isVideoPlayerSmall) { if (isVideoPlayerSmall) {
resetHideTimeout() resetHideTimeout();
return return;
} }
console.log('sup') togglePlay();
togglePlay() }, [isVideoPlayerSmall, togglePlay]);
}, [isVideoPlayerSmall, togglePlay])
console.log("showControlsMobile", isVideoPlayerSmall);
return ( return (
<> <>
{/* <video controls src={"http://127.0.0.1:22393/arbitrary/VIDEO/a-test/MYTEST2_like_MYTEST2_vid_test-parallel_cSYmIk"} ref={videoRefForCanvas} ></video> */}
<VideoContainer <VideoContainer
tabIndex={0} tabIndex={0}
style={videoStylesContainer} style={videoStylesContainer}
@ -883,7 +757,6 @@ export const VideoPlayer = ({
/> />
)} )}
{isReady && showControls && ( {isReady && showControls && (
<VideoControlsBar <VideoControlsBar
isVideoPlayerSmall={isVideoPlayerSmall} isVideoPlayerSmall={isVideoPlayerSmall}
@ -895,7 +768,6 @@ export const VideoPlayer = ({
isFullScreen={isFullscreen} isFullScreen={isFullscreen}
showControlsFullScreen={showControlsFullScreen} showControlsFullScreen={showControlsFullScreen}
showControls={showControls} showControls={showControls}
extractFrames={extractFrames}
toggleFullscreen={toggleFullscreen} toggleFullscreen={toggleFullscreen}
onVolumeChange={onVolumeChange} onVolumeChange={onVolumeChange}
volume={volume} volume={volume}

View File

@ -0,0 +1,94 @@
import React, { useEffect, useRef } from "react";
import { TimelineAction, VideoPlayer, VideoPlayerProps } from "./VideoPlayer";
import { useGlobalPlayerStore } from "../../state/pip";
import Player from "video.js/dist/types/player";
import { useIsPlaying } from "../../state/video";
import { QortalGetMetadata } from "../../types/interfaces/resources";
export interface VideoPlayerParentProps {
qortalVideoResource: QortalGetMetadata;
videoRef: any;
retryAttempts?: number;
poster?: string;
autoPlay?: boolean;
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
timelineActions?: TimelineAction[];
}
export const VideoPlayerParent = ({
videoRef,
qortalVideoResource,
retryAttempts,
poster,
autoPlay,
onEnded,
timelineActions,
}: VideoPlayerParentProps) => {
const playerRef = useRef<Player | null>(null);
const locationRef = useRef<string | null>(null);
const videoLocationRef = useRef<null | string>(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 (
<VideoPlayer
key={`${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`}
videoRef={videoRef}
qortalVideoResource={qortalVideoResource}
retryAttempts={retryAttempts}
poster={poster}
autoPlay={autoPlay}
onEnded={onEnded}
timelineActions={timelineActions}
playerRef={playerRef}
locationRef={locationRef}
videoLocationRef={videoLocationRef}
/>
);
};

View File

@ -1,14 +1,5 @@
import { import { useState, useEffect, useCallback, useRef } from "react";
useState, import { useVideoStore } from "../../state/video";
useEffect,
RefObject,
useMemo,
useCallback,
Ref,
useRef,
useImperativeHandle,
} from "react";
import { useProgressStore, useVideoStore } from "../../state/video";
import { QortalGetMetadata } from "../../types/interfaces/resources"; import { QortalGetMetadata } from "../../types/interfaces/resources";
import { useResourceStatus } from "../../hooks/useResourceStatus"; import { useResourceStatus } from "../../hooks/useResourceStatus";
import useIdleTimeout from "../../common/useIdleTimeout"; import useIdleTimeout from "../../common/useIdleTimeout";
@ -24,50 +15,43 @@ interface UseVideoControls {
autoPlay?: boolean; autoPlay?: boolean;
qortalVideoResource: QortalGetMetadata; qortalVideoResource: QortalGetMetadata;
retryAttempts?: number; retryAttempts?: number;
isPlayerInitialized: boolean isMuted: boolean;
isMuted: boolean videoRef: any;
videoRef: any
} }
export const useVideoPlayerController = (props: UseVideoControls) => { 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 [isFullscreen, setIsFullscreen] = useState(false);
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false) const [showControlsFullScreen, setShowControlsFullScreen] = useState(false);
const [videoObjectFit, setVideoObjectFit] = useState<"contain" | "fill">( const [videoObjectFit, setVideoObjectFit] = useState<"contain" | "fill">(
"contain" "contain"
); );
const [alwaysShowControls, setAlwaysShowControls] = useState(false);
const [startPlay, setStartPlay] = useState(false); const [startPlay, setStartPlay] = useState(false);
const [startedFetch, setStartedFetch] = useState(false); const [startedFetch, setStartedFetch] = useState(false);
const startedFetchRef = useRef(false); const startedFetchRef = useRef(false);
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore(); const { playbackSettings, setPlaybackRate } = useVideoStore();
const { getProgress } = useProgressStore();
const { isReady, resourceUrl, status, percentLoaded, downloadResource } = useResourceStatus({ const { isReady, resourceUrl, status, percentLoaded, downloadResource } =
useResourceStatus({
resource: !startedFetch ? null : qortalVideoResource, resource: !startedFetch ? null : qortalVideoResource,
retryAttempts, retryAttempts,
}); });
const idleTime = 5000; // Time in milliseconds const idleTime = 5000; // Time in milliseconds
useIdleTimeout({ useIdleTimeout({
onIdle: () => (setShowControlsFullScreen(false)), onIdle: () => setShowControlsFullScreen(false),
onActive: () => (setShowControlsFullScreen(true)), onActive: () => setShowControlsFullScreen(true),
idleTime, idleTime,
}); });
const videoLocation = useMemo(() => {
if (!qortalVideoResource) return null;
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
}, [qortalVideoResource]);
const [playbackRate, _setLocalPlaybackRate] = useState(
playbackSettings.playbackRate
);
const updatePlaybackRate = useCallback( const updatePlaybackRate = useCallback(
(newSpeed: number) => { (newSpeed: number) => {
try { try {
@ -78,17 +62,13 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed); const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed);
player.playbackRate(clampedSpeed); // ✅ Video.js API player.playbackRate(clampedSpeed); // ✅ Video.js API
// _setLocalPlaybackRate(clampedSpeed);
// setPlaybackRate(clampedSpeed);
} catch (error) { } catch (error) {
console.error('updatePlaybackRate', error) console.error("updatePlaybackRate", error);
} }
}, },
[setPlaybackRate, _setLocalPlaybackRate, minSpeed, maxSpeed] [setPlaybackRate, minSpeed, maxSpeed]
); );
const increaseSpeed = useCallback( const increaseSpeed = useCallback(
(wrapOverflow = true) => { (wrapOverflow = true) => {
try { try {
@ -98,7 +78,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
: Math.min(changedSpeed, maxSpeed); : Math.min(changedSpeed, maxSpeed);
updatePlaybackRate(newSpeed); updatePlaybackRate(newSpeed);
} catch (error) { } catch (error) {
console.error('increaseSpeed', increaseSpeed) console.error("increaseSpeed", increaseSpeed);
} }
}, },
[updatePlaybackRate, playbackSettings.playbackRate] [updatePlaybackRate, playbackSettings.playbackRate]
@ -108,10 +88,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
updatePlaybackRate(playbackSettings.playbackRate - speedChange); updatePlaybackRate(playbackSettings.playbackRate - speedChange);
}, [updatePlaybackRate, playbackSettings.playbackRate]); }, [updatePlaybackRate, playbackSettings.playbackRate]);
const toggleAlwaysShowControls = useCallback(() => {
setAlwaysShowControls((prev) => !prev);
}, [setAlwaysShowControls]);
useEffect(() => { useEffect(() => {
const handleFullscreenChange = () => { const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement); setIsFullscreen(!!document.fullscreenElement);
@ -121,40 +97,34 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
document.removeEventListener("fullscreenchange", handleFullscreenChange); document.removeEventListener("fullscreenchange", handleFullscreenChange);
}, []); }, []);
const onVolumeChange = useCallback( const onVolumeChange = useCallback((_: any, value: number | number[]) => {
(_: any, value: number | number[]) => {
try { try {
const newVolume = value as number; const newVolume = value as number;
const ref = playerRef as any; const ref = playerRef as any;
if (!ref.current) return; if (!ref.current) return;
if (ref.current) { if (ref.current) {
playerRef.current?.volume(newVolume); playerRef.current?.volume(newVolume);
} }
} catch (error) { } catch (error) {
console.error('onVolumeChange', error) console.error("onVolumeChange", error);
} }
}, }, []);
[]
);
const toggleMute = useCallback(() => { const toggleMute = useCallback(() => {
try { try {
const ref = playerRef as any; const ref = playerRef as any;
if (!ref.current) return; if (!ref.current) return;
ref.current?.muted(!isMuted);
ref.current?.muted(!isMuted)
} catch (error) { } catch (error) {
console.error('toggleMute', toggleMute) console.error("toggleMute", toggleMute);
} }
}, [isMuted]); }, [isMuted]);
const changeVolume = useCallback( const changeVolume = useCallback((delta: number) => {
(delta: number) => {
try { try {
const player = playerRef.current; 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 (01) const currentVolume = player.volume(); // Get current volume (01)
let newVolume = Math.max(0, Math.min(currentVolume + delta, 1)); let newVolume = Math.max(0, Math.min(currentVolume + delta, 1));
@ -163,18 +133,19 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
player.volume(newVolume); // Set new volume player.volume(newVolume); // Set new volume
player.muted(false); // Ensure it's unmuted player.muted(false); // Ensure it's unmuted
} catch (error) { } catch (error) {
console.error('changeVolume', error) console.error("changeVolume", error);
} }
}, }, []);
[]
);
const setProgressRelative = useCallback((seconds: number) => { const setProgressRelative = useCallback((seconds: number) => {
try { try {
const player = playerRef.current; const player = playerRef.current;
if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return; if (
!player ||
typeof player.currentTime !== "function" ||
typeof player.duration !== "function"
)
return;
const current = player.currentTime(); const current = player.currentTime();
const duration = player.duration() || 100; const duration = player.duration() || 100;
@ -182,15 +153,19 @@ const setProgressRelative = useCallback((seconds: number) => {
player.currentTime(newTime); player.currentTime(newTime);
} catch (error) { } catch (error) {
console.error('setProgressRelative', error) console.error("setProgressRelative", error);
} }
}, []); }, []);
const setProgressAbsolute = useCallback((percent: number) => { const setProgressAbsolute = useCallback((percent: number) => {
try { try {
const player = playerRef.current; const player = playerRef.current;
if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return; if (
!player ||
typeof player.duration !== "function" ||
typeof player.currentTime !== "function"
)
return;
const duration = player.duration(); const duration = player.duration();
const clampedPercent = Math.min(100, Math.max(0, percent)); const clampedPercent = Math.min(100, Math.max(0, percent));
@ -198,29 +173,31 @@ try {
player.currentTime(finalTime); player.currentTime(finalTime);
} catch (error) { } catch (error) {
console.error('setProgressAbsolute', error) console.error("setProgressAbsolute", error);
} }
}, []); }, []);
const seekTo = useCallback((time: number) => { const seekTo = useCallback((time: number) => {
try { try {
const player = playerRef.current; const player = playerRef.current;
if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return; if (
!player ||
typeof player.duration !== "function" ||
typeof player.currentTime !== "function"
)
return;
player.currentTime(time); player.currentTime(time);
} catch (error) { } catch (error) {
console.error('setProgressAbsolute', error) console.error("setProgressAbsolute", error);
} }
}, []); }, []);
const toggleObjectFit = useCallback(() => { const toggleObjectFit = useCallback(() => {
setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain"); setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain");
}, [setVideoObjectFit]); }, [setVideoObjectFit]);
const togglePlay = useCallback(async () => { const togglePlay = useCallback(async () => {
try { try {
if (!startedFetchRef.current) { if (!startedFetchRef.current) {
setStartedFetch(true); setStartedFetch(true);
@ -235,18 +212,17 @@ const togglePlay = useCallback(async () => {
try { try {
await player.play(); await player.play();
} catch (err) { } catch (err) {
console.warn('Play failed:', err); console.warn("Play failed:", err);
} }
} else { } else {
player.pause(); player.pause();
} }
} }
} catch (error) { } catch (error) {
console.error('togglePlay', error) console.error("togglePlay", error);
} }
}, [setStartedFetch, isReady]); }, [setStartedFetch, isReady]);
const reloadVideo = useCallback(async () => { const reloadVideo = useCallback(async () => {
try { try {
const player = playerRef.current; const player = playerRef.current;
@ -254,21 +230,20 @@ const togglePlay = useCallback(async () => {
const currentTime = player.currentTime(); const currentTime = player.currentTime();
player.src({ src: resourceUrl, type: 'video/mp4' }); // Adjust type if needed player.src({ src: resourceUrl, type: "video/mp4" }); // Adjust type if needed
player.load(); player.load();
player.ready(() => { player.ready(() => {
player.currentTime(currentTime); player.currentTime(currentTime);
player.play().catch((err: any) => { player.play().catch((err: any) => {
console.warn('Playback failed after reload:', err); console.warn("Playback failed after reload:", err);
}); });
}); });
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
}, [isReady, resourceUrl]); }, [isReady, resourceUrl]);
useEffect(() => { useEffect(() => {
if (autoPlay) togglePlay(); if (autoPlay) togglePlay();
}, [autoPlay]); }, [autoPlay]);
@ -279,34 +254,25 @@ const togglePlay = useCallback(async () => {
} }
}, [togglePlay, isReady]); }, [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; if (!videoRef.current) return;
const player = playerRef.current; const player = playerRef.current;
if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return; if (
!player ||
typeof player.currentTime !== "function" ||
typeof player.duration !== "function"
)
return;
const current = player.currentTime(); const current = player.currentTime();
useGlobalPlayerStore.getState().setVideoState({ useGlobalPlayerStore.getState().setVideoState({
videoSrc: videoRef.current.src, videoSrc: videoRef.current.src,
currentTime: current, currentTime: current,
isPlaying: true, isPlaying: true,
mode: 'floating', // or 'floating' mode: "floating", // or 'floating'
}); });
}; };
return { return {
reloadVideo, reloadVideo,
togglePlay, togglePlay,
@ -318,14 +284,18 @@ const togglePlay = useCallback(async () => {
toggleObjectFit, toggleObjectFit,
controlsHeight, controlsHeight,
setProgressRelative, setProgressRelative,
toggleAlwaysShowControls,
changeVolume, changeVolume,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls,
startedFetch, startedFetch,
isReady, isReady,
resourceUrl, resourceUrl,
startPlay, startPlay,
status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo, togglePictureInPicture, downloadResource status,
percentLoaded,
showControlsFullScreen,
onSelectPlaybackRate: updatePlaybackRate,
seekTo,
togglePictureInPicture,
downloadResource,
}; };
}; };

View File

@ -3,10 +3,8 @@ import { useEffect, useCallback } from 'react';
interface UseVideoControls { interface UseVideoControls {
reloadVideo: () => void; reloadVideo: () => void;
togglePlay: () => void; togglePlay: () => void;
setAlwaysShowControls: React.Dispatch<React.SetStateAction<boolean>>;
setProgressRelative: (seconds: number) => void; setProgressRelative: (seconds: number) => void;
toggleObjectFit: () => void; toggleObjectFit: () => void;
toggleAlwaysShowControls: () => void;
increaseSpeed: (wrapOverflow?: boolean) => void; increaseSpeed: (wrapOverflow?: boolean) => void;
decreaseSpeed: () => void; decreaseSpeed: () => void;
changeVolume: (delta: number) => void; changeVolume: (delta: number) => void;
@ -21,7 +19,6 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
togglePlay, togglePlay,
setProgressRelative, setProgressRelative,
toggleObjectFit, toggleObjectFit,
toggleAlwaysShowControls,
increaseSpeed, increaseSpeed,
decreaseSpeed, decreaseSpeed,
changeVolume, changeVolume,
@ -51,9 +48,6 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
case "f": case "f":
toggleFullscreen(); toggleFullscreen();
break; break;
case "c":
toggleAlwaysShowControls();
break;
case "+": case "+":
case ">": case ">":
increaseSpeed(false); increaseSpeed(false);
@ -126,7 +120,7 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => {
togglePlay, togglePlay,
setProgressRelative, setProgressRelative,
toggleObjectFit, toggleObjectFit,
toggleAlwaysShowControls,
increaseSpeed, increaseSpeed,
decreaseSpeed, decreaseSpeed,
changeVolume, changeVolume,

View File

@ -58,7 +58,6 @@ export const GlobalProvider = ({
// ✅ Call hooks and pass in options dynamically // ✅ Call hooks and pass in options dynamically
const auth = useAuth(config?.auth || {}); const auth = useAuth(config?.auth || {});
const isPublishing = useMultiplePublishStore((s)=> s.isPublishing); const isPublishing = useMultiplePublishStore((s)=> s.isPublishing);
const videoSrc = useGlobalPlayerStore((s)=> s.videoSrc);
const appInfo = useAppInfo(config.appName, config?.publicSalt); const appInfo = useAppInfo(config.appName, config?.publicSalt);
const lists = useResources(); const lists = useResources();
const identifierOperations = useIdentifiers( const identifierOperations = useIdentifiers(
@ -91,8 +90,11 @@ export const GlobalProvider = ({
return ( return (
<GlobalContext.Provider value={contextValue}> <GlobalContext.Provider value={contextValue}>
<GlobalPipPlayer /> <GlobalPipPlayer />
{isPublishing && ( {isPublishing && (
<MultiPublishDialog /> <MultiPublishDialog />
)} )}

View File

@ -6,7 +6,7 @@ export { useModal } from './hooks/useModal';
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls'; export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls';
export {TimelineAction} from './components/VideoPlayer/VideoPlayer' export {TimelineAction} from './components/VideoPlayer/VideoPlayer'
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys'; 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'; export { useListReturn } from './hooks/useListData';
import './index.css' import './index.css'
export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events'; export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events';

View File

@ -118,3 +118,14 @@ export const useProgressStore = create<ProgressStore>()(
} }
) )
); );
interface IsPlayingState {
isPlaying: boolean;
setIsPlaying: (value: boolean) => void;
}
export const useIsPlaying = create<IsPlayingState>((set) => ({
isPlaying: false,
setIsPlaying: (value) => set({ isPlaying: value }),
}));

View File

@ -4,14 +4,19 @@ export const createAvatarLink = (qortalName: string)=> {
const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); 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) {
if(path && path.startsWith('/')){ link += path.startsWith('/')
link = link + removeTrailingSlash(path) ? removeTrailingSlash(path)
: '/' + removeTrailingSlash(path);
} }
if(path && !path.startsWith('/')){
link = link + '/' + removeTrailingSlash(path) return link;
}
return link
}; };