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",
"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",

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 { 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" && (
<Box
position="absolute"
top={0}
@ -36,7 +47,7 @@ export const LoadingVideo = ({
justifyContent="center"
alignItems="center"
zIndex={500}
bgcolor={alpha('#000000', !startPlay ? 0 : 0.95)}
bgcolor={alpha("#000000", !startPlay ? 0 : 0.95)}
sx={{
display: "flex",
flexDirection: "column",
@ -45,77 +56,70 @@ export const LoadingVideo = ({
}}
>
{status !== "NOT_PUBLISHED" && status !== "FAILED_TO_DOWNLOAD" && (
<CircularProgress sx={{
color: 'white'
}} />
<CircularProgress
sx={{
color: "white",
}}
/>
)}
{status && (
<Typography
component="div"
sx={{
color: "white",
fontSize: "15px",
textAlign: "center",
fontFamily: "sans-serif"
fontFamily: "sans-serif",
}}
>
{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)}</>
)}
</Typography>
)}
{status === 'FAILED_TO_DOWNLOAD' && (
<Button variant="outlined" onClick={downloadResource} sx={{
color: 'white'
}}>Try again</Button>
{status === "FAILED_TO_DOWNLOAD" && (
<Button
variant="outlined"
onClick={downloadResource}
sx={{
color: "white",
}}
>
Try again
</Button>
)}
</Box>
)}
{(status === 'INITIAL') && (
{status === "INITIAL" && (
<>
<IconButton
onClick={() => {
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",
}}
>
<PlayArrow

View File

@ -21,8 +21,8 @@ interface MobileControlsProps {
openPlaybackMenu: () => 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 (
<Box
@ -70,33 +70,35 @@ export const MobileControls = ({
openSubtitleManager();
}}
sx={{
background: 'rgba(0,0,0,0.3)',
borderRadius: '50%',
padding: '7px'
background: "rgba(0,0,0,0.3)",
borderRadius: "50%",
padding: "7px",
}}
>
<SubtitlesIcon
sx={{
fontSize: "24px",
color: 'white'
color: "white",
}}
/>
</IconButton>
<IconButton
sx={{
background: 'rgba(0,0,0,0.3)',
borderRadius: '50%',
padding: '7px'
sx={{
background: "rgba(0,0,0,0.3)",
borderRadius: "50%",
padding: "7px",
}}
onClick={(e) => {
e.stopPropagation();
openPlaybackMenu();
}}
>
<SlowMotionVideoIcon sx={{
<SlowMotionVideoIcon
sx={{
fontSize: "24px",
color: 'white'
}}/>
color: "white",
}}
/>
</IconButton>
</Box>
<Box
@ -106,17 +108,17 @@ export const MobileControls = ({
left: "50%",
transform: "translate(-50%, -50%)",
gap: "50px",
display: 'flex',
alignItems: 'center'
display: "flex",
alignItems: "center",
}}
>
<IconButton
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();
@ -126,7 +128,7 @@ export const MobileControls = ({
<Replay10Icon
sx={{
fontSize: "36px",
color: 'white'
color: "white",
}}
/>
</IconButton>
@ -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 = ({
<PauseIcon
sx={{
fontSize: "36px",
color: 'white'
color: "white",
}}
/>
</IconButton>
@ -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 = ({
<PlayArrowIcon
sx={{
fontSize: "36px",
color: 'white'
color: "white",
}}
/>
</IconButton>
@ -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 = ({
<Forward10Icon
sx={{
fontSize: "36px",
color: 'white'
color: "white",
}}
/>
</IconButton>
@ -206,19 +208,21 @@ export const MobileControls = ({
<IconButton
sx={{
fontSize: "24px",
background: 'rgba(0,0,0,0.3)',
borderRadius: '50%',
padding: '7px'
background: "rgba(0,0,0,0.3)",
borderRadius: "50%",
padding: "7px",
}}
onClick={(e) => {
e.stopPropagation();
toggleFullscreen();
}}
>
<Fullscreen sx={{
color: 'white',
fontSize: "24px",
}} />
<Fullscreen
sx={{
color: "white",
fontSize: "24px",
}}
/>
</IconButton>
</Box>
<Box
@ -226,14 +230,16 @@ export const MobileControls = ({
width: "100%",
position: "absolute",
bottom: 0,
display: 'flex',
flexDirection: 'column'
display: "flex",
flexDirection: "column",
}}
>
<Box sx={{
padding: '0px 10px'
}}>
<VideoTime isScreenSmall progress={progress} duration={duration}/>
<Box
sx={{
padding: "0px 10px",
}}
>
<VideoTime isScreenSmall progress={progress} duration={duration} />
</Box>
<ProgressSlider
playerRef={playerRef}

View File

@ -42,8 +42,8 @@ import { useGlobal } from "../../context/GlobalProvider";
import { ENTITY_SUBTITLE, SERVICE_SUBTITLE } from "./video-player-constants";
import ISO6391, { LanguageCode } from "iso-639-1";
import LanguageSelect from "./LanguageSelect";
import DownloadIcon from '@mui/icons-material/Download';
import DownloadingIcon from '@mui/icons-material/Downloading';
import DownloadIcon from "@mui/icons-material/Download";
import DownloadingIcon from "@mui/icons-material/Downloading";
import {
useDropzone,
DropzoneRootProps,
@ -64,17 +64,16 @@ import { RequestQueueWithPromise } from "../../utils/queue";
export const requestQueueGetStatus = new RequestQueueWithPromise(1);
export interface SubtitleManagerProps {
qortalMetadata: QortalGetMetadata;
close: () => 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<any>(null)
const ref = useRef<any>(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 (
<>
<Box
ref={ref}
tabIndex={-1}
onBlur={handleBlur}
bgcolor={alpha("#181818", 0.98)}
sx={
{
position: isFromDrawer ? 'relative' : 'absolute',
bottom: isFromDrawer ? 'unset' : 60,
right: isFromDrawer ? 'unset' : 5,
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: isFromDrawer ? 'unset' : 5,
p: 1,
minWidth: 225,
height: 300,
overflow: "hidden",
display: "flex",
flexDirection: "column",
zIndex: 10,
}
}
ref={ref}
tabIndex={-1}
onBlur={handleBlur}
bgcolor={alpha("#181818", 0.98)}
sx={{
position: isFromDrawer ? "relative" : "absolute",
bottom: isFromDrawer ? "unset" : 60,
right: isFromDrawer ? "unset" : 5,
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: isFromDrawer ? "unset" : 5,
p: 1,
minWidth: 225,
height: 300,
overflow: "hidden",
display: "flex",
flexDirection: "column",
zIndex: 10,
}}
>
<Box
sx={{
@ -318,8 +319,7 @@ const SubtitleManagerComponent = ({
marginLeft: "auto",
}}
onClick={() => {
setIsOpenPublish(true)
setIsOpenPublish(true);
}}
>
<ModeEditIcon
@ -399,8 +399,6 @@ const SubtitleManagerComponent = ({
Load community subs
</Button>
</Box>
</Box>
<PublishSubtitles
isOpen={isOpenPublish}
@ -408,12 +406,10 @@ const SubtitleManagerComponent = ({
publishHandler={publishHandler}
mySubtitles={mySubtitles}
/>
</>
);
};
interface PublisherSubtitlesProps {
publisherName: string;
subtitles: any[];
@ -450,13 +446,13 @@ const PublisherSubtitles = ({
{!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase>
{subtitles?.map((sub) => {
{subtitles?.map((sub, i) => {
return (
<Subtitle
currentSubtrack={currentSubTrack}
onSelect={onSelect}
sub={sub}
key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}
key={i}
/>
);
})}
@ -725,6 +721,7 @@ const PublishSubtitles = ({
{mySubtitles?.map((sub, i) => {
return (
<Card
key={i}
sx={{
padding: "10px",
width: "500px",
@ -758,66 +755,65 @@ interface SubProps {
onSelect: (subtitle: Subtitle) => void;
currentSubtrack: null | string;
}
const subtitlesStatus: Record<string, boolean> = {}
const subtitlesStatus: Record<string, boolean> = {};
const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => {
const [selectedToDownload, setSelectedToDownload] = useState<null | QortalGetMetadata>(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<string> => {
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<string> => {
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 (
<ButtonBase
onClick={() => {
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 && <Skeleton variant="text" sx={{ fontSize: "1.25rem", width: '100%' }} />}
{isGettingStatus && (
<Skeleton variant="text" sx={{ fontSize: "1.25rem", width: "100%" }} />
)}
{!isGettingStatus && (
<>
<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>
);
};

View File

@ -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<NonNullable<TimelineAction['placement']>, 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<TimelineAction["placement"]>,
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; // Dont 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 (
<ButtonBase
sx={{
position: 'absolute',
bgcolor: alpha("#181818", 0.95),
p: 1,
borderRadius: 1,
boxShadow: 3,
zIndex: 10,
outline: '1px solid white',
...placementStyles[placement || 'TOP-RIGHT'],
}}
>
<Typography key={index} sx={{
fontSize: isVideoPlayerSmall ? '16px' : '18px'
}} onClick={()=> handleClick(action)}>
{action.label}
</Typography>
</ButtonBase>
)
} )}
<ButtonBase
key={index}
sx={{
position: "absolute",
bgcolor: alpha("#181818", 0.95),
p: 1,
borderRadius: 1,
boxShadow: 3,
zIndex: 10,
outline: "1px solid white",
...placementStyles[placement || "TOP-RIGHT"],
}}
>
<Typography
key={index}
sx={{
fontSize: isVideoPlayerSmall ? "16px" : "18px",
}}
onClick={() => handleClick(action)}
>
{action.label}
</Typography>
</ButtonBase>
);
})}
</>
)
}
);
};

View File

@ -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<number | null>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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] } }]}
>
<Box
sx={{
@ -241,37 +232,15 @@ handleMouseLeave()
flexDirection: "column",
alignItems: "center",
bgcolor: alpha("#181818", 0.75),
padding: '5px',
borderRadius: '5px'
padding: "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
sx={{
fontSize: "0.8rom",
textShadow: "0 0 5px rgba(0, 0, 0, 0.7)",
fontFamily: "sans-serif"
fontFamily: "sans-serif",
}}
>
{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}
>
<Typography
sx={{
@ -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 (
<Box
sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }}
@ -381,7 +356,7 @@ export const PlaybackRate = ({
increaseSpeed,
isScreenSmall,
onSelect,
openPlaybackMenu
openPlaybackMenu,
}: any) => {
const [isOpen, setIsOpen] = useState(false);
const btnRef = useRef(null);
@ -398,7 +373,7 @@ export const PlaybackRate = ({
arrow
>
<IconButton
ref={btnRef}
ref={btnRef}
sx={{
color: "white",
fontSize: fontSizeSmall,
@ -409,8 +384,6 @@ export const PlaybackRate = ({
<SlowMotionVideoIcon />
</IconButton>
</CustomFontTooltip>
</>
);
};
@ -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<any>(null)
export const PlayBackMenu = ({
close,
onSelect,
isOpen,
playbackRate,
isFromDrawer,
}: PlayBackMenuProps) => {
const theme = useTheme();
const ref = useRef<any>(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 (
<Box
ref={ref}
tabIndex={-1}
onBlur={handleBlur}
bgcolor={alpha("#181818", 0.98)}
sx={
{
position: isFromDrawer ? 'relative' : 'absolute',
bottom: isFromDrawer ? 'relative' : 60,
right:isFromDrawer ? 'relative' : 5,
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: isFromDrawer ? 'relative' : 5,
p: 1,
minWidth: 225,
height: 300,
overflow: "hidden",
display: "flex",
flexDirection: "column",
zIndex: 10,
}
}
<Box
ref={ref}
tabIndex={-1}
onBlur={handleBlur}
bgcolor={alpha("#181818", 0.98)}
sx={{
position: isFromDrawer ? "relative" : "absolute",
bottom: isFromDrawer ? "relative" : 60,
right: isFromDrawer ? "relative" : 5,
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: isFromDrawer ? "relative" : 5,
p: 1,
minWidth: 225,
height: 300,
overflow: "hidden",
display: "flex",
flexDirection: "column",
zIndex: 10,
}}
>
<Box
sx={{
padding: "5px 0px 10px 0px",
display: "flex",
gap: "10px",
width: "100%",
}}
>
<Box
sx={{
padding: "5px 0px 10px 0px",
display: "flex",
gap: "10px",
width: "100%",
}}
>
<ButtonBase onClick={close}>
<ArrowBackIosIcon
sx={{
fontSize: "1.15em",
<ButtonBase onClick={close}>
<ArrowBackIosIcon
sx={{
fontSize: "1.15em",
}}
/>
</ButtonBase>
<ButtonBase>
<Typography
onClick={close}
sx={{
fontSize: "0.85rem",
}}
>
Playback speed
</Typography>
</ButtonBase>
</Box>
<Divider />
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
"::-webkit-scrollbar-track": {
backgroundColor: "transparent",
},
"::-webkit-scrollbar": {
width: "16px",
height: "10px",
},
"::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
transition: "0.3s background-color",
},
"::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.primary.dark,
},
}}
>
{speeds?.map((speed) => {
const isSelected = speed === playbackRate;
return (
<ButtonBase
disabled={isSelected}
key={speed}
onClick={(e) => {
onSelect(speed);
close();
}}
/>
</ButtonBase>
<ButtonBase>
<Typography
onClick={close}
sx={{
fontSize: "0.85rem",
px: 2,
py: 1,
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
width: "100%",
justifyContent: "space-between",
}}
>
Playback speed
</Typography>
</ButtonBase>
</Box>
<Divider />
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
"::-webkit-scrollbar-track": {
backgroundColor: "transparent",
},
"::-webkit-scrollbar": {
width: "16px",
height: "10px",
},
"::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
transition: "0.3s background-color",
},
"::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.primary.dark,
},
}}
>
{speeds?.map((speed) => {
const isSelected = speed === playbackRate;
return (
<ButtonBase
disabled={isSelected}
key={speed}
onClick={(e) => {
onSelect(speed)
close()
}}
sx={{
px: 2,
py: 1,
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
width: "100%",
justifyContent: "space-between",
}}
>
<Typography>{speed}</Typography>
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase>
);
})}
</Box>
<Typography>{speed}</Typography>
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase>
);
})}
</Box>
)
}
</Box>
);
};

View File

@ -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 ? (
<Box sx={{
display: 'flex',
flexDirection: 'column',
width: '100%'
}}>
<ProgressSlider setLocalProgress={setLocalProgress} playerRef={playerRef} progress={progress} duration={duration} />
{!isVideoPlayerSmall && (
<Box sx={{
width: '100%',
display: 'flex'
}}>
<Box sx={controlGroupSX}>
<PlayButton isPlaying={isPlaying} togglePlay={togglePlay}/>
<ReloadButton reloadVideo={reloadVideo} />
{showMobileControls ? null : canPlay ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
>
<ProgressSlider
setLocalProgress={setLocalProgress}
playerRef={playerRef}
progress={progress}
duration={duration}
/>
{!isVideoPlayerSmall && (
<Box
sx={{
width: "100%",
display: "flex",
}}
>
<Box sx={controlGroupSX}>
<PlayButton isPlaying={isPlaying} togglePlay={togglePlay} />
<ReloadButton reloadVideo={reloadVideo} />
<VolumeControl
onVolumeChange={onVolumeChange}
volume={volume}
sliderWidth={"100px"}
isMuted={isMuted}
toggleMute={toggleMute}
/>
<VideoTime progress={progress} duration={duration} />
</Box>
<VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} isMuted={isMuted} toggleMute={toggleMute} />
<VideoTime progress={progress} duration={duration}/>
</Box>
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}>
<PlaybackRate openPlaybackMenu={openPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
{/* <ObjectFitButton /> */}
<CustomFontTooltip
title="Subtitles"
placement="bottom"
arrow
<Box sx={{ ...controlGroupSX, marginLeft: "auto" }}>
<PlaybackRate
openPlaybackMenu={openPlaybackMenu}
onSelect={onSelectPlaybackRate}
playbackRate={playbackRate}
increaseSpeed={increaseSpeed}
decreaseSpeed={decreaseSpeed}
/>
{/* <ObjectFitButton /> */}
<CustomFontTooltip title="Subtitles" placement="bottom" arrow>
<IconButton
ref={subtitleBtnRef}
onClick={openSubtitleManager}
>
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
<SubtitlesIcon sx={{
color: "white",
}} />
</IconButton>
</CustomFontTooltip>
{/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */}
<FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box>
</Box>
)}
<SubtitlesIcon
sx={{
color: "white",
}}
/>
</IconButton>
</CustomFontTooltip>
{/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */}
<FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box>
</Box>
)}
</Box>
) : null}
</ControlsContainer>

View File

@ -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<HTMLVideoElement>;
videoRef: any;
retryAttempts?: number;
poster?: string;
autoPlay?: boolean;
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
timelineActions?: TimelineAction[];
playerRef: any;
locationRef: RefObject<string | null>;
videoLocationRef: RefObject<string | null>;
}
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<HTMLDivElement | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("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<Player | null>(null);
// const playerRef = useRef<Player | null>(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 | false | string>(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 | string>(null);
const location = useLocation()
const location = useLocation();
const locationRef = useRef<string | null>(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 | string>(null);
useEffect(() => {
videoLocationRef.current = videoLocation;
}, [videoLocation]);
@ -352,15 +330,6 @@ export const VideoPlayer = ({
}
}
}, [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(() => {
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<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 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 (
<>
{/* <video controls src={"http://127.0.0.1:22393/arbitrary/VIDEO/a-test/MYTEST2_like_MYTEST2_vid_test-parallel_cSYmIk"} ref={videoRefForCanvas} ></video> */}
<VideoContainer
tabIndex={0}
style={videoStylesContainer}
@ -852,8 +726,8 @@ export const VideoPlayer = ({
status={status}
percentLoaded={percentLoaded}
isLoading={isLoading}
startPlay={startPlay}
downloadResource={downloadResource}
startPlay={startPlay}
downloadResource={downloadResource}
/>
<VideoElement
ref={videoRef}
@ -875,14 +749,13 @@ export const VideoPlayer = ({
/>
{!isVideoPlayerSmall && (
<PlayBackMenu
isFromDrawer={false}
close={closePlaybackMenu}
isOpen={isOpenPlaybackMenu}
onSelect={onSelectPlaybackRate}
playbackRate={playbackRate}
/>
isFromDrawer={false}
close={closePlaybackMenu}
isOpen={isOpenPlaybackMenu}
onSelect={onSelectPlaybackRate}
playbackRate={playbackRate}
/>
)}
{isReady && showControls && (
<VideoControlsBar
@ -895,7 +768,6 @@ export const VideoPlayer = ({
isFullScreen={isFullscreen}
showControlsFullScreen={showControlsFullScreen}
showControls={showControls}
extractFrames={extractFrames}
toggleFullscreen={toggleFullscreen}
onVolumeChange={onVolumeChange}
volume={volume}
@ -956,40 +828,40 @@ export const VideoPlayer = ({
exitFullscreen={exitFullscreen}
/>
)}
<ClickAwayListener onClickAway={() => setDrawerOpenSubtitles(false)}>
<Drawer
variant="persistent"
anchor="bottom"
open={drawerOpenSubtitles && isVideoPlayerSmall}
sx={{}}
slotProps={{
paper: {
sx: {
backgroundColor: alpha("#181818", 0.98),
borderRadius: 2,
width: "90%",
margin: "0 auto",
p: 1,
backgroundImage: "none",
mb: 1,
position: "absolute",
<ClickAwayListener onClickAway={() => setDrawerOpenSubtitles(false)}>
<Drawer
variant="persistent"
anchor="bottom"
open={drawerOpenSubtitles && isVideoPlayerSmall}
sx={{}}
slotProps={{
paper: {
sx: {
backgroundColor: alpha("#181818", 0.98),
borderRadius: 2,
width: "90%",
margin: "0 auto",
p: 1,
backgroundImage: "none",
mb: 1,
position: "absolute",
},
},
},
}}
>
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={true}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
setDrawerOpenSubtitles={setDrawerOpenSubtitles}
isFromDrawer={true}
exitFullscreen={exitFullscreen}
/>
</Drawer>
</ClickAwayListener>
}}
>
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={true}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
setDrawerOpenSubtitles={setDrawerOpenSubtitles}
isFromDrawer={true}
exitFullscreen={exitFullscreen}
/>
</Drawer>
</ClickAwayListener>
<ClickAwayListener onClickAway={() => setDrawerOpenPlayback(false)}>
<Drawer
variant="persistent"

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 {
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 (01)
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 (01)
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,
};
};

View File

@ -3,10 +3,8 @@ import { useEffect, useCallback } from 'react';
interface UseVideoControls {
reloadVideo: () => void;
togglePlay: () => void;
setAlwaysShowControls: React.Dispatch<React.SetStateAction<boolean>>;
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,

View File

@ -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 (
<GlobalContext.Provider value={contextValue}>
<GlobalPipPlayer />
<GlobalPipPlayer />
{isPublishing && (
<MultiPublishDialog />

View File

@ -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';

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(/\/$/, '');
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
};
if (path) {
link += path.startsWith('/')
? removeTrailingSlash(path)
: '/' + removeTrailingSlash(path);
}
return link;
};