feat: implement pointer-lock and keyboard-lock

This commit is contained in:
Siyuan Miao 2025-04-11 07:03:32 +02:00
parent 82c018a2f6
commit b156e772a6
1 changed files with 76 additions and 7 deletions

View File

@ -6,6 +6,7 @@ import {
useMouseStore, useMouseStore,
useRTCStore, useRTCStore,
useSettingsStore, useSettingsStore,
useUiStore,
useVideoStore, useVideoStore,
} from "@/hooks/stores"; } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
@ -23,6 +24,7 @@ import {
LoadingVideoOverlay, LoadingVideoOverlay,
NoAutoplayPermissionsOverlay, NoAutoplayPermissionsOverlay,
} from "./VideoOverlay"; } from "./VideoOverlay";
import notifications from "@/notifications";
export default function WebRTCVideo() { export default function WebRTCVideo() {
// Video and stream related refs and states // Video and stream related refs and states
@ -61,6 +63,7 @@ export default function WebRTCVideo() {
// Misc states and hooks // Misc states and hooks
const [blockWheelEvent, setBlockWheelEvent] = useState(false); const [blockWheelEvent, setBlockWheelEvent] = useState(false);
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const [send] = useJsonRpc(); const [send] = useJsonRpc();
// Video-related // Video-related
@ -97,6 +100,62 @@ export default function WebRTCVideo() {
[setVideoClientSize, updateVideoSizeStore, setVideoSize], [setVideoClientSize, updateVideoSizeStore, setVideoSize],
); );
// Pointer lock and keyboard lock related
const isPointerLockPossible = useMemo(() => {
return window.location.protocol === "https:";
}, [window.location.protocol]);
const checkNavigatorPermissions = async (permissionName: string) => {
const name = permissionName as PermissionName;
const { state } = await navigator.permissions.query({ name });
return state === "granted";
};
const requestPointerLock = useCallback(async () => {
if (document.pointerLockElement) return;
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
if (isPointerLockGranted && settings.mouseMode === "relative") {
videoElm.current?.requestPointerLock();
}
}, [settings.mouseMode, videoElm]);
useEffect(() => {
if (!isPointerLockPossible || !videoElm.current) return;
const handlePointerLockChange = () => {
if (document.pointerLockElement) {
notifications.success("Pointer lock enabled, to exit it, press the escape key for a few seconds");
} else {
notifications.success("Pointer lock disabled");
}
};
document.addEventListener("pointerlockchange", handlePointerLockChange);
return () => {
document.removeEventListener("pointerlockchange", handlePointerLockChange);
};
}, [isPointerLockPossible, videoElm]);
const requestFullscreen = useCallback(async () => {
videoElm.current?.requestFullscreen({
navigationUI: "show",
});
// we do not care about pointer lock if it's for fullscreen
await requestPointerLock();
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
if (isKeyboardLockGranted) {
if ('keyboard' in navigator) {
// @ts-ignore
await navigator.keyboard.lock();
}
}
}, [disableVideoFocusTrap, requestPointerLock, checkNavigatorPermissions]);
// Mouse-related // Mouse-related
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos); const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
const sendRelMouseMovement = useCallback( const sendRelMouseMovement = useCallback(
@ -294,7 +353,8 @@ export default function WebRTCVideo() {
// console.log("KEYUP: Not focusing on the video", document.activeElement); // console.log("KEYUP: Not focusing on the video", document.activeElement);
// return; // return;
// } // }
console.log(document.activeElement);
// console.log(document.activeElement);
setIsNumLockActive(e.getModifierState("NumLock")); setIsNumLockActive(e.getModifierState("NumLock"));
setIsCapsLockActive(e.getModifierState("CapsLock")); setIsCapsLockActive(e.getModifierState("CapsLock"));
@ -537,11 +597,24 @@ export default function WebRTCVideo() {
const preventContextMenu = (e: MouseEvent) => e.preventDefault(); const preventContextMenu = (e: MouseEvent) => e.preventDefault();
containerElm.addEventListener("contextmenu", preventContextMenu, { signal }); containerElm.addEventListener("contextmenu", preventContextMenu, { signal });
// Request pointer lock when the container is clicked
if (isPointerLockPossible) {
const videoElmRefValue = videoElm.current;
if (videoElmRefValue) containerElm.addEventListener("click", () => {
if (disableVideoFocusTrap) return;
console.log("Requesting pointer lock");
requestPointerLock();
}, { signal });
}
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, },
[settings.mouseMode, relMouseMoveHandler, mouseWheelHandler], [
settings.mouseMode, relMouseMoveHandler, mouseWheelHandler,
disableVideoFocusTrap, requestPointerLock, isPointerLockPossible
],
); );
const hasNoAutoPlayPermissions = useMemo(() => { const hasNoAutoPlayPermissions = useMemo(() => {
@ -558,11 +631,7 @@ export default function WebRTCVideo() {
<div className="flex flex-col"> <div className="flex flex-col">
<fieldset disabled={peerConnection?.connectionState !== "connected"} className="contents"> <fieldset disabled={peerConnection?.connectionState !== "connected"} className="contents">
<Actionbar <Actionbar
requestFullscreen={async () => requestFullscreen={requestFullscreen}
videoElm.current?.requestFullscreen({
navigationUI: "show",
})
}
/> />
<MacroBar /> <MacroBar />
</fieldset> </fieldset>