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
};