From c3b409631e66d6e117f0caf43e9fa54d386521eb Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sat, 4 Feb 2023 18:24:06 +0100 Subject: [PATCH] volume control + progress listener --- src/hooks/useVolumeToggle.ts | 16 ++-- src/index.tsx | 1 + src/video/components/VideoPlayer.tsx | 6 +- src/video/components/VideoPlayerBase.tsx | 3 +- src/video/components/actions/VolumeAction.tsx | 92 +++++++++++++++++++ .../ProgressListenerController.tsx | 47 ++++++++++ src/video/components/hooks/volumeStore.ts | 25 +++++ .../internal/VideoElementInternal.tsx | 21 ++++- src/video/state/init.ts | 3 +- src/video/state/logic/controls.ts | 3 + src/video/state/logic/mediaplaying.ts | 2 + src/video/state/providers/providerTypes.ts | 1 + .../state/providers/videoStateProvider.ts | 66 ++++++++++++- src/video/state/types.ts | 7 +- src/views/media/MediaView.tsx | 7 +- 15 files changed, 276 insertions(+), 24 deletions(-) create mode 100644 src/video/components/actions/VolumeAction.tsx create mode 100644 src/video/components/controllers/ProgressListenerController.tsx create mode 100644 src/video/components/hooks/volumeStore.ts diff --git a/src/hooks/useVolumeToggle.ts b/src/hooks/useVolumeToggle.ts index b0fad7b6..636b787b 100644 --- a/src/hooks/useVolumeToggle.ts +++ b/src/hooks/useVolumeToggle.ts @@ -1,16 +1,18 @@ -import { useVideoPlayerState } from "@/../__old/VideoContext"; +import { useControls } from "@/video/state/logic/controls"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useState } from "react"; -export function useVolumeControl() { +export function useVolumeControl(descriptor: string) { const [storedVolume, setStoredVolume] = useState(1); - const { videoState } = useVideoPlayerState(); + const controls = useControls(descriptor); + const mediaPlaying = useMediaPlaying(descriptor); const toggleVolume = () => { - if (videoState.volume > 0) { - setStoredVolume(videoState.volume); - videoState.setVolume(0); + if (mediaPlaying.volume > 0) { + setStoredVolume(mediaPlaying.volume); + controls.setVolume(0); } else { - videoState.setVolume(storedVolume > 0 ? storedVolume : 1); + controls.setVolume(storedVolume > 0 ? storedVolume : 1); } }; diff --git a/src/index.tsx b/src/index.tsx index 1a7a7aa0..ec109383 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,7 @@ if (key) { initializeChromecast(); // TODO video todos: +// - mobile controls start showing when resizing // - captions // - chrome cast support // - safari fullscreen will make video overlap player controls diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 67977b93..cb441ce8 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -13,6 +13,7 @@ import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayA import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction"; import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction"; import { TimeAction } from "@/video/components/actions/TimeAction"; +import { VolumeAction } from "@/video/components/actions/VolumeAction"; import { VideoPlayerError } from "@/video/components/parts/VideoPlayerError"; import { VideoPlayerBase, @@ -54,7 +55,7 @@ function LeftSideControls() { > - {/* */} + @@ -73,10 +74,9 @@ export function VideoPlayer(props: Props) { [setShow] ); - // TODO autoplay // TODO safe area only if full screen or fill screen return ( - + diff --git a/src/video/components/VideoPlayerBase.tsx b/src/video/components/VideoPlayerBase.tsx index 2dd6ced0..71da6fd0 100644 --- a/src/video/components/VideoPlayerBase.tsx +++ b/src/video/components/VideoPlayerBase.tsx @@ -5,6 +5,7 @@ import { VideoElementInternal } from "./internal/VideoElementInternal"; export interface VideoPlayerBaseProps { children?: React.ReactNode; + autoPlay?: boolean; } export function VideoPlayerBase(props: VideoPlayerBaseProps) { @@ -19,7 +20,7 @@ export function VideoPlayerBase(props: VideoPlayerBaseProps) { ref={ref} className="is-video-player relative h-full w-full select-none overflow-hidden bg-black [border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]" > - +
{props.children}
diff --git a/src/video/components/actions/VolumeAction.tsx b/src/video/components/actions/VolumeAction.tsx new file mode 100644 index 00000000..c3aee7c1 --- /dev/null +++ b/src/video/components/actions/VolumeAction.tsx @@ -0,0 +1,92 @@ +import { Icon, Icons } from "@/components/Icon"; +import { + makePercentage, + makePercentageString, + useProgressBar, +} from "@/hooks/useProgressBar"; +import { useVolumeControl } from "@/hooks/useVolumeToggle"; +import { canChangeVolume } from "@/utils/detectFeatures"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useControls } from "@/video/state/logic/controls"; +import { useInterface } from "@/video/state/logic/interface"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; +import { useCallback, useEffect, useRef, useState } from "react"; + +interface Props { + className?: string; +} + +export function VolumeAction(props: Props) { + const descriptor = useVideoPlayerDescriptor(); + const controls = useControls(descriptor); + const mediaPlaying = useMediaPlaying(descriptor); + const videoInterface = useInterface(descriptor); + const { setStoredVolume, toggleVolume } = useVolumeControl(descriptor); + const ref = useRef(null); + const [hoveredOnce, setHoveredOnce] = useState(false); + + const commitVolume = useCallback( + (percentage) => { + controls.setVolume(percentage); + setStoredVolume(percentage); + }, + [controls, setStoredVolume] + ); + const { dragging, dragPercentage, dragMouseDown } = useProgressBar( + ref, + commitVolume, + true + ); + + useEffect(() => { + if (!videoInterface.leftControlHovering) setHoveredOnce(false); + }, [videoInterface]); + + const handleClick = useCallback(() => { + toggleVolume(); + }, [toggleVolume]); + + const handleMouseEnter = useCallback(async () => { + if (await canChangeVolume()) setHoveredOnce(true); + }, [setHoveredOnce]); + + let percentage = makePercentage(mediaPlaying.volume * 100); + if (dragging) percentage = makePercentage(dragPercentage); + const percentageString = makePercentageString(percentage); + + return ( +
+
+
+ 0 ? Icons.VOLUME : Icons.VOLUME_X} /> +
+
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/video/components/controllers/ProgressListenerController.tsx b/src/video/components/controllers/ProgressListenerController.tsx new file mode 100644 index 00000000..27434bd5 --- /dev/null +++ b/src/video/components/controllers/ProgressListenerController.tsx @@ -0,0 +1,47 @@ +import { useEffect, useMemo, useRef } from "react"; +import throttle from "lodash.throttle"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; +import { useProgress } from "@/video/state/logic/progress"; +import { useControls } from "@/video/state/logic/controls"; + +interface Props { + startAt?: number; + onProgress?: (time: number, duration: number) => void; +} + +export function ProgressListenerController(props: Props) { + const descriptor = useVideoPlayerDescriptor(); + const mediaPlaying = useMediaPlaying(descriptor); + const progress = useProgress(descriptor); + const controls = useControls(descriptor); + const didInitialize = useRef(null); + + // time updates (throttled) + const updateTime = useMemo( + () => throttle((a: number, b: number) => props.onProgress?.(a, b), 1000), + [props] + ); + useEffect(() => { + if (!mediaPlaying.isPlaying) return; + if (progress.duration === 0 || progress.time === 0) return; + updateTime(progress.time, progress.duration); + }, [progress, mediaPlaying, updateTime]); + useEffect(() => { + return () => { + updateTime.cancel(); + }; + }, [updateTime]); + + // initialize + useEffect(() => { + if (didInitialize.current) return; + if (mediaPlaying.isFirstLoading || Number.isNaN(progress.duration)) return; + if (props.startAt !== undefined) { + controls.setTime(props.startAt); + } + didInitialize.current = true; + }, [didInitialize, props, progress, mediaPlaying, controls]); + + return null; +} diff --git a/src/video/components/hooks/volumeStore.ts b/src/video/components/hooks/volumeStore.ts new file mode 100644 index 00000000..3b328810 --- /dev/null +++ b/src/video/components/hooks/volumeStore.ts @@ -0,0 +1,25 @@ +import { versionedStoreBuilder } from "@/utils/storage"; + +export const volumeStore = versionedStoreBuilder() + .setKey("mw-volume") + .addVersion({ + version: 0, + create() { + return { + volume: 1, + }; + }, + }) + .build(); + +export function getStoredVolume(): number { + const store = volumeStore.get(); + return store.volume; +} + +export function setStoredVolume(volume: number) { + const store = volumeStore.get(); + store.save({ + volume, + }); +} diff --git a/src/video/components/internal/VideoElementInternal.tsx b/src/video/components/internal/VideoElementInternal.tsx index 7a8f1219..c4e6ae28 100644 --- a/src/video/components/internal/VideoElementInternal.tsx +++ b/src/video/components/internal/VideoElementInternal.tsx @@ -1,10 +1,16 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { setProvider, unsetStateProvider } from "@/video/state/providers/utils"; import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider"; import { useEffect, useRef } from "react"; -export function VideoElementInternal() { +interface Props { + autoPlay?: boolean; +} + +export function VideoElementInternal(props: Props) { const descriptor = useVideoPlayerDescriptor(); + const mediaPlaying = useMediaPlaying(descriptor); const ref = useRef(null); useEffect(() => { @@ -18,7 +24,16 @@ export function VideoElementInternal() { }; }, [descriptor]); - // TODO autoplay and muted + // TODO shortcuts + // this element is remotely controlled by a state provider - return