mirror of
https://github.com/Qortal/q-tube.git
synced 2025-02-11 17:55:51 +00:00
Refactor to video player
Video player split into multiple components Video player uses React signals for some of its state, this may improve performance.
This commit is contained in:
parent
302144f251
commit
0bed6cab8c
122
src/components/common/VideoPlayer/Components/LoadingVideo.tsx
Normal file
122
src/components/common/VideoPlayer/Components/LoadingVideo.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { PlayArrow } from "@mui/icons-material";
|
||||
|
||||
export interface LoadingVideoProps {
|
||||
isLoading: boolean;
|
||||
resourceStatus: any;
|
||||
src: any;
|
||||
startPlay: boolean;
|
||||
from: any;
|
||||
togglePlay: (isPlay?: boolean) => void;
|
||||
}
|
||||
export const LoadingVideo = ({
|
||||
isLoading,
|
||||
resourceStatus,
|
||||
src,
|
||||
startPlay,
|
||||
from,
|
||||
togglePlay,
|
||||
}: LoadingVideoProps) => {
|
||||
const getDownloadProgress = (current: number, total: number) => {
|
||||
const progress = (current / total) * 100;
|
||||
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={resourceStatus?.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" />
|
||||
{resourceStatus && (
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="div"
|
||||
sx={{
|
||||
color: "white",
|
||||
fontSize: "15px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{resourceStatus?.status === "NOT_PUBLISHED" && (
|
||||
<>Video file was not published. Please inform the publisher!</>
|
||||
)}
|
||||
{resourceStatus?.status === "REFETCHING" ? (
|
||||
<>
|
||||
<>
|
||||
{getDownloadProgress(
|
||||
resourceStatus?.localChunkCount,
|
||||
resourceStatus?.totalChunkCount
|
||||
)}
|
||||
</>
|
||||
|
||||
<> Refetching in 25 seconds</>
|
||||
</>
|
||||
) : resourceStatus?.status === "DOWNLOADED" ? (
|
||||
<>Download Completed: building video...</>
|
||||
) : resourceStatus?.status !== "READY" ? (
|
||||
<>
|
||||
{getDownloadProgress(
|
||||
resourceStatus?.localChunkCount,
|
||||
resourceStatus?.totalChunkCount
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>Fetching video...</>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{((!src && !isLoading) || !startPlay) && (
|
||||
<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={() => {
|
||||
if (from === "create") return;
|
||||
dispatch(setVideoPlaying(null));
|
||||
togglePlay();
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<PlayArrow
|
||||
sx={{
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,393 @@
|
||||
import { Key } from "ts-key-enum";
|
||||
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
|
||||
import {
|
||||
setReduxPlaybackRate,
|
||||
setStretchVideoSetting,
|
||||
setVolumeSetting,
|
||||
} from "../../../../state/features/persistSlice.ts";
|
||||
import { RootState, store } from "../../../../state/store.ts";
|
||||
import {
|
||||
canPlay,
|
||||
isLoading,
|
||||
isMuted,
|
||||
mutedVolume,
|
||||
playbackRate,
|
||||
playing,
|
||||
progress,
|
||||
startPlay,
|
||||
useVideoPlayerState,
|
||||
videoObjectFit,
|
||||
volume,
|
||||
} from "../VideoPlayer-State.ts";
|
||||
import { useEffect } from "react";
|
||||
import { signal, useSignal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { VideoPlayerProps } from "../VideoPlayer.tsx";
|
||||
import ReactDOM from "react-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export const showControlsFullScreen = signal(true);
|
||||
|
||||
export const useVideoControlsState = (
|
||||
props: VideoPlayerProps,
|
||||
videoRef,
|
||||
videoPlayerState: ReturnType<typeof useVideoPlayerState>
|
||||
) => {
|
||||
useSignals();
|
||||
const { src, getSrc, resourceStatus } = videoPlayerState;
|
||||
const { identifier, autoPlay } = props;
|
||||
const persistSelector = store.getState().persist;
|
||||
|
||||
const videoPlaying = useSelector(
|
||||
(state: RootState) => state.global.videoPlaying
|
||||
);
|
||||
|
||||
const minSpeed = 0.25;
|
||||
const maxSpeed = 4.0;
|
||||
const speedChange = 0.25;
|
||||
|
||||
const updatePlaybackRate = (newSpeed: number) => {
|
||||
if (videoRef.current) {
|
||||
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
|
||||
|
||||
videoRef.current.playbackRate = playbackRate.value;
|
||||
playbackRate.value = newSpeed;
|
||||
store.dispatch(setReduxPlaybackRate(newSpeed));
|
||||
}
|
||||
};
|
||||
|
||||
const increaseSpeed = (wrapOverflow = true) => {
|
||||
const changedSpeed = playbackRate.value + speedChange;
|
||||
const newSpeed = wrapOverflow
|
||||
? changedSpeed
|
||||
: Math.min(changedSpeed, maxSpeed);
|
||||
|
||||
if (videoRef.current) {
|
||||
updatePlaybackRate(newSpeed);
|
||||
}
|
||||
};
|
||||
|
||||
const decreaseSpeed = () => {
|
||||
if (videoRef.current) {
|
||||
updatePlaybackRate(playbackRate.value - speedChange);
|
||||
}
|
||||
};
|
||||
|
||||
const isFullscreen = useSignal(false);
|
||||
|
||||
const enterFullscreen = () => {
|
||||
if (!videoRef.current) return;
|
||||
if (videoRef.current.requestFullscreen) {
|
||||
videoRef.current.requestFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value ? exitFullscreen() : enterFullscreen();
|
||||
};
|
||||
const togglePictureInPicture = async () => {
|
||||
if (!videoRef.current) return;
|
||||
if (document.pictureInPictureElement === videoRef.current) {
|
||||
await document.exitPictureInPicture();
|
||||
} else {
|
||||
await videoRef.current.requestPictureInPicture();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const reloadVideo = async () => {
|
||||
if (!videoRef.current) return;
|
||||
const currentTime = videoRef.current.currentTime;
|
||||
videoRef.current.src = src;
|
||||
videoRef.current.load();
|
||||
videoRef.current.currentTime = currentTime;
|
||||
|
||||
playing.value = true;
|
||||
togglePlay();
|
||||
};
|
||||
|
||||
const togglePlay = async () => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
if (!src || resourceStatus?.status !== "READY") {
|
||||
const el = document.getElementById("videoWrapper");
|
||||
if (el) {
|
||||
el?.parentElement?.removeChild(el);
|
||||
}
|
||||
ReactDOM.flushSync(() => {
|
||||
isLoading.value = true;
|
||||
});
|
||||
getSrc();
|
||||
}
|
||||
|
||||
startPlay.value = true;
|
||||
playing.value = !playing.value;
|
||||
|
||||
if (playing.value) videoRef.current.pause();
|
||||
else await videoRef.current.play();
|
||||
};
|
||||
|
||||
const onVolumeChange = (_: any, value: number | number[]) => {
|
||||
if (!videoRef.current) return;
|
||||
const newVolume = value as number;
|
||||
videoRef.current.volume = newVolume;
|
||||
volume.value = newVolume;
|
||||
isMuted.value = false;
|
||||
store.dispatch(setVolumeSetting(newVolume));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
playing.value = true;
|
||||
if (autoPlay && identifier) togglePlay();
|
||||
}, [autoPlay, identifier]);
|
||||
|
||||
const mute = () => {
|
||||
isMuted.value = true;
|
||||
mutedVolume.value = volume.value;
|
||||
volume.value = 0;
|
||||
if (videoRef.current) videoRef.current.volume = 0;
|
||||
};
|
||||
const unMute = () => {
|
||||
isMuted.value = false;
|
||||
volume.value = mutedVolume.value;
|
||||
if (videoRef.current) videoRef.current.volume = mutedVolume.value;
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
isMuted.value ? unMute() : mute();
|
||||
};
|
||||
|
||||
const changeVolume = (volumeChange: number) => {
|
||||
if (videoRef.current) {
|
||||
const minVolume = 0;
|
||||
const maxVolume = 1;
|
||||
|
||||
let newVolume = volumeChange + volume.value;
|
||||
|
||||
newVolume = Math.max(newVolume, minVolume);
|
||||
newVolume = Math.min(newVolume, maxVolume);
|
||||
|
||||
isMuted.value = false;
|
||||
mutedVolume.value = newVolume;
|
||||
videoRef.current.volume = newVolume;
|
||||
volume.value = newVolume;
|
||||
store.dispatch(setVolumeSetting(newVolume));
|
||||
}
|
||||
};
|
||||
const setProgressRelative = (secondsChange: number) => {
|
||||
if (videoRef.current) {
|
||||
const currentTime = videoRef.current?.currentTime;
|
||||
const minTime = 0;
|
||||
const maxTime = videoRef.current?.duration || 100;
|
||||
|
||||
let newTime = currentTime + secondsChange;
|
||||
newTime = Math.max(newTime, minTime);
|
||||
newTime = Math.min(newTime, maxTime);
|
||||
videoRef.current.currentTime = newTime;
|
||||
progress.value = newTime;
|
||||
}
|
||||
};
|
||||
|
||||
const setProgressAbsolute = (videoPercent: number) => {
|
||||
if (videoRef.current) {
|
||||
videoPercent = Math.min(videoPercent, 100);
|
||||
videoPercent = Math.max(videoPercent, 0);
|
||||
const finalTime = (videoRef.current?.duration * videoPercent) / 100;
|
||||
videoRef.current.currentTime = finalTime;
|
||||
progress.value = finalTime;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleStretchVideoSetting = () => {
|
||||
const newStretchVideoSetting =
|
||||
persistSelector.stretchVideoSetting === "contain" ? "fill" : "contain";
|
||||
|
||||
videoObjectFit.value = newStretchVideoSetting;
|
||||
store.dispatch(setStretchVideoSetting(newStretchVideoSetting));
|
||||
};
|
||||
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
switch (e.key) {
|
||||
case "o":
|
||||
toggleStretchVideoSetting();
|
||||
break;
|
||||
|
||||
case Key.Add:
|
||||
increaseSpeed(false);
|
||||
break;
|
||||
case "+":
|
||||
increaseSpeed(false);
|
||||
break;
|
||||
case ">":
|
||||
increaseSpeed(false);
|
||||
break;
|
||||
|
||||
case Key.Subtract:
|
||||
decreaseSpeed();
|
||||
break;
|
||||
case "-":
|
||||
decreaseSpeed();
|
||||
break;
|
||||
case "<":
|
||||
decreaseSpeed();
|
||||
break;
|
||||
|
||||
case Key.ArrowLeft:
|
||||
{
|
||||
if (e.shiftKey) setProgressRelative(-300);
|
||||
else if (e.ctrlKey) setProgressRelative(-60);
|
||||
else if (e.altKey) setProgressRelative(-10);
|
||||
else setProgressRelative(-5);
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ArrowRight:
|
||||
{
|
||||
if (e.shiftKey) setProgressRelative(300);
|
||||
else if (e.ctrlKey) setProgressRelative(60);
|
||||
else if (e.altKey) setProgressRelative(10);
|
||||
else setProgressRelative(5);
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ArrowDown:
|
||||
changeVolume(-0.05);
|
||||
break;
|
||||
case Key.ArrowUp:
|
||||
changeVolume(0.05);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
switch (e.key) {
|
||||
case " ":
|
||||
togglePlay();
|
||||
break;
|
||||
case "m":
|
||||
toggleMute();
|
||||
break;
|
||||
|
||||
case "f":
|
||||
enterFullscreen();
|
||||
break;
|
||||
case Key.Escape:
|
||||
exitFullscreen();
|
||||
break;
|
||||
|
||||
case "0":
|
||||
setProgressAbsolute(0);
|
||||
break;
|
||||
case "1":
|
||||
setProgressAbsolute(10);
|
||||
break;
|
||||
case "2":
|
||||
setProgressAbsolute(20);
|
||||
break;
|
||||
case "3":
|
||||
setProgressAbsolute(30);
|
||||
break;
|
||||
case "4":
|
||||
setProgressAbsolute(40);
|
||||
break;
|
||||
case "5":
|
||||
setProgressAbsolute(50);
|
||||
break;
|
||||
case "6":
|
||||
setProgressAbsolute(60);
|
||||
break;
|
||||
case "7":
|
||||
setProgressAbsolute(70);
|
||||
break;
|
||||
case "8":
|
||||
setProgressAbsolute(80);
|
||||
break;
|
||||
case "9":
|
||||
setProgressAbsolute(90);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanPlay = () => {
|
||||
isLoading.value = false;
|
||||
canPlay.value = true;
|
||||
updatePlaybackRate(playbackRate.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
videoRef.current.volume = volume.value;
|
||||
if (
|
||||
videoPlaying &&
|
||||
videoPlaying.id === identifier &&
|
||||
src &&
|
||||
videoRef?.current
|
||||
) {
|
||||
handleCanPlay();
|
||||
|
||||
videoRef.current.currentTime = videoPlaying.currentTime;
|
||||
videoRef.current.play().then(() => {
|
||||
playing.value = true;
|
||||
startPlay.value = true;
|
||||
store.dispatch(setVideoPlaying(null));
|
||||
});
|
||||
}
|
||||
}, [videoPlaying, identifier, src]);
|
||||
|
||||
return {
|
||||
reloadVideo,
|
||||
togglePlay,
|
||||
onVolumeChange,
|
||||
increaseSpeed,
|
||||
togglePictureInPicture,
|
||||
toggleFullscreen,
|
||||
formatTime,
|
||||
keyboardShortcutsUp,
|
||||
keyboardShortcutsDown,
|
||||
handleCanPlay,
|
||||
toggleMute,
|
||||
};
|
||||
};
|
240
src/components/common/VideoPlayer/Components/VideoControls.tsx
Normal file
240
src/components/common/VideoPlayer/Components/VideoControls.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
import {
|
||||
Fullscreen,
|
||||
MoreVert as MoreIcon,
|
||||
Pause,
|
||||
PictureInPicture,
|
||||
PlayArrow,
|
||||
Refresh,
|
||||
VolumeOff,
|
||||
VolumeUp,
|
||||
} from "@mui/icons-material";
|
||||
import { IconButton, Menu, MenuItem, Slider, Typography } from "@mui/material";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
anchorEl,
|
||||
canPlay,
|
||||
isMobileView,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
playing,
|
||||
progress,
|
||||
useVideoPlayerState,
|
||||
volume,
|
||||
} from "../VideoPlayer-State.ts";
|
||||
import { ControlsContainer } from "../VideoPlayer-styles.ts";
|
||||
import { VideoPlayerProps } from "../VideoPlayer.tsx";
|
||||
import {
|
||||
showControlsFullScreen,
|
||||
useVideoControlsState,
|
||||
} from "./VideoControls-State.ts";
|
||||
|
||||
export interface VideoControlProps {
|
||||
controlState: ReturnType<typeof useVideoControlsState>;
|
||||
videoState: ReturnType<typeof useVideoPlayerState>;
|
||||
props: VideoPlayerProps;
|
||||
videoRef: any;
|
||||
}
|
||||
export const VideoControls = ({
|
||||
controlState,
|
||||
videoState,
|
||||
props,
|
||||
videoRef,
|
||||
}: VideoControlProps) => {
|
||||
useSignals();
|
||||
const {
|
||||
reloadVideo,
|
||||
togglePlay,
|
||||
onVolumeChange,
|
||||
increaseSpeed,
|
||||
togglePictureInPicture,
|
||||
toggleFullscreen,
|
||||
formatTime,
|
||||
toggleMute,
|
||||
} = controlState;
|
||||
const { onProgressChange, handleMenuOpen, handleMenuClose, toggleRef } =
|
||||
videoState;
|
||||
const { from = null } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const videoWidth = videoRef?.current?.offsetWidth;
|
||||
if (videoWidth && videoWidth <= 600) {
|
||||
isMobileView.value = true;
|
||||
}
|
||||
}, [canPlay.value]);
|
||||
|
||||
return (
|
||||
<ControlsContainer
|
||||
style={{ bottom: from === "create" ? "15px" : 0, padding: "0px" }}
|
||||
display={showControlsFullScreen.value ? "flex" : "none"}
|
||||
>
|
||||
{isMobileView.value && canPlay.value && showControlsFullScreen.value ? (
|
||||
<>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
}}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
{!playing.value ? <Pause /> : <PlayArrow />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
marginLeft: "15px",
|
||||
}}
|
||||
onClick={reloadVideo}
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<Slider
|
||||
value={progress.value}
|
||||
onChange={onProgressChange}
|
||||
min={0}
|
||||
max={videoRef.current?.duration || 100}
|
||||
sx={{ flexGrow: 1, mx: 2 }}
|
||||
/>
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="simple-menu"
|
||||
anchorEl={anchorEl.value}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
width: "250px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem>
|
||||
<VolumeUp />
|
||||
<Slider
|
||||
value={volume.value}
|
||||
onChange={onVolumeChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => increaseSpeed()}>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
Speed: {playbackRate.value}x
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={togglePictureInPicture}>
|
||||
<PictureInPicture />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleFullscreen}>
|
||||
<Fullscreen />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
) : canPlay.value ? (
|
||||
<>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
}}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
{!playing.value ? <Pause /> : <PlayArrow />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
marginLeft: "15px",
|
||||
}}
|
||||
onClick={reloadVideo}
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
<Slider
|
||||
value={progress.value}
|
||||
onChange={onProgressChange}
|
||||
min={0}
|
||||
max={videoRef.current?.duration || 100}
|
||||
sx={{ flexGrow: 1, mx: 2 }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
marginRight: "5px",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
visibility:
|
||||
!videoRef.current?.duration || !progress ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
{progress &&
|
||||
videoRef.current?.duration &&
|
||||
formatTime(progress.value)}
|
||||
/
|
||||
{progress &&
|
||||
videoRef.current?.duration &&
|
||||
formatTime(videoRef.current?.duration)}
|
||||
</Typography>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
onClick={toggleMute}
|
||||
>
|
||||
{isMuted.value ? <VolumeOff /> : <VolumeUp />}
|
||||
</IconButton>
|
||||
<Slider
|
||||
value={volume.value}
|
||||
onChange={onVolumeChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
sx={{
|
||||
maxWidth: "100px",
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
fontSize: "14px",
|
||||
marginLeft: "5px",
|
||||
}}
|
||||
onClick={e => increaseSpeed()}
|
||||
>
|
||||
Speed: {playbackRate}x
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
marginLeft: "15px",
|
||||
}}
|
||||
ref={toggleRef}
|
||||
onClick={togglePictureInPicture}
|
||||
>
|
||||
<PictureInPicture />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
}}
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
<Fullscreen />
|
||||
</IconButton>
|
||||
</>
|
||||
) : null}
|
||||
</ControlsContainer>
|
||||
);
|
||||
};
|
273
src/components/common/VideoPlayer/VideoPlayer-State.ts
Normal file
273
src/components/common/VideoPlayer/VideoPlayer-State.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import { signal } from "@preact/signals-react";
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
|
||||
import { StretchVideoType } from "../../../state/features/persistSlice.ts";
|
||||
import { RootState, store } from "../../../state/store.ts";
|
||||
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
|
||||
import { VideoPlayerProps } from "./VideoPlayer.tsx";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const playing = signal(false);
|
||||
|
||||
export const isMuted = signal(false);
|
||||
export const progress = signal(0);
|
||||
export const isLoading = signal(false);
|
||||
export const canPlay = signal(false);
|
||||
export const startPlay = signal(false);
|
||||
export const isMobileView = signal(false);
|
||||
|
||||
export const volume = signal(0.5);
|
||||
export const mutedVolume = signal(0.5);
|
||||
export const playbackRate = signal(1);
|
||||
export const anchorEl = signal(null);
|
||||
export const videoObjectFit = signal<StretchVideoType>("contain");
|
||||
|
||||
export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
|
||||
useSignals();
|
||||
const persistSelector = useSelector((state: RootState) => state.persist);
|
||||
volume.value = persistSelector.volume;
|
||||
mutedVolume.value = persistSelector.volume;
|
||||
playbackRate.value = persistSelector.playbackRate;
|
||||
videoObjectFit.value = persistSelector.stretchVideoSetting;
|
||||
|
||||
const {
|
||||
name,
|
||||
identifier,
|
||||
service,
|
||||
user = "",
|
||||
jsonId = "",
|
||||
nextVideo,
|
||||
onEnd,
|
||||
} = props;
|
||||
const dispatch = useDispatch();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { downloads } = useSelector((state: RootState) => state.global);
|
||||
const reDownload = useRef<boolean>(false);
|
||||
const reDownloadNextVid = useRef<boolean>(false);
|
||||
const isFetchingProperties = useRef<boolean>(false);
|
||||
const status = useRef<null | string>(null);
|
||||
|
||||
const download = useMemo(() => {
|
||||
if (!downloads || !identifier) return {};
|
||||
const findDownload = downloads[identifier];
|
||||
|
||||
if (!findDownload) return {};
|
||||
return findDownload;
|
||||
}, [downloads, identifier]);
|
||||
|
||||
const src = useMemo(() => {
|
||||
return download?.url || "";
|
||||
}, [download?.url]);
|
||||
|
||||
const resourceStatus = useMemo(() => {
|
||||
return download?.status || {};
|
||||
}, [download]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getVideoRef: () => {
|
||||
return videoRef;
|
||||
},
|
||||
getContainerRef: () => {
|
||||
return containerRef;
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
reDownload.current = false;
|
||||
reDownloadNextVid.current = false;
|
||||
isLoading.value = false;
|
||||
canPlay.value = false;
|
||||
progress.value = 0;
|
||||
playing.value = false;
|
||||
startPlay.value = false;
|
||||
isFetchingProperties.current = false;
|
||||
status.current = null;
|
||||
}, [identifier]);
|
||||
|
||||
const refetch = React.useCallback(async () => {
|
||||
if (!name || !identifier || !service || isFetchingProperties.current)
|
||||
return;
|
||||
try {
|
||||
isFetchingProperties.current = true;
|
||||
await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_PROPERTIES",
|
||||
name,
|
||||
service,
|
||||
identifier,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
isFetchingProperties.current = false;
|
||||
}
|
||||
}, [identifier, name, service]);
|
||||
|
||||
const toggleRef = useRef<any>(null);
|
||||
const { downloadVideo } = useContext(MyContext);
|
||||
|
||||
const onProgressChange = async (_: any, value: number | number[]) => {
|
||||
if (!videoRef.current) return;
|
||||
videoRef.current.currentTime = value as number;
|
||||
progress.value = value as number;
|
||||
if (!playing) {
|
||||
await videoRef.current.play();
|
||||
playing.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnded = () => {
|
||||
playing.value = false;
|
||||
if (onEnd) {
|
||||
onEnd();
|
||||
}
|
||||
};
|
||||
|
||||
const updateProgress = () => {
|
||||
if (!videoRef.current) return;
|
||||
progress.value = videoRef.current.currentTime;
|
||||
};
|
||||
|
||||
const getSrc = React.useCallback(async () => {
|
||||
if (!name || !identifier || !service || !jsonId || !user) return;
|
||||
try {
|
||||
downloadVideo({
|
||||
name,
|
||||
service,
|
||||
identifier,
|
||||
properties: {
|
||||
jsonId,
|
||||
user,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [identifier, name, service, jsonId, user]);
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
|
||||
const handleLeavePictureInPicture = async (event: any) => {
|
||||
const target = event?.target;
|
||||
if (target) {
|
||||
target.pause();
|
||||
if (playing.value) {
|
||||
playing.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (videoElement) {
|
||||
videoElement.addEventListener(
|
||||
"leavepictureinpicture",
|
||||
handleLeavePictureInPicture
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (videoElement) {
|
||||
videoElement.removeEventListener(
|
||||
"leavepictureinpicture",
|
||||
handleLeavePictureInPicture
|
||||
);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
|
||||
const minimizeVideo = async () => {
|
||||
if (!videoElement) return;
|
||||
|
||||
dispatch(setVideoPlaying(videoElement));
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (videoElement) {
|
||||
if (videoElement && !videoElement.paused && !videoElement.ended) {
|
||||
minimizeVideo();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refetchInInterval = () => {
|
||||
try {
|
||||
const interval = setInterval(() => {
|
||||
if (status?.current === "DOWNLOADED") refetch();
|
||||
if (status?.current === "READY") clearInterval(interval);
|
||||
}, 7500);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (resourceStatus?.status) {
|
||||
status.current = resourceStatus?.status;
|
||||
}
|
||||
if (
|
||||
resourceStatus?.status === "DOWNLOADED" &&
|
||||
reDownload?.current === false
|
||||
) {
|
||||
refetchInInterval();
|
||||
reDownload.current = true;
|
||||
}
|
||||
}, [getSrc, resourceStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resourceStatus?.status) {
|
||||
status.current = resourceStatus?.status;
|
||||
}
|
||||
if (
|
||||
resourceStatus?.status === "READY" &&
|
||||
reDownloadNextVid?.current === false
|
||||
) {
|
||||
if (nextVideo) {
|
||||
downloadVideo({
|
||||
name: nextVideo?.name,
|
||||
service: nextVideo?.service,
|
||||
identifier: nextVideo?.identifier,
|
||||
properties: {
|
||||
jsonId: nextVideo?.jsonId,
|
||||
user,
|
||||
},
|
||||
});
|
||||
}
|
||||
reDownloadNextVid.current = true;
|
||||
}
|
||||
}, [getSrc, resourceStatus]);
|
||||
|
||||
const handleMenuOpen = (event: any) => {
|
||||
anchorEl.value = event.currentTarget;
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
anchorEl.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
resourceStatus,
|
||||
videoRef,
|
||||
src,
|
||||
getSrc,
|
||||
updateProgress,
|
||||
handleEnded,
|
||||
onProgressChange,
|
||||
handleMenuOpen,
|
||||
handleMenuClose,
|
||||
toggleRef,
|
||||
};
|
||||
};
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user