mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-15 01:41:21 +00:00
started on controls
This commit is contained in:
parent
58d56e25dd
commit
1ba3bfca26
22
src/components/VideoPlayer/CustomFontTooltip.tsx
Normal file
22
src/components/VideoPlayer/CustomFontTooltip.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Box, Tooltip, TooltipProps } from "@mui/material";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export interface CustomFontTooltipProps extends TooltipProps {
|
||||||
|
fontSize?: string;
|
||||||
|
}
|
||||||
|
export const CustomFontTooltip = ({
|
||||||
|
fontSize,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<CustomFontTooltipProps>) => {
|
||||||
|
if (!fontSize) fontSize = "160%";
|
||||||
|
const text = <Box sx={{ fontSize: fontSize }}>{title}</Box>;
|
||||||
|
|
||||||
|
// put controls into individual components
|
||||||
|
return (
|
||||||
|
<Tooltip title={text} {...props} sx={{ display: "contents", ...props.sx }}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
116
src/components/VideoPlayer/LoadingVideo.tsx
Normal file
116
src/components/VideoPlayer/LoadingVideo.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { Box, CircularProgress, 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
|
||||||
|
}
|
||||||
|
export const LoadingVideo = ({
|
||||||
|
status, percentLoaded, isReady, isLoading, togglePlay
|
||||||
|
}: LoadingVideoProps) => {
|
||||||
|
|
||||||
|
const getDownloadProgress = (percentLoaded: number) => {
|
||||||
|
const progress = percentLoaded;
|
||||||
|
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && status !== 'INITIAL' && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={status === "READY" ? "55px " : 0}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
zIndex={25}
|
||||||
|
bgcolor="rgba(0, 0, 0, 0.6)"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "10px",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress color="secondary" />
|
||||||
|
{status && (
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
fontSize: "15px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status === "NOT_PUBLISHED" && (
|
||||||
|
<>Video file was not published. Please inform the publisher!</>
|
||||||
|
)}
|
||||||
|
{status === "REFETCHING" ? (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
{getDownloadProgress(
|
||||||
|
percentLoaded
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
<> Refetching in 25 seconds</>
|
||||||
|
</>
|
||||||
|
) : status === "DOWNLOADED" ? (
|
||||||
|
<>Download Completed: building video...</>
|
||||||
|
) : status !== "READY" ? (
|
||||||
|
<>
|
||||||
|
{getDownloadProgress(
|
||||||
|
percentLoaded
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Fetching video...</>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(status === 'INITIAL') && (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
zIndex={500}
|
||||||
|
bgcolor="rgba(0, 0, 0, 0.6)"
|
||||||
|
onClick={() => {
|
||||||
|
togglePlay();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlayArrow
|
||||||
|
sx={{
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
69
src/components/VideoPlayer/MobileControlsBar.tsx
Normal file
69
src/components/VideoPlayer/MobileControlsBar.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { MoreVert as MoreIcon } from "@mui/icons-material";
|
||||||
|
import { Box, IconButton, Menu, MenuItem } from "@mui/material";
|
||||||
|
import {
|
||||||
|
FullscreenButton,
|
||||||
|
ObjectFitButton,
|
||||||
|
PictureInPictureButton,
|
||||||
|
PlaybackRate,
|
||||||
|
PlayButton,
|
||||||
|
ProgressSlider,
|
||||||
|
ReloadButton,
|
||||||
|
VideoTime,
|
||||||
|
VolumeControl,
|
||||||
|
} from "./VideoControls";
|
||||||
|
|
||||||
|
export const MobileControlsBar = ({handleMenuOpen, handleMenuClose, anchorEl, controlsHeight}) => {
|
||||||
|
|
||||||
|
|
||||||
|
const controlGroupSX = {
|
||||||
|
display: "flex",
|
||||||
|
gap: "5px",
|
||||||
|
alignItems: "center",
|
||||||
|
height: controlsHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={controlGroupSX}>
|
||||||
|
<PlayButton />
|
||||||
|
<ReloadButton />
|
||||||
|
<ProgressSlider />
|
||||||
|
<VideoTime />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={controlGroupSX}>
|
||||||
|
<PlaybackRate />
|
||||||
|
<FullscreenButton />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
onClick={handleMenuOpen}
|
||||||
|
sx={{ paddingLeft: "0px", marginRight: "0px" }}
|
||||||
|
>
|
||||||
|
<MoreIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Menu
|
||||||
|
id="simple-menu"
|
||||||
|
anchorEl={anchorEl.value}
|
||||||
|
keepMounted
|
||||||
|
open={Boolean(anchorEl.value)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
PaperProps={{
|
||||||
|
style: {
|
||||||
|
width: "250px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
|
<ObjectFitButton />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<PictureInPictureButton />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
249
src/components/VideoPlayer/VideoControls.tsx
Normal file
249
src/components/VideoPlayer/VideoControls.tsx
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { Box, IconButton, Slider, Typography } from "@mui/material";
|
||||||
|
export const fontSizeExSmall = "60%";
|
||||||
|
export const fontSizeSmall = "80%";
|
||||||
|
import AspectRatioIcon from "@mui/icons-material/AspectRatio";
|
||||||
|
import {
|
||||||
|
Fullscreen,
|
||||||
|
Pause,
|
||||||
|
PictureInPicture,
|
||||||
|
PlayArrow,
|
||||||
|
Refresh,
|
||||||
|
VolumeOff,
|
||||||
|
VolumeUp,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { formatTime } from "../../utils/time.js";
|
||||||
|
import { CustomFontTooltip } from "./CustomFontTooltip.js";
|
||||||
|
|
||||||
|
const buttonPaddingBig = "6px";
|
||||||
|
const buttonPaddingSmall = "4px";
|
||||||
|
|
||||||
|
export const PlayButton = ({togglePlay, isPlaying , isScreenSmall}: any) => {
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
|
}}
|
||||||
|
onClick={() => togglePlay()}
|
||||||
|
>
|
||||||
|
{isPlaying ? <Pause /> : <PlayArrow />}
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReloadButton = ({reloadVideo, isScreenSmall}: any) => {
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
|
}}
|
||||||
|
onClick={reloadVideo}
|
||||||
|
>
|
||||||
|
<Refresh />
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProgressSlider = ({progress, duration, videoRef}: any) => {
|
||||||
|
const onProgressChange = async (_: any, value: number | number[]) => {
|
||||||
|
if (!videoRef.current) return;
|
||||||
|
videoRef.current.currentTime = value as number;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
value={progress}
|
||||||
|
onChange={onProgressChange}
|
||||||
|
min={0}
|
||||||
|
max={duration || 100}
|
||||||
|
step={0.1}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "42px",
|
||||||
|
color: "#00abff",
|
||||||
|
padding: "0px",
|
||||||
|
// prevents the slider from jumping up 20px in certain mobile conditions
|
||||||
|
"@media (pointer: coarse)": { padding: "0px" },
|
||||||
|
|
||||||
|
"& .MuiSlider-thumb": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-thumb::after": { width: "20px", height: "20px" },
|
||||||
|
"& .MuiSlider-rail": { opacity: 0.5, height: "6px" },
|
||||||
|
"& .MuiSlider-track": { height: "6px", border: "0px" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VideoTime = ({videoRef, progress, isScreenSmall}: any) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip
|
||||||
|
title="Seek video in 10% increments (0-9)"
|
||||||
|
placement="bottom"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall,
|
||||||
|
color: "white",
|
||||||
|
visibility: !videoRef.current?.duration ? "hidden" : "visible",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{videoRef.current?.duration ? formatTime(progress) : ""}
|
||||||
|
{" / "}
|
||||||
|
{videoRef.current?.duration
|
||||||
|
? formatTime(videoRef.current?.duration)
|
||||||
|
: ""}
|
||||||
|
</Typography>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VolumeButton = ({isMuted, toggleMute}: any) => {
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip
|
||||||
|
title="Toggle Mute (M), Raise (UP), Lower (DOWN)"
|
||||||
|
placement="bottom"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
onClick={toggleMute}
|
||||||
|
>
|
||||||
|
{isMuted ? <VolumeOff /> : <VolumeUp />}
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VolumeSlider = ({ width, volume, onVolumeChange }: any) => {
|
||||||
|
let color = "";
|
||||||
|
if (volume <= 0.5) color = "green";
|
||||||
|
else if (volume <= 0.75) color = "yellow";
|
||||||
|
else color = "red";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
value={volume}
|
||||||
|
onChange={onVolumeChange}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
sx={{
|
||||||
|
width,
|
||||||
|
marginRight: "10px",
|
||||||
|
color,
|
||||||
|
"& .MuiSlider-thumb": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-thumb::after": { width: "16px", height: "16px" },
|
||||||
|
"& .MuiSlider-rail": { opacity: 0.5, height: "6px" },
|
||||||
|
"& .MuiSlider-track": { height: "6px", border: "0px" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VolumeControl = ({ sliderWidth, onVolumeChange, volume }: any) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }}
|
||||||
|
>
|
||||||
|
<VolumeButton />
|
||||||
|
<VolumeSlider width={sliderWidth} onVolumeChange={onVolumeChange} volume={volume} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlaybackRate = ({playbackRate, increaseSpeed, isScreenSmall}: any) => {
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip
|
||||||
|
title="Video Speed. Increase (+ or >), Decrease (- or <)"
|
||||||
|
placement="bottom"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
fontSize: fontSizeSmall,
|
||||||
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
|
}}
|
||||||
|
onClick={() => increaseSpeed()}
|
||||||
|
>
|
||||||
|
{playbackRate}x
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectFitButton = ({toggleObjectFit, isScreenSmall}: any) => {
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip title="Toggle Aspect Ratio (O)" placement="bottom" arrow>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
|
}}
|
||||||
|
onClick={() => toggleObjectFit()}
|
||||||
|
>
|
||||||
|
<AspectRatioIcon />
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PictureInPictureButton = ({isFullscreen, toggleRef, togglePictureInPicture, isScreenSmall}: any) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isFullscreen && (
|
||||||
|
<CustomFontTooltip
|
||||||
|
title="Picture in Picture (P)"
|
||||||
|
placement="bottom"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
|
}}
|
||||||
|
ref={toggleRef}
|
||||||
|
onClick={togglePictureInPicture}
|
||||||
|
>
|
||||||
|
<PictureInPicture />
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullscreenButton = ({toggleFullscreen, isScreenSmall}: any) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow>
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
|
}}
|
||||||
|
onClick={() => toggleFullscreen()}
|
||||||
|
>
|
||||||
|
<Fullscreen />
|
||||||
|
</IconButton>
|
||||||
|
</CustomFontTooltip>
|
||||||
|
);
|
||||||
|
};
|
74
src/components/VideoPlayer/VideoControlsBar.tsx
Normal file
74
src/components/VideoPlayer/VideoControlsBar.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { ControlsContainer } from "./VideoPlayer-styles";
|
||||||
|
// import { MobileControlsBar } from "./MobileControlsBar";
|
||||||
|
import {
|
||||||
|
FullscreenButton,
|
||||||
|
ObjectFitButton,
|
||||||
|
PictureInPictureButton,
|
||||||
|
PlaybackRate,
|
||||||
|
PlayButton,
|
||||||
|
ProgressSlider,
|
||||||
|
ReloadButton,
|
||||||
|
VideoTime,
|
||||||
|
VolumeControl,
|
||||||
|
} from "./VideoControls";
|
||||||
|
import { Ref } from "react";
|
||||||
|
|
||||||
|
interface VideoControlsBarProps {
|
||||||
|
canPlay: boolean
|
||||||
|
isScreenSmall: boolean
|
||||||
|
controlsHeight?: string
|
||||||
|
videoRef:Ref<HTMLVideoElement>;
|
||||||
|
progress: number;
|
||||||
|
duration: number
|
||||||
|
isPlaying: boolean;
|
||||||
|
togglePlay: ()=> void;
|
||||||
|
reloadVideo: ()=> void;
|
||||||
|
volume: number
|
||||||
|
onVolumeChange: (_: any, val: number)=> void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoControlsBar = ({reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, videoRef, duration, progress, togglePlay}: VideoControlsBarProps) => {
|
||||||
|
|
||||||
|
const showMobileControls = isScreenSmall && canPlay;
|
||||||
|
|
||||||
|
const controlGroupSX = {
|
||||||
|
display: "flex",
|
||||||
|
gap: "5px",
|
||||||
|
alignItems: "center",
|
||||||
|
height: controlsHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlsContainer
|
||||||
|
style={{
|
||||||
|
padding: "0px",
|
||||||
|
height: controlsHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showMobileControls ? (
|
||||||
|
null
|
||||||
|
// <MobileControlsBar />
|
||||||
|
) : canPlay ? (
|
||||||
|
<>
|
||||||
|
<Box sx={controlGroupSX}>
|
||||||
|
<PlayButton isPlaying={isPlaying} togglePlay={togglePlay}/>
|
||||||
|
<ReloadButton reloadVideo={reloadVideo} />
|
||||||
|
|
||||||
|
<ProgressSlider videoRef={videoRef} progress={progress} duration={duration} />
|
||||||
|
|
||||||
|
<VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} />
|
||||||
|
<VideoTime videoRef={videoRef} progress={progress}/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={controlGroupSX}>
|
||||||
|
<PlaybackRate />
|
||||||
|
<ObjectFitButton />
|
||||||
|
<PictureInPictureButton />
|
||||||
|
<FullscreenButton />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</ControlsContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -1,16 +1,11 @@
|
|||||||
import {
|
import { Ref, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
Ref,
|
|
||||||
RefObject,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
||||||
import { VideoContainer, VideoElement } from "./VideoPlayer-styles";
|
import { VideoContainer, VideoElement } from "./VideoPlayer-styles";
|
||||||
import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys";
|
import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys";
|
||||||
import { useProgressStore, useVideoStore } from "../../state/video";
|
import { useProgressStore, useVideoStore } from "../../state/video";
|
||||||
import { useVideoPlayerController } from "./useVideoPlayerController";
|
import { useVideoPlayerController } from "./useVideoPlayerController";
|
||||||
|
import { LoadingVideo } from "./LoadingVideo";
|
||||||
|
import { VideoControlsBar } from "./VideoControlsBar";
|
||||||
|
|
||||||
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
||||||
|
|
||||||
@ -21,7 +16,7 @@ export interface VideoPlayerProps {
|
|||||||
showControls?: boolean;
|
showControls?: boolean;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
||||||
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>)=> void
|
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoStyles = {
|
const videoStyles = {
|
||||||
@ -35,16 +30,22 @@ export const VideoPlayer = ({
|
|||||||
showControls,
|
showControls,
|
||||||
poster,
|
poster,
|
||||||
autoPlay,
|
autoPlay,
|
||||||
onEnded
|
onEnded,
|
||||||
}: VideoPlayerProps) => {
|
}: VideoPlayerProps) => {
|
||||||
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
|
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
|
||||||
const [videoObjectFit] = useState<StretchVideoType>("contain");
|
const [videoObjectFit] = useState<StretchVideoType>("contain");
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [volume, setVolume] = useState(0);
|
const { volume, setVolume } = useVideoStore((state) => ({
|
||||||
|
volume: state.playbackSettings.volume,
|
||||||
|
setVolume: state.setVolume,
|
||||||
|
}));
|
||||||
|
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
const { setProgress } = useProgressStore();
|
const { setProgress } = useProgressStore();
|
||||||
|
const [localProgress, setLocalProgress] = useState(0)
|
||||||
|
const [duration, setDuration] = useState(0)
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reloadVideo,
|
reloadVideo,
|
||||||
@ -67,7 +68,13 @@ export const VideoPlayer = ({
|
|||||||
startPlay,
|
startPlay,
|
||||||
setProgressAbsolute,
|
setProgressAbsolute,
|
||||||
setAlwaysShowControls,
|
setAlwaysShowControls,
|
||||||
} = useVideoPlayerController({ autoPlay, videoRef, qortalVideoResource, retryAttempts });
|
status, percentLoaded
|
||||||
|
} = useVideoPlayerController({
|
||||||
|
autoPlay,
|
||||||
|
videoRef,
|
||||||
|
qortalVideoResource,
|
||||||
|
retryAttempts
|
||||||
|
});
|
||||||
|
|
||||||
const hotkeyHandlers = useMemo(
|
const hotkeyHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -109,8 +116,18 @@ export const VideoPlayer = ({
|
|||||||
if (!ref.current || !videoLocation) return;
|
if (!ref.current || !videoLocation) return;
|
||||||
if (typeof ref.current.currentTime === "number") {
|
if (typeof ref.current.currentTime === "number") {
|
||||||
setProgress(videoLocation, ref.current.currentTime);
|
setProgress(videoLocation, ref.current.currentTime);
|
||||||
|
setLocalProgress(ref.current.currentTime)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
||||||
|
if (!ref.current) return;
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.volume = volume;
|
||||||
|
}
|
||||||
|
// Only run on mount
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onPlay = useCallback(() => {
|
const onPlay = useCallback(() => {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
@ -152,11 +169,37 @@ export const VideoPlayer = ({
|
|||||||
};
|
};
|
||||||
}, [videoObjectFit, showControls, isFullscreen]);
|
}, [videoObjectFit, showControls, isFullscreen]);
|
||||||
|
|
||||||
const handleEnded = useCallback((e: React.SyntheticEvent<HTMLVideoElement, Event>)=> {
|
const handleEnded = useCallback(
|
||||||
if(onEnded){
|
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
||||||
onEnded(e)
|
if (onEnded) {
|
||||||
}
|
onEnded(e);
|
||||||
}, [onEnded])
|
}
|
||||||
|
},
|
||||||
|
[onEnded]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCanPlay = useCallback(()=> {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [setIsLoading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
||||||
|
if (!ref.current) return;
|
||||||
|
const video = ref.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
if(video?.duration){
|
||||||
|
setDuration(video.duration)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoContainer
|
<VideoContainer
|
||||||
@ -166,7 +209,7 @@ export const VideoPlayer = ({
|
|||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{/* <LoadingVideo /> */}
|
<LoadingVideo togglePlay={togglePlay} isReady={isReady} status={status} percentLoaded={percentLoaded} isLoading={isLoading} />
|
||||||
<VideoElement
|
<VideoElement
|
||||||
id={qortalVideoResource?.identifier}
|
id={qortalVideoResource?.identifier}
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
@ -175,18 +218,16 @@ export const VideoPlayer = ({
|
|||||||
poster={startPlay ? "" : poster}
|
poster={startPlay ? "" : poster}
|
||||||
onTimeUpdate={updateProgress}
|
onTimeUpdate={updateProgress}
|
||||||
autoPlay={autoPlay}
|
autoPlay={autoPlay}
|
||||||
onClick={() => togglePlay()}
|
onClick={togglePlay}
|
||||||
onEnded={handleEnded}
|
onEnded={handleEnded}
|
||||||
onCanPlay={() => {
|
onCanPlay={handleCanPlay}
|
||||||
setIsLoading(false);
|
|
||||||
}}
|
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
style={videoStylesVideo}
|
style={videoStylesVideo}
|
||||||
onPlay={onPlay}
|
onPlay={onPlay}
|
||||||
onPause={onPause}
|
onPause={onPause}
|
||||||
onVolumeChange={onVolumeChangeHandler}
|
onVolumeChange={onVolumeChangeHandler}
|
||||||
/>
|
/>
|
||||||
{/* {showControls && <VideoControlsBar />} */}
|
<VideoControlsBar onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} videoRef={videoRef} duration={duration} progress={localProgress} />
|
||||||
</VideoContainer>
|
</VideoContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
|||||||
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
|
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
|
||||||
const { getProgress } = useProgressStore();
|
const { getProgress } = useProgressStore();
|
||||||
|
|
||||||
const { isReady, resourceUrl } = useResourceStatus({
|
const { isReady, resourceUrl, status, percentLoaded } = useResourceStatus({
|
||||||
resource: !startedFetch ? null : qortalVideoResource,
|
resource: !startedFetch ? null : qortalVideoResource,
|
||||||
retryAttempts,
|
retryAttempts,
|
||||||
});
|
});
|
||||||
@ -194,7 +194,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
|||||||
ref.current.load();
|
ref.current.load();
|
||||||
ref.current.currentTime = currentTime;
|
ref.current.currentTime = currentTime;
|
||||||
ref.current.play();
|
ref.current.play();
|
||||||
}, []);
|
}, [isReady, resourceUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoPlay) togglePlay();
|
if (autoPlay) togglePlay();
|
||||||
@ -227,5 +227,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
|||||||
isReady,
|
isReady,
|
||||||
resourceUrl,
|
resourceUrl,
|
||||||
startPlay,
|
startPlay,
|
||||||
|
status, percentLoaded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -200,7 +200,7 @@ export const useResourceStatus = ({
|
|||||||
const resourceUrl = resource ? `/arbitrary/${resource.service}/${resource.name}/${resource.identifier}` : null;
|
const resourceUrl = resource ? `/arbitrary/${resource.service}/${resource.name}/${resource.identifier}` : null;
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
status: status?.status || "SEARCHING",
|
status: status?.status || "INITIAL",
|
||||||
localChunkCount: status?.localChunkCount || 0,
|
localChunkCount: status?.localChunkCount || 0,
|
||||||
totalChunkCount: status?.totalChunkCount || 0,
|
totalChunkCount: status?.totalChunkCount || 0,
|
||||||
percentLoaded: status?.percentLoaded || 0,
|
percentLoaded: status?.percentLoaded || 0,
|
||||||
|
@ -21,6 +21,7 @@ export type Status =
|
|||||||
| 'FAILED_TO_DOWNLOAD'
|
| 'FAILED_TO_DOWNLOAD'
|
||||||
| 'REFETCHING'
|
| 'REFETCHING'
|
||||||
| 'SEARCHING'
|
| 'SEARCHING'
|
||||||
|
| 'INITIAL'
|
||||||
|
|
||||||
export interface ResourceStatus {
|
export interface ResourceStatus {
|
||||||
status: Status
|
status: Status
|
||||||
|
@ -25,4 +25,29 @@ export function formatTimestamp(timestamp: number): string {
|
|||||||
export function oneMonthAgo(){
|
export function oneMonthAgo(){
|
||||||
const oneMonthAgoTimestamp = dayjs().subtract(1, "month").valueOf();
|
const oneMonthAgoTimestamp = dayjs().subtract(1, "month").valueOf();
|
||||||
return oneMonthAgoTimestamp
|
return oneMonthAgoTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTime(seconds: number): string {
|
||||||
|
seconds = Math.floor(seconds);
|
||||||
|
const minutes: number | string = Math.floor(seconds / 60);
|
||||||
|
let hours: number | string = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
let remainingSeconds: number | string = seconds % 60;
|
||||||
|
let remainingMinutes: number | string = minutes % 60;
|
||||||
|
|
||||||
|
if (remainingSeconds < 10) {
|
||||||
|
remainingSeconds = "0" + remainingSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingMinutes < 10) {
|
||||||
|
remainingMinutes = "0" + remainingMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours === 0) {
|
||||||
|
hours = "";
|
||||||
|
} else {
|
||||||
|
hours = hours + ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hours + remainingMinutes + ":" + remainingSeconds;
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user