3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-11 17:55:51 +00:00

Merge pull request #46 from QortalSeth/main

Videoplayer data is stored in a context.
This commit is contained in:
Qortal Dev 2024-10-24 16:04:54 -06:00 committed by GitHub
commit 4b0953666d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 317 additions and 283 deletions

View File

@ -2,23 +2,12 @@ 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";
import { useVideoContext } from "./VideoContext.ts";
export const LoadingVideo = () => {
const { isLoading, resourceStatus, src, startPlay, from, togglePlay } =
useVideoContext();
export interface LoadingVideoProps {
isLoading: boolean;
resourceStatus: any;
src: any;
startPlay: boolean;
from: any;
togglePlay: () => 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) + "%";
@ -27,7 +16,7 @@ export const LoadingVideo = ({
const dispatch = useDispatch();
return (
<>
{isLoading && (
{isLoading.value && (
<Box
position="absolute"
top={0}
@ -87,7 +76,7 @@ export const LoadingVideo = ({
)}
</Box>
)}
{((!src && !isLoading) || !startPlay) && (
{((!src && !isLoading.value) || !startPlay.value) && (
<Box
position="absolute"
top={0}

View File

@ -0,0 +1,107 @@
import {
Fullscreen,
MoreVert as MoreIcon,
Pause,
PictureInPicture,
PlayArrow,
Refresh,
VolumeUp,
} from "@mui/icons-material";
import { IconButton, Menu, MenuItem, Slider, Typography } from "@mui/material";
import { useVideoContext } from "./VideoContext.ts";
export const MobileControls = () => {
const {
togglePlay,
reloadVideo,
onProgressChange,
videoRef,
handleMenuOpen,
handleMenuClose,
onVolumeChange,
increaseSpeed,
togglePictureInPicture,
toggleFullscreen,
playing,
progress,
anchorEl,
volume,
playbackRate,
} = useVideoContext();
return (
<>
<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.value)}
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}x
</Typography>
</MenuItem>
<MenuItem onClick={togglePictureInPicture}>
<PictureInPicture />
</MenuItem>
<MenuItem onClick={toggleFullscreen}>
<Fullscreen />
</MenuItem>
</Menu>
</>
);
};

View File

@ -0,0 +1,28 @@
import { createContext, useContext } from "react";
import { useVideoPlayerState } from "../VideoPlayer-State.ts";
import { VideoPlayerProps } from "../VideoPlayer.tsx";
import { useVideoControlsState } from "./VideoControls-State.ts";
export type VideoContextType = VideoPlayerProps &
ReturnType<typeof useVideoPlayerState> &
ReturnType<typeof useVideoControlsState>;
export const VideoContext = createContext<VideoContextType | undefined>(
undefined
);
export const useContextData = (props, ref) => {
const videoState = useVideoPlayerState(props, ref);
const controlState = useVideoControlsState(props, videoState);
return {
...props,
...videoState,
...controlState,
};
};
export const useVideoContext = () => {
const context = useContext<VideoContextType>(VideoContext);
if (!context) throw new Error("VideoContext is NULL");
return context;
};

View File

@ -1,3 +1,9 @@
import { useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import { useEffect } from "react";
import ReactDOM from "react-dom";
import { useSelector } from "react-redux";
import { Key } from "ts-key-enum";
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
import {
@ -6,36 +12,33 @@ import {
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 { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useVideoPlayerState } from "../VideoPlayer-State.ts";
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: React.MutableRefObject<HTMLVideoElement>,
videoPlayerState: ReturnType<typeof useVideoPlayerState>
) => {
useSignals();
const { src, getSrc, resourceStatus } = videoPlayerState;
const {
src,
getSrc,
resourceStatus,
videoRef,
playbackRate,
playing,
isLoading,
startPlay,
volume,
isMuted,
mutedVolume,
progress,
videoObjectFit,
canPlay,
} = videoPlayerState;
const { identifier, autoPlay } = props;
const showControlsFullScreen = useSignal(true);
const persistSelector = store.getState().persist;
const videoPlaying = useSelector(
@ -146,8 +149,19 @@ export const useVideoControlsState = (
togglePlay();
};
const handleCanPlay = () => {
isLoading.value = false;
canPlay.value = true;
updatePlaybackRate(playbackRate.value);
setPlaying(true); // makes the video play when fully loaded
};
const setPlaying = async (setPlay: boolean) => {
setPlay ? await videoRef.current.play() : videoRef.current.pause();
playing.value = setPlay;
};
const togglePlay = async () => {
// console.log("in toggleplay playing is: ", playing.value);
if (!videoRef.current) return;
if (!src || resourceStatus?.status !== "READY") {
const el = document.getElementById("videoWrapper");
@ -159,13 +173,9 @@ export const useVideoControlsState = (
});
getSrc();
}
startPlay.value = true;
const pause = playing.value;
playing.value = !playing.value;
if (pause) videoRef.current.pause();
else await videoRef.current.play();
setPlaying(!playing.value);
};
const onVolumeChange = (_: any, value: number | number[]) => {
@ -178,9 +188,7 @@ export const useVideoControlsState = (
};
useEffect(() => {
if (autoPlay && identifier) {
togglePlay();
}
if (autoPlay && identifier) togglePlay();
}, [autoPlay, identifier]);
const mute = () => {
@ -353,12 +361,6 @@ export const useVideoControlsState = (
}
};
const handleCanPlay = () => {
isLoading.value = false;
canPlay.value = true;
updatePlaybackRate(playbackRate.value);
};
useEffect(() => {
videoRef.current.volume = volume.value;
if (
@ -390,5 +392,6 @@ export const useVideoControlsState = (
keyboardShortcutsDown,
handleCanPlay,
toggleMute,
showControlsFullScreen,
};
};

View File

@ -1,6 +1,5 @@
import {
Fullscreen,
MoreVert as MoreIcon,
Pause,
PictureInPicture,
PlayArrow,
@ -8,39 +7,15 @@ import {
VolumeOff,
VolumeUp,
} from "@mui/icons-material";
import { IconButton, Menu, MenuItem, Slider, Typography } from "@mui/material";
import { useSignals } from "@preact/signals-react/runtime";
import { IconButton, Slider, Typography } from "@mui/material";
import { useSignalEffect, 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) => {
import { ControlsContainer } from "../VideoPlayer-styles.ts";
import { MobileControls } from "./MobileControls.tsx";
import { useVideoContext } from "./VideoContext.ts";
export const VideoControls = () => {
useSignals();
const {
reloadVideo,
@ -51,97 +26,38 @@ export const VideoControls = ({
toggleFullscreen,
formatTime,
toggleMute,
} = controlState;
const { onProgressChange, handleMenuOpen, handleMenuClose, toggleRef } =
videoState;
const { from = null } = props;
onProgressChange,
toggleRef,
from,
videoRef,
canPlay,
isMobileView,
isMuted,
playbackRate,
playing,
progress,
volume,
showControlsFullScreen,
} = useVideoContext();
useEffect(() => {
useSignalEffect(() => {
console.log("canPlay is: ", canPlay.value); // makes the function execute when canPlay changes
const videoWidth = videoRef?.current?.offsetWidth;
if (videoWidth && videoWidth <= 600) {
isMobileView.value = true;
}
}, [canPlay.value]);
});
const showMobileControls =
isMobileView.value && canPlay.value && showControlsFullScreen.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>
</>
{showMobileControls ? (
<MobileControls />
) : canPlay.value ? (
<>
<IconButton
@ -174,14 +90,16 @@ export const VideoControls = ({
marginRight: "5px",
color: "rgba(255, 255, 255, 0.7)",
visibility:
!videoRef.current?.duration || !progress ? "hidden" : "visible",
!videoRef.current?.duration || !progress.value
? "hidden"
: "visible",
}}
>
{progress &&
{progress.value &&
videoRef.current?.duration &&
formatTime(progress.value)}
/
{progress &&
{progress.value &&
videoRef.current?.duration &&
formatTime(videoRef.current?.duration)}
</Typography>

View File

@ -1,4 +1,4 @@
import { signal } from "@preact/signals-react";
import { useSignal, useSignals } from "@preact/signals-react/runtime";
import React, {
useContext,
useEffect,
@ -10,33 +10,28 @@ import 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 { RootState } 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 playing = useSignal(false);
const isMuted = useSignal(false);
const progress = useSignal(0);
const isLoading = useSignal(false);
const canPlay = useSignal(false);
const startPlay = useSignal(false);
const isMobileView = useSignal(false);
const volume = useSignal(persistSelector.volume);
const mutedVolume = useSignal(persistSelector.volume);
const playbackRate = useSignal(persistSelector.playbackRate);
const anchorEl = useSignal(null);
const videoObjectFit = useSignal<StretchVideoType>(
persistSelector.stretchVideoSetting
);
const {
name,
@ -119,7 +114,8 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
if (!videoRef.current) return;
videoRef.current.currentTime = value as number;
progress.value = value as number;
if (!playing) {
if (!playing.value) {
await videoRef.current.play();
playing.value = true;
}
@ -269,5 +265,17 @@ export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
handleMenuOpen,
handleMenuClose,
toggleRef,
playing,
isMuted,
progress,
isLoading,
canPlay,
startPlay,
isMobileView,
volume,
mutedVolume,
playbackRate,
anchorEl,
videoObjectFit,
};
};

View File

@ -2,17 +2,8 @@ import { useSignals } from "@preact/signals-react/runtime";
import CSS from "csstype";
import { forwardRef } from "react";
import { LoadingVideo } from "./Components/LoadingVideo.tsx";
import {
showControlsFullScreen,
useVideoControlsState,
} from "./Components/VideoControls-State.ts";
import { useContextData, VideoContext } from "./Components/VideoContext.ts";
import { VideoControls } from "./Components/VideoControls.tsx";
import {
isLoading,
startPlay,
useVideoPlayerState,
videoObjectFit,
} from "./VideoPlayer-State.ts";
import { VideoContainer, VideoElement } from "./VideoPlayer-styles.ts";
export interface VideoStyles {
@ -37,95 +28,85 @@ export interface VideoPlayerProps {
style?: CSS.Properties;
}
export type refType = {
export type videoRefType = {
getContainerRef: () => React.MutableRefObject<HTMLDivElement>;
getVideoRef: () => React.MutableRefObject<HTMLVideoElement>;
};
export const VideoPlayer = forwardRef<refType, VideoPlayerProps>(
export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>(
(props: VideoPlayerProps, ref) => {
useSignals();
const contextData = useContextData(props, ref);
const {
poster,
identifier,
autoplay = true,
from = null,
videoStyles = {},
} = props;
const videoState = useVideoPlayerState(props, ref);
const {
keyboardShortcutsUp,
keyboardShortcutsDown,
from,
videoStyles,
containerRef,
resourceStatus,
videoRef,
src,
togglePlay,
identifier,
videoRef,
poster,
updateProgress,
autoplay,
handleEnded,
getSrc,
toggleRef,
} = videoState;
const controlState = useVideoControlsState(props, videoRef, videoState);
const { keyboardShortcutsUp, keyboardShortcutsDown, togglePlay } =
controlState;
handleCanPlay,
startPlay,
videoObjectFit,
showControlsFullScreen,
} = contextData;
return (
<VideoContainer
tabIndex={0}
onKeyUp={keyboardShortcutsUp}
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === "create" ? "8px" : 0,
...videoStyles?.videoContainer,
}}
ref={containerRef}
>
<LoadingVideo
isLoading={isLoading.value}
resourceStatus={resourceStatus}
src={src}
startPlay={startPlay.value}
from={from}
togglePlay={togglePlay}
/>
<VideoElement
id={identifier}
ref={videoRef}
src={
!startPlay.value
? ""
: resourceStatus?.status === "READY"
? src
: ""
}
poster={!startPlay.value ? poster : ""}
onTimeUpdate={updateProgress}
autoPlay={autoplay}
onClick={() => togglePlay()}
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={controlState.handleCanPlay}
onMouseEnter={e => {
showControlsFullScreen.value = true;
<VideoContext.Provider value={contextData}>
<VideoContainer
tabIndex={0}
onKeyUp={keyboardShortcutsUp}
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === "create" ? "8px" : 0,
...videoStyles?.videoContainer,
}}
onMouseLeave={e => {
showControlsFullScreen.value = false;
}}
preload="metadata"
style={
startPlay.value
? {
...videoStyles?.video,
objectFit: videoObjectFit.value,
}
: { height: "100%", ...videoStyles }
}
/>
<VideoControls
controlState={controlState}
videoState={videoState}
props={props}
videoRef={videoRef}
/>
</VideoContainer>
ref={containerRef}
>
<LoadingVideo />
<VideoElement
id={identifier}
ref={videoRef}
src={
!startPlay.value
? ""
: resourceStatus?.status === "READY"
? src
: ""
}
poster={!startPlay.value ? poster : ""}
onTimeUpdate={updateProgress}
autoPlay={autoplay}
onClick={() => togglePlay()}
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay}
onMouseEnter={e => {
showControlsFullScreen.value = true;
}}
onMouseLeave={e => {
showControlsFullScreen.value = false;
}}
preload="metadata"
style={
startPlay.value
? {
...videoStyles?.video,
objectFit: videoObjectFit.value,
}
: { height: "100%", ...videoStyles }
}
/>
<VideoControls />
</VideoContainer>
</VideoContext.Provider>
);
}
);

View File

@ -4,7 +4,7 @@ import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAn
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx";
import FileElement from "../../../components/common/FileElement.tsx";
import { refType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { videoRefType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { titleFormatterOnSave } from "../../../constants/Misc.ts";
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
import DownloadIcon from "@mui/icons-material/Download";

View File

@ -1,4 +1,4 @@
import { refType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { videoRefType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import {
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
@ -27,7 +27,7 @@ export const useVideoContentState = () => {
const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false);
const containerRef = useRef<refType>(null);
const containerRef = useRef<videoRefType>(null);
const [nameAddress, setNameAddress] = useState<string>("");
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(

View File

@ -6,7 +6,7 @@ import { CommentSection } from "../../../components/common/Comments/CommentSecti
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import {
refType,
videoRefType,
VideoPlayer,
} from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import {