From a0c24209bb09008c5aab9901c000db1a217a7bb3 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Fri, 3 Feb 2023 16:34:41 +0100 Subject: [PATCH] implement more progres controls --- .../components/actions/LoadingAction.tsx | 14 +++ .../components/actions/MiddlePauseAction.tsx | 32 +++++ .../components/actions/ProgressAction.tsx | 81 ++++++++++++ .../components/actions/SkipTimeAction.tsx | 48 ++++++++ src/video/components/actions/TimeAction.tsx | 50 ++++++++ .../controllers/SourceController.tsx | 24 ++++ .../internal/VideoElementInternal.tsx | 10 +- src/video/state/events.ts | 2 +- src/video/state/init.ts | 2 + src/video/state/logic/controls.ts | 9 ++ src/video/state/logic/mediaplaying.ts | 4 + src/video/state/logic/progress.ts | 45 +++++++ src/video/state/logic/source.ts | 40 ++++++ src/video/state/providers/providerTypes.ts | 11 ++ src/video/state/providers/utils.ts | 9 ++ .../state/providers/videoStateProvider.ts | 115 +++++++++++++++++- src/video/state/types.ts | 7 ++ src/views/TestView.tsx | 17 +++ 18 files changed, 510 insertions(+), 10 deletions(-) create mode 100644 src/video/components/actions/LoadingAction.tsx create mode 100644 src/video/components/actions/MiddlePauseAction.tsx create mode 100644 src/video/components/actions/ProgressAction.tsx create mode 100644 src/video/components/actions/SkipTimeAction.tsx create mode 100644 src/video/components/actions/TimeAction.tsx create mode 100644 src/video/components/controllers/SourceController.tsx create mode 100644 src/video/state/logic/progress.ts create mode 100644 src/video/state/logic/source.ts 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 ( -