mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-07-12 12:21:24 +00:00
fixes and update version
This commit is contained in:
parent
205da3ca24
commit
98a51c0b5f
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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; // Don’t render unless active
|
||||
return (
|
||||
<>
|
||||
{activeActions.map((action, index) => {
|
||||
const placement = (action.placement ?? 'TOP-RIGHT') as keyof typeof placementStyles;
|
||||
{activeActions?.map((action, index) => {
|
||||
const placement = (action.placement ??
|
||||
"TOP-RIGHT") as keyof typeof placementStyles;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
94
src/components/VideoPlayer/VideoPlayerParent.tsx
Normal file
94
src/components/VideoPlayer/VideoPlayerParent.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,14 +1,5 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
RefObject,
|
||||
useMemo,
|
||||
useCallback,
|
||||
Ref,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useProgressStore, useVideoStore } from "../../state/video";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useVideoStore } from "../../state/video";
|
||||
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
||||
import { useResourceStatus } from "../../hooks/useResourceStatus";
|
||||
import useIdleTimeout from "../../common/useIdleTimeout";
|
||||
@ -24,81 +15,70 @@ interface UseVideoControls {
|
||||
autoPlay?: boolean;
|
||||
qortalVideoResource: QortalGetMetadata;
|
||||
retryAttempts?: number;
|
||||
isPlayerInitialized: boolean
|
||||
isMuted: boolean
|
||||
videoRef: any
|
||||
isMuted: boolean;
|
||||
videoRef: any;
|
||||
}
|
||||
|
||||
export const useVideoPlayerController = (props: UseVideoControls) => {
|
||||
const { autoPlay, videoRef , playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, isMuted } = props;
|
||||
const {
|
||||
autoPlay,
|
||||
videoRef,
|
||||
playerRef,
|
||||
qortalVideoResource,
|
||||
retryAttempts,
|
||||
isMuted,
|
||||
} = props;
|
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false)
|
||||
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false);
|
||||
const [videoObjectFit, setVideoObjectFit] = useState<"contain" | "fill">(
|
||||
"contain"
|
||||
);
|
||||
const [alwaysShowControls, setAlwaysShowControls] = useState(false);
|
||||
const [startPlay, setStartPlay] = useState(false);
|
||||
const [startedFetch, setStartedFetch] = useState(false);
|
||||
const startedFetchRef = useRef(false);
|
||||
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
|
||||
const { getProgress } = useProgressStore();
|
||||
const { playbackSettings, setPlaybackRate } = useVideoStore();
|
||||
|
||||
const { isReady, resourceUrl, status, percentLoaded, downloadResource } = useResourceStatus({
|
||||
resource: !startedFetch ? null : qortalVideoResource,
|
||||
retryAttempts,
|
||||
});
|
||||
|
||||
const idleTime = 5000; // Time in milliseconds
|
||||
useIdleTimeout({
|
||||
onIdle: () => (setShowControlsFullScreen(false)),
|
||||
onActive: () => (setShowControlsFullScreen(true)),
|
||||
idleTime,
|
||||
const { isReady, resourceUrl, status, percentLoaded, downloadResource } =
|
||||
useResourceStatus({
|
||||
resource: !startedFetch ? null : qortalVideoResource,
|
||||
retryAttempts,
|
||||
});
|
||||
|
||||
|
||||
const videoLocation = useMemo(() => {
|
||||
if (!qortalVideoResource) return null;
|
||||
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
||||
}, [qortalVideoResource]);
|
||||
|
||||
|
||||
|
||||
const [playbackRate, _setLocalPlaybackRate] = useState(
|
||||
playbackSettings.playbackRate
|
||||
);
|
||||
const idleTime = 5000; // Time in milliseconds
|
||||
useIdleTimeout({
|
||||
onIdle: () => setShowControlsFullScreen(false),
|
||||
onActive: () => setShowControlsFullScreen(true),
|
||||
idleTime,
|
||||
});
|
||||
|
||||
const updatePlaybackRate = useCallback(
|
||||
(newSpeed: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
(newSpeed: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
|
||||
|
||||
const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed);
|
||||
player.playbackRate(clampedSpeed); // ✅ Video.js API
|
||||
|
||||
// _setLocalPlaybackRate(clampedSpeed);
|
||||
// setPlaybackRate(clampedSpeed);
|
||||
} catch (error) {
|
||||
console.error('updatePlaybackRate', error)
|
||||
}
|
||||
},
|
||||
[setPlaybackRate, _setLocalPlaybackRate, minSpeed, maxSpeed]
|
||||
);
|
||||
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
|
||||
|
||||
const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed);
|
||||
player.playbackRate(clampedSpeed); // ✅ Video.js API
|
||||
} catch (error) {
|
||||
console.error("updatePlaybackRate", error);
|
||||
}
|
||||
},
|
||||
[setPlaybackRate, minSpeed, maxSpeed]
|
||||
);
|
||||
|
||||
const increaseSpeed = useCallback(
|
||||
(wrapOverflow = true) => {
|
||||
try {
|
||||
const changedSpeed = playbackSettings.playbackRate + speedChange;
|
||||
const newSpeed = wrapOverflow
|
||||
? changedSpeed
|
||||
: Math.min(changedSpeed, maxSpeed);
|
||||
updatePlaybackRate(newSpeed);
|
||||
const newSpeed = wrapOverflow
|
||||
? changedSpeed
|
||||
: Math.min(changedSpeed, maxSpeed);
|
||||
updatePlaybackRate(newSpeed);
|
||||
} catch (error) {
|
||||
console.error('increaseSpeed', increaseSpeed)
|
||||
console.error("increaseSpeed", increaseSpeed);
|
||||
}
|
||||
},
|
||||
[updatePlaybackRate, playbackSettings.playbackRate]
|
||||
@ -108,10 +88,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
||||
updatePlaybackRate(playbackSettings.playbackRate - speedChange);
|
||||
}, [updatePlaybackRate, playbackSettings.playbackRate]);
|
||||
|
||||
const toggleAlwaysShowControls = useCallback(() => {
|
||||
setAlwaysShowControls((prev) => !prev);
|
||||
}, [setAlwaysShowControls]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement);
|
||||
@ -121,153 +97,152 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
}, []);
|
||||
|
||||
const onVolumeChange = useCallback(
|
||||
(_: any, value: number | number[]) => {
|
||||
try {
|
||||
const newVolume = value as number;
|
||||
const onVolumeChange = useCallback((_: any, value: number | number[]) => {
|
||||
try {
|
||||
const newVolume = value as number;
|
||||
const ref = playerRef as any;
|
||||
if (!ref.current) return;
|
||||
if (ref.current) {
|
||||
playerRef.current?.volume(newVolume);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('onVolumeChange', error)
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("onVolumeChange", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
try {
|
||||
const ref = playerRef as any;
|
||||
if (!ref.current) return;
|
||||
|
||||
const ref = playerRef as any;
|
||||
if (!ref.current) return;
|
||||
|
||||
ref.current?.muted(!isMuted)
|
||||
ref.current?.muted(!isMuted);
|
||||
} catch (error) {
|
||||
console.error('toggleMute', toggleMute)
|
||||
console.error("toggleMute", toggleMute);
|
||||
}
|
||||
}, [isMuted]);
|
||||
|
||||
const changeVolume = useCallback(
|
||||
(delta: number) => {
|
||||
const changeVolume = useCallback((delta: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.volume !== 'function') return;
|
||||
if (!player || typeof player.volume !== "function") return;
|
||||
|
||||
const currentVolume = player.volume(); // Get current volume (0–1)
|
||||
let newVolume = Math.max(0, Math.min(currentVolume + delta, 1));
|
||||
newVolume = +newVolume.toFixed(2); // Round to 2 decimal places
|
||||
const currentVolume = player.volume(); // Get current volume (0–1)
|
||||
let newVolume = Math.max(0, Math.min(currentVolume + delta, 1));
|
||||
newVolume = +newVolume.toFixed(2); // Round to 2 decimal places
|
||||
|
||||
player.volume(newVolume); // Set new volume
|
||||
player.muted(false); // Ensure it's unmuted
|
||||
player.volume(newVolume); // Set new volume
|
||||
player.muted(false); // Ensure it's unmuted
|
||||
} catch (error) {
|
||||
console.error('changeVolume', error)
|
||||
console.error("changeVolume", error);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
||||
const setProgressRelative = useCallback((seconds: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (
|
||||
!player ||
|
||||
typeof player.currentTime !== "function" ||
|
||||
typeof player.duration !== "function"
|
||||
)
|
||||
return;
|
||||
|
||||
const setProgressRelative = useCallback((seconds: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return;
|
||||
const current = player.currentTime();
|
||||
const duration = player.duration() || 100;
|
||||
const newTime = Math.max(0, Math.min(current + seconds, duration));
|
||||
|
||||
const current = player.currentTime();
|
||||
const duration = player.duration() || 100;
|
||||
const newTime = Math.max(0, Math.min(current + seconds, duration));
|
||||
player.currentTime(newTime);
|
||||
} catch (error) {
|
||||
console.error("setProgressRelative", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
player.currentTime(newTime);
|
||||
} catch (error) {
|
||||
console.error('setProgressRelative', error)
|
||||
}
|
||||
}, []);
|
||||
const setProgressAbsolute = useCallback((percent: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (
|
||||
!player ||
|
||||
typeof player.duration !== "function" ||
|
||||
typeof player.currentTime !== "function"
|
||||
)
|
||||
return;
|
||||
|
||||
const duration = player.duration();
|
||||
const clampedPercent = Math.min(100, Math.max(0, percent));
|
||||
const finalTime = (duration * clampedPercent) / 100;
|
||||
|
||||
const setProgressAbsolute = useCallback((percent: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return;
|
||||
player.currentTime(finalTime);
|
||||
} catch (error) {
|
||||
console.error("setProgressAbsolute", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const duration = player.duration();
|
||||
const clampedPercent = Math.min(100, Math.max(0, percent));
|
||||
const finalTime = (duration * clampedPercent) / 100;
|
||||
|
||||
player.currentTime(finalTime);
|
||||
} catch (error) {
|
||||
console.error('setProgressAbsolute', error)
|
||||
}
|
||||
}, []);
|
||||
|
||||
const seekTo = useCallback((time: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return;
|
||||
|
||||
player.currentTime(time);
|
||||
} catch (error) {
|
||||
console.error('setProgressAbsolute', error)
|
||||
}
|
||||
}, []);
|
||||
const seekTo = useCallback((time: number) => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (
|
||||
!player ||
|
||||
typeof player.duration !== "function" ||
|
||||
typeof player.currentTime !== "function"
|
||||
)
|
||||
return;
|
||||
|
||||
player.currentTime(time);
|
||||
} catch (error) {
|
||||
console.error("setProgressAbsolute", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleObjectFit = useCallback(() => {
|
||||
setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain");
|
||||
}, [setVideoObjectFit]);
|
||||
|
||||
const togglePlay = useCallback(async () => {
|
||||
|
||||
|
||||
try {
|
||||
if (!startedFetchRef.current) {
|
||||
setStartedFetch(true);
|
||||
startedFetchRef.current = true;
|
||||
setStartPlay(true);
|
||||
return;
|
||||
}
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
if (isReady) {
|
||||
if (player.paused()) {
|
||||
try {
|
||||
await player.play();
|
||||
} catch (err) {
|
||||
console.warn('Play failed:', err);
|
||||
const togglePlay = useCallback(async () => {
|
||||
try {
|
||||
if (!startedFetchRef.current) {
|
||||
setStartedFetch(true);
|
||||
startedFetchRef.current = true;
|
||||
setStartPlay(true);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
player.pause();
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
if (isReady) {
|
||||
if (player.paused()) {
|
||||
try {
|
||||
await player.play();
|
||||
} catch (err) {
|
||||
console.warn("Play failed:", err);
|
||||
}
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("togglePlay", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('togglePlay', error)
|
||||
}
|
||||
}, [setStartedFetch, isReady]);
|
||||
}, [setStartedFetch, isReady]);
|
||||
|
||||
const reloadVideo = useCallback(async () => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || !isReady || !resourceUrl) return;
|
||||
|
||||
const reloadVideo = useCallback(async () => {
|
||||
try {
|
||||
const player = playerRef.current;
|
||||
if (!player || !isReady || !resourceUrl) return;
|
||||
const currentTime = player.currentTime();
|
||||
|
||||
const currentTime = player.currentTime();
|
||||
|
||||
player.src({ src: resourceUrl, type: 'video/mp4' }); // Adjust type if needed
|
||||
player.load();
|
||||
|
||||
player.ready(() => {
|
||||
player.currentTime(currentTime);
|
||||
player.play().catch((err: any) => {
|
||||
console.warn('Playback failed after reload:', err);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [isReady, resourceUrl]);
|
||||
player.src({ src: resourceUrl, type: "video/mp4" }); // Adjust type if needed
|
||||
player.load();
|
||||
|
||||
player.ready(() => {
|
||||
player.currentTime(currentTime);
|
||||
player.play().catch((err: any) => {
|
||||
console.warn("Playback failed after reload:", err);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [isReady, resourceUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoPlay) togglePlay();
|
||||
@ -279,34 +254,25 @@ const togglePlay = useCallback(async () => {
|
||||
}
|
||||
}, [togglePlay, isReady]);
|
||||
|
||||
// videoRef?.current?.addEventListener("enterpictureinpicture", () => {
|
||||
// setPipVideoPath(window.location.pathname);
|
||||
|
||||
// });
|
||||
|
||||
// // when PiP ends (and you're on the wrong page), go back
|
||||
// videoRef?.current?.addEventListener("leavepictureinpicture", () => {
|
||||
// const { pipVideoPath } = usePipStore.getState();
|
||||
// if (pipVideoPath && window.location.pathname !== pipVideoPath) {
|
||||
// navigate(pipVideoPath);
|
||||
// }
|
||||
// });
|
||||
|
||||
const togglePictureInPicture = async () => {
|
||||
const togglePictureInPicture = async () => {
|
||||
if (!videoRef.current) return;
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return;
|
||||
const player = playerRef.current;
|
||||
if (
|
||||
!player ||
|
||||
typeof player.currentTime !== "function" ||
|
||||
typeof player.duration !== "function"
|
||||
)
|
||||
return;
|
||||
|
||||
const current = player.currentTime();
|
||||
useGlobalPlayerStore.getState().setVideoState({
|
||||
videoSrc: videoRef.current.src,
|
||||
currentTime: current,
|
||||
isPlaying: true,
|
||||
mode: 'floating', // or 'floating'
|
||||
});
|
||||
const current = player.currentTime();
|
||||
useGlobalPlayerStore.getState().setVideoState({
|
||||
videoSrc: videoRef.current.src,
|
||||
currentTime: current,
|
||||
isPlaying: true,
|
||||
mode: "floating", // or 'floating'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
reloadVideo,
|
||||
togglePlay,
|
||||
@ -318,14 +284,18 @@ const togglePlay = useCallback(async () => {
|
||||
toggleObjectFit,
|
||||
controlsHeight,
|
||||
setProgressRelative,
|
||||
toggleAlwaysShowControls,
|
||||
changeVolume,
|
||||
setProgressAbsolute,
|
||||
setAlwaysShowControls,
|
||||
startedFetch,
|
||||
isReady,
|
||||
resourceUrl,
|
||||
startPlay,
|
||||
status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo, togglePictureInPicture, downloadResource
|
||||
status,
|
||||
percentLoaded,
|
||||
showControlsFullScreen,
|
||||
onSelectPlaybackRate: updatePlaybackRate,
|
||||
seekTo,
|
||||
togglePictureInPicture,
|
||||
downloadResource,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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 />
|
||||
|
@ -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';
|
||||
|
@ -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 }),
|
||||
}));
|
@ -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;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user