diff --git a/src/video/components/actions/LoadingAction.tsx b/src/video/components/actions/LoadingAction.tsx new file mode 100644 index 00000000..0bbe6072 --- /dev/null +++ b/src/video/components/actions/LoadingAction.tsx @@ -0,0 +1,14 @@ +import { Spinner } from "@/components/layout/Spinner"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; + +export function LoadingAction() { + const descriptor = useVideoPlayerDescriptor(); + const mediaPlaying = useMediaPlaying(descriptor); + + const isLoading = mediaPlaying.isFirstLoading || mediaPlaying.isLoading; + + if (!isLoading) return null; + + return ; +} diff --git a/src/video/components/actions/MiddlePauseAction.tsx b/src/video/components/actions/MiddlePauseAction.tsx new file mode 100644 index 00000000..1ed2fefc --- /dev/null +++ b/src/video/components/actions/MiddlePauseAction.tsx @@ -0,0 +1,32 @@ +import { Icon, Icons } from "@/components/Icon"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useControls } from "@/video/state/logic/controls"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; +import { useCallback } from "react"; + +export function MiddlePauseAction() { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const mediaPlaying = useMediaPlaying(descriptor); + + const handleClick = useCallback(() => { + if (mediaPlaying?.isPlaying) controls.pause(); + else controls.play(); + }, [controls, mediaPlaying]); + + if (mediaPlaying.hasPlayedOnce) return null; + if (mediaPlaying.isPlaying) return null; + if (mediaPlaying.isFirstLoading) return null; + + return ( +
+ +
+ ); +} diff --git a/src/video/components/actions/ProgressAction.tsx b/src/video/components/actions/ProgressAction.tsx new file mode 100644 index 00000000..096985ae --- /dev/null +++ b/src/video/components/actions/ProgressAction.tsx @@ -0,0 +1,81 @@ +import { + makePercentage, + makePercentageString, + useProgressBar, +} from "@/hooks/useProgressBar"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useControls } from "@/video/state/logic/controls"; +import { useProgress } from "@/video/state/logic/progress"; +import { useCallback, useEffect, useRef } from "react"; + +export function ProgressAction() { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const videoTime = useProgress(descriptor); + const ref = useRef(null); + const dragRef = useRef(false); + + const commitTime = useCallback( + (percentage) => { + controls.setTime(percentage * videoTime.duration); + }, + [controls, videoTime] + ); + const { dragging, dragPercentage, dragMouseDown } = useProgressBar( + ref, + commitTime + ); + + // TODO make dragging update timer + useEffect(() => { + if (dragRef.current === dragging) return; + dragRef.current = dragging; + controls.setSeeking(dragging); + }, [dragRef, dragging, controls]); + + let watchProgress = makePercentageString( + makePercentage((videoTime.time / videoTime.duration) * 100) + ); + if (dragging) + watchProgress = makePercentageString(makePercentage(dragPercentage)); + + const bufferProgress = makePercentageString( + makePercentage((videoTime.buffered / videoTime.duration) * 100) + ); + + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/video/components/actions/SkipTimeAction.tsx b/src/video/components/actions/SkipTimeAction.tsx new file mode 100644 index 00000000..117d31f0 --- /dev/null +++ b/src/video/components/actions/SkipTimeAction.tsx @@ -0,0 +1,48 @@ +import { Icons } from "@/components/Icon"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useControls } from "@/video/state/logic/controls"; +import { useProgress } from "@/video/state/logic/progress"; +import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; + +interface Props { + className?: string; +} + +export function SkipTimeBackwardAction() { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const videoTime = useProgress(descriptor); + + const skipBackward = () => { + controls.setTime(videoTime.time - 10); + }; + + return ( + + ); +} + +export function SkipTimeForwardAction() { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const videoTime = useProgress(descriptor); + + const skipForward = () => { + controls.setTime(videoTime.time + 10); + }; + + return ( + + ); +} + +export function SkipTimeAction(props: Props) { + return ( +
+
+ + +
+
+ ); +} diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx new file mode 100644 index 00000000..cd1caadc --- /dev/null +++ b/src/video/components/actions/TimeAction.tsx @@ -0,0 +1,50 @@ +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useProgress } from "@/video/state/logic/progress"; + +function durationExceedsHour(secs: number): boolean { + return secs > 60 * 60; +} + +function formatSeconds(secs: number, showHours = false): string { + if (Number.isNaN(secs)) { + if (showHours) return "0:00:00"; + return "0:00"; + } + + let time = secs; + const seconds = Math.floor(time % 60); + + time /= 60; + const minutes = Math.floor(time % 60); + + time /= 60; + const hours = Math.floor(time); + + const paddedSecs = seconds.toString().padStart(2, "0"); + const paddedMins = minutes.toString().padStart(2, "0"); + + if (!showHours) return [paddedMins, paddedSecs].join(":"); + return [hours, paddedMins, paddedSecs].join(":"); +} + +interface Props { + className?: string; + noDuration?: boolean; +} + +export function TimeAction(props: Props) { + const descriptor = useVideoPlayerDescriptor(); + const videoTime = useProgress(descriptor); + + const hasHours = durationExceedsHour(videoTime.duration); + const time = formatSeconds(videoTime.time, hasHours); + const duration = formatSeconds(videoTime.duration, hasHours); + + return ( +
+

+ {time} {props.noDuration ? "" : `/ ${duration}`} +

+
+ ); +} diff --git a/src/video/components/controllers/SourceController.tsx b/src/video/components/controllers/SourceController.tsx new file mode 100644 index 00000000..af3f715d --- /dev/null +++ b/src/video/components/controllers/SourceController.tsx @@ -0,0 +1,24 @@ +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useControls } from "@/video/state/logic/controls"; +import { useEffect, useRef } from "react"; + +interface SourceControllerProps { + source: string; + type: MWStreamType; + quality: MWStreamQuality; +} + +export function SourceController(props: SourceControllerProps) { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const didInitialize = useRef(false); + + useEffect(() => { + if (didInitialize.current) return; + controls.setSource(props); + didInitialize.current = true; + }, [props, controls]); + + return null; +} diff --git a/src/video/components/internal/VideoElementInternal.tsx b/src/video/components/internal/VideoElementInternal.tsx index d084b1ac..7a8f1219 100644 --- a/src/video/components/internal/VideoElementInternal.tsx +++ b/src/video/components/internal/VideoElementInternal.tsx @@ -19,12 +19,6 @@ export function VideoElementInternal() { }, [descriptor]); // TODO autoplay and muted - return ( -