diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 22d96502..8e5888a1 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -31,6 +31,7 @@ import { PictureInPictureAction } from "@/video/components/actions/PictureInPict import { CaptionRendererAction } from "./actions/CaptionRendererAction"; import { SettingsAction } from "./actions/SettingsAction"; import { DividerAction } from "./actions/DividerAction"; +import { VolumeAdjustedAction } from "./actions/VolumeAdjustedAction"; type Props = VideoPlayerBaseProps; @@ -91,6 +92,7 @@ export function VideoPlayer(props: Props) { <> + diff --git a/src/video/components/actions/KeyboardShortcutsAction.tsx b/src/video/components/actions/KeyboardShortcutsAction.tsx index 24e8b813..ba5ffc32 100644 --- a/src/video/components/actions/KeyboardShortcutsAction.tsx +++ b/src/video/components/actions/KeyboardShortcutsAction.tsx @@ -65,12 +65,12 @@ export function KeyboardShortcutsAction() { // Decrease volume case "arrowdown": - controls.setVolume(Math.max(mediaPlaying.volume - 0.1, 0)); + controls.setVolume(Math.max(mediaPlaying.volume - 0.1, 0), true); break; // Increase volume case "arrowup": - controls.setVolume(Math.min(mediaPlaying.volume + 0.1, 1)); + controls.setVolume(Math.min(mediaPlaying.volume + 0.1, 1), true); break; // Do a barrel Roll! diff --git a/src/video/components/actions/VolumeAdjustedAction.tsx b/src/video/components/actions/VolumeAdjustedAction.tsx new file mode 100644 index 00000000..29a58da1 --- /dev/null +++ b/src/video/components/actions/VolumeAdjustedAction.tsx @@ -0,0 +1,33 @@ +import { Icon, Icons } from "@/components/Icon"; +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"; + +export function VolumeAdjustedAction() { + const descriptor = useVideoPlayerDescriptor(); + const videoInterface = useInterface(descriptor); + const mediaPlaying = useMediaPlaying(descriptor); + + return ( +
+ 0 ? Icons.VOLUME : Icons.VOLUME_X} + className="text-xl text-white" + /> +
+
+
+
+ ); +} diff --git a/src/video/state/init.ts b/src/video/state/init.ts index bd4037fe..3c55a642 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -32,6 +32,7 @@ function initPlayer(): VideoPlayerState { isFocused: false, leftControlHovering: false, popoutBounds: null, + volumeChangedWithKeybind: false, }, mediaPlaying: { diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index e6d33369..fc8f99e5 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -5,6 +5,8 @@ import { VideoPlayerMeta } from "@/video/state/types"; import { getPlayerState } from "../cache"; import { VideoPlayerStateController } from "../providers/providerTypes"; +let volumeChangedWithKeybindDebounce: NodeJS.Timeout | null = null; + export type ControlMethods = { openPopout(id: string): void; closePopout(): void; @@ -48,8 +50,20 @@ export function useControls( enterFullscreen() { state.stateProvider?.enterFullscreen(); }, - setVolume(volume) { - state.stateProvider?.setVolume(volume); + setVolume(volume, isKeyboardEvent = false) { + if (isKeyboardEvent) { + if (volumeChangedWithKeybindDebounce) + clearTimeout(volumeChangedWithKeybindDebounce); + + state.interface.volumeChangedWithKeybind = true; + updateInterface(descriptor, state); + + volumeChangedWithKeybindDebounce = setTimeout(() => { + state.interface.volumeChangedWithKeybind = false; + updateInterface(descriptor, state); + }, 3e3); + } + state.stateProvider?.setVolume(volume, isKeyboardEvent); }, startAirplay() { state.stateProvider?.startAirplay(); diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 2f22823f..43185a3a 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -9,6 +9,7 @@ export type VideoInterfaceEvent = { isFocused: boolean; isFullscreen: boolean; popoutBounds: null | DOMRect; + volumeChangedWithKeybind: boolean; }; function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { @@ -18,6 +19,7 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { isFocused: state.interface.isFocused, isFullscreen: state.interface.isFullscreen, popoutBounds: state.interface.popoutBounds, + volumeChangedWithKeybind: state.interface.volumeChangedWithKeybind, }; } diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index ad09e812..acc73dc5 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -16,7 +16,7 @@ export type VideoPlayerStateController = { setSeeking(active: boolean): void; exitFullscreen(): void; enterFullscreen(): void; - setVolume(volume: number): void; + setVolume(volume: number, isKeyboardEvent?: boolean): void; startAirplay(): void; setCaption(id: string, url: string): void; clearCaption(): void; diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 1ba9ef7a..be6016c4 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -28,6 +28,7 @@ export type VideoPlayerState = { isFullscreen: boolean; popout: string | null; // id of current popout (eg source select, episode select) isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused) + volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently? leftControlHovering: boolean; // is the cursor hovered over the left side of player controls popoutBounds: null | DOMRect; // bounding box of current popout };