mirror of https://github.com/jetkvm/kvm.git
Compare commits
5 Commits
8d77d75294
...
52dd675e52
Author | SHA1 | Date |
---|---|---|
|
52dd675e52 | |
|
e95e30e48c | |
|
eaa58492ab | |
|
f4bb47c544 | |
|
a7693df92c |
15
display.go
15
display.go
|
@ -339,10 +339,18 @@ func startBacklightTickers() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
// Stop existing tickers to prevent multiple active instances on repeated calls
|
||||||
|
if dimTicker != nil {
|
||||||
|
dimTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if offTicker != nil {
|
||||||
|
offTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DisplayDimAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("dim_ticker has started")
|
displayLogger.Info().Msg("dim_ticker has started")
|
||||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
defer dimTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:staticcheck
|
||||||
|
@ -354,10 +362,9 @@ func startBacklightTickers() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
if config.DisplayOffAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("off_ticker has started")
|
displayLogger.Info().Msg("off_ticker has started")
|
||||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
defer offTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:staticcheck
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
@ -149,6 +150,12 @@ func (c *DHCPClient) loadLeaseFile() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
isFirstLoad := c.lease == nil
|
isFirstLoad := c.lease == nil
|
||||||
|
|
||||||
|
// Skip processing if lease hasn't changed to avoid unnecessary wake-ups.
|
||||||
|
if reflect.DeepEqual(c.lease, lease) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
c.lease = lease
|
c.lease = lease
|
||||||
|
|
||||||
if lease.IPAddress == nil {
|
if lease.IPAddress == nil {
|
||||||
|
|
24
terminal.go
24
terminal.go
|
@ -1,6 +1,7 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -55,18 +56,23 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg.IsString {
|
if msg.IsString {
|
||||||
var size TerminalSize
|
maybeJson := bytes.TrimSpace(msg.Data)
|
||||||
err := json.Unmarshal([]byte(msg.Data), &size)
|
// Cheap check to see if this resembles JSON
|
||||||
if err == nil {
|
if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' {
|
||||||
err = pty.Setsize(ptmx, &pty.Winsize{
|
var size TerminalSize
|
||||||
Rows: uint16(size.Rows),
|
err := json.Unmarshal(maybeJson, &size)
|
||||||
Cols: uint16(size.Cols),
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
err = pty.Setsize(ptmx, &pty.Winsize{
|
||||||
|
Rows: uint16(size.Rows),
|
||||||
|
Cols: uint16(size.Cols),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
scopedLogger.Info().Int("rows", size.Rows).Int("cols", size.Cols).Msg("Set terminal size")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
||||||
}
|
}
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
|
||||||
}
|
}
|
||||||
_, err := ptmx.Write(msg.Data)
|
_, err := ptmx.Write(msg.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
|
|
||||||
interface OverlayContentProps {
|
interface OverlayContentProps {
|
||||||
children: React.ReactNode;
|
readonly children: React.ReactNode;
|
||||||
}
|
}
|
||||||
function OverlayContent({ children }: OverlayContentProps) {
|
function OverlayContent({ children }: OverlayContentProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +23,7 @@ function OverlayContent({ children }: OverlayContentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
|
@ -57,8 +57,8 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingConnectionOverlayProps {
|
interface LoadingConnectionOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
text: string;
|
readonly text: string;
|
||||||
}
|
}
|
||||||
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -91,8 +91,8 @@ export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverla
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectionErrorOverlayProps {
|
interface ConnectionErrorOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
setupPeerConnection: () => Promise<void>;
|
readonly setupPeerConnection: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionFailedOverlay({
|
export function ConnectionFailedOverlay({
|
||||||
|
@ -153,7 +153,7 @@ export function ConnectionFailedOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PeerConnectionDisconnectedOverlay {
|
interface PeerConnectionDisconnectedOverlay {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PeerConnectionDisconnectedOverlay({
|
export function PeerConnectionDisconnectedOverlay({
|
||||||
|
@ -207,8 +207,8 @@ export function PeerConnectionDisconnectedOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HDMIErrorOverlayProps {
|
interface HDMIErrorOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
hdmiState: string;
|
readonly hdmiState: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
|
@ -310,8 +310,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NoAutoplayPermissionsOverlayProps {
|
interface NoAutoplayPermissionsOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
onPlayClick: () => void;
|
readonly onPlayClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoAutoplayPermissionsOverlay({
|
export function NoAutoplayPermissionsOverlay({
|
||||||
|
@ -361,7 +361,7 @@ export function NoAutoplayPermissionsOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PointerLockBarProps {
|
interface PointerLockBarProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PointerLockBar({ show }: PointerLockBarProps) {
|
export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
|
@ -369,10 +369,10 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{show ? (
|
{show ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute -top-[36px] left-0 right-0 z-20 bg-white"
|
className="flex w-full items-center justify-between bg-transparent"
|
||||||
initial={{ y: 20, opacity: 0, zIndex: 0 }}
|
initial={{ opacity: 0, zIndex: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, zIndex: 20 }}
|
||||||
exit={{ y: 43, zIndex: 0 }}
|
exit={{ opacity: 0, zIndex: 0 }}
|
||||||
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useResizeObserver } from "usehooks-ts";
|
import { useResizeObserver } from "usehooks-ts";
|
||||||
|
|
||||||
|
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||||
|
import Actionbar from "@components/ActionBar";
|
||||||
|
import MacroBar from "@/components/MacroBar";
|
||||||
|
import InfoBar from "@components/InfoBar";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMouseStore,
|
useMouseStore,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUiStore,
|
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
|
||||||
import Actionbar from "@components/ActionBar";
|
|
||||||
import MacroBar from "@/components/MacroBar";
|
|
||||||
import InfoBar from "@components/InfoBar";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HDMIErrorOverlay,
|
HDMIErrorOverlay,
|
||||||
|
@ -67,8 +66,9 @@ export default function WebRTCVideo() {
|
||||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||||
const isVideoLoading = !isPlaying;
|
const isVideoLoading = !isPlaying;
|
||||||
|
|
||||||
|
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||||
|
|
||||||
// Misc states and hooks
|
// Misc states and hooks
|
||||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
// Video-related
|
// Video-related
|
||||||
|
@ -106,8 +106,9 @@ export default function WebRTCVideo() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pointer lock and keyboard lock related
|
// Pointer lock and keyboard lock related
|
||||||
const isPointerLockPossible = window.location.protocol === "https:";
|
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
||||||
|
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||||
|
|
||||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||||
const name = permissionName as PermissionName;
|
const name = permissionName as PermissionName;
|
||||||
const { state } = await navigator.permissions.query({ name });
|
const { state } = await navigator.permissions.query({ name });
|
||||||
|
@ -115,23 +116,47 @@ export default function WebRTCVideo() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const requestPointerLock = useCallback(async () => {
|
const requestPointerLock = useCallback(async () => {
|
||||||
if (document.pointerLockElement) return;
|
if (!isPointerLockPossible
|
||||||
|
|| videoElm.current === null
|
||||||
|
|| document.pointerLockElement) return;
|
||||||
|
|
||||||
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
||||||
|
|
||||||
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
||||||
videoElm.current?.requestPointerLock();
|
await videoElm.current.requestPointerLock();
|
||||||
}
|
}
|
||||||
}, [checkNavigatorPermissions, settings.mouseMode]);
|
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
|
||||||
|
|
||||||
|
const requestKeyboardLock = useCallback(async () => {
|
||||||
|
if (videoElm.current === null) return;
|
||||||
|
|
||||||
|
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||||
|
if (isKeyboardLockGranted) {
|
||||||
|
if ("keyboard" in navigator) {
|
||||||
|
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||||
|
await navigator.keyboard.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [checkNavigatorPermissions]);
|
||||||
|
|
||||||
|
const releaseKeyboardLock = useCallback(async () => {
|
||||||
|
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
||||||
|
|
||||||
|
if ("keyboard" in navigator) {
|
||||||
|
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||||
|
await navigator.keyboard.unlock();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPointerLockPossible || !videoElm.current) return;
|
if (!isPointerLockPossible || !videoElm.current) return;
|
||||||
|
|
||||||
const handlePointerLockChange = () => {
|
const handlePointerLockChange = () => {
|
||||||
if (document.pointerLockElement) {
|
if (document.pointerLockElement) {
|
||||||
notifications.success("Pointer lock Enabled, hold escape to exit");
|
notifications.success("Pointer lock Enabled, press escape to unlock");
|
||||||
setIsPointerLockActive(true);
|
setIsPointerLockActive(true);
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Pointer lock disabled");
|
notifications.success("Pointer lock Disabled");
|
||||||
setIsPointerLockActive(false);
|
setIsPointerLockActive(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -144,27 +169,39 @@ export default function WebRTCVideo() {
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [isPointerLockPossible, videoElm]);
|
}, [isPointerLockPossible]);
|
||||||
|
|
||||||
const requestFullscreen = useCallback(async () => {
|
const requestFullscreen = useCallback(async () => {
|
||||||
videoElm.current?.requestFullscreen({
|
if (!isFullscreenEnabled || !videoElm.current) return;
|
||||||
navigationUI: "show",
|
|
||||||
});
|
|
||||||
|
|
||||||
// we do not care about pointer lock if it's for fullscreen
|
// per https://wicg.github.io/keyboard-lock/#system-key-press-handler
|
||||||
|
// If keyboard lock is activated after fullscreen is already in effect, then the user my
|
||||||
|
// see multiple messages about how to exit fullscreen. For this reason, we recommend that
|
||||||
|
// developers call lock() before they enter fullscreen:
|
||||||
|
await requestKeyboardLock();
|
||||||
await requestPointerLock();
|
await requestPointerLock();
|
||||||
|
|
||||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
await videoElm.current.requestFullscreen({
|
||||||
if (isKeyboardLockGranted) {
|
navigationUI: "show",
|
||||||
if ("keyboard" in navigator) {
|
});
|
||||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
}, [isFullscreenEnabled, requestKeyboardLock, requestPointerLock]);
|
||||||
await navigator.keyboard.lock();
|
|
||||||
|
// setup to release the keyboard lock anytime the fullscreen ends
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
releaseKeyboardLock();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [requestPointerLock, checkNavigatorPermissions]);
|
|
||||||
|
document.addEventListener("fullscreenchange ", handleFullscreenChange);
|
||||||
|
}, [releaseKeyboardLock]);
|
||||||
|
|
||||||
// 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(
|
||||||
(x: number, y: number, buttons: number) => {
|
(x: number, y: number, buttons: number) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (settings.mouseMode !== "relative") return;
|
||||||
|
@ -179,18 +216,13 @@ export default function WebRTCVideo() {
|
||||||
const relMouseMoveHandler = useCallback(
|
const relMouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (settings.mouseMode !== "relative") return;
|
||||||
if (isPointerLockActive === false && isPointerLockPossible === true) return;
|
if (isPointerLockActive === false && isPointerLockPossible) return;
|
||||||
|
|
||||||
// Send mouse movement
|
// Send mouse movement
|
||||||
const { buttons } = e;
|
const { buttons } = e;
|
||||||
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
||||||
},
|
},
|
||||||
[
|
[isPointerLockActive, isPointerLockPossible, sendRelMouseMovement, settings.mouseMode],
|
||||||
isPointerLockActive,
|
|
||||||
isPointerLockPossible,
|
|
||||||
sendRelMouseMovement,
|
|
||||||
settings.mouseMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendAbsMouseMovement = useCallback(
|
const sendAbsMouseMovement = useCallback(
|
||||||
|
@ -244,18 +276,16 @@ export default function WebRTCVideo() {
|
||||||
const { buttons } = e;
|
const { buttons } = e;
|
||||||
sendAbsMouseMovement(x, y, buttons);
|
sendAbsMouseMovement(x, y, buttons);
|
||||||
},
|
},
|
||||||
[
|
[settings.mouseMode, videoClientWidth, videoClientHeight, videoWidth, videoHeight, sendAbsMouseMovement],
|
||||||
sendAbsMouseMovement,
|
|
||||||
videoClientHeight,
|
|
||||||
videoClientWidth,
|
|
||||||
videoWidth,
|
|
||||||
videoHeight,
|
|
||||||
settings.mouseMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseWheelHandler = useCallback(
|
const mouseWheelHandler = useCallback(
|
||||||
(e: WheelEvent) => {
|
(e: WheelEvent) => {
|
||||||
|
|
||||||
|
if (settings.scrollThrottling && blockWheelEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if the wheel event is an accel scroll value
|
// Determine if the wheel event is an accel scroll value
|
||||||
const isAccel = Math.abs(e.deltaY) >= 100;
|
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||||
|
|
||||||
|
@ -263,7 +293,7 @@ export default function WebRTCVideo() {
|
||||||
const accelScrollValue = e.deltaY / 100;
|
const accelScrollValue = e.deltaY / 100;
|
||||||
|
|
||||||
// Calculate the no accel scroll value
|
// Calculate the no accel scroll value
|
||||||
const noAccelScrollValue = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
|
const noAccelScrollValue = Math.sign(e.deltaY);
|
||||||
|
|
||||||
// Get scroll value
|
// Get scroll value
|
||||||
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||||
|
@ -275,8 +305,14 @@ export default function WebRTCVideo() {
|
||||||
const invertedScrollValue = -clampedScrollValue;
|
const invertedScrollValue = -clampedScrollValue;
|
||||||
|
|
||||||
send("wheelReport", { wheelY: invertedScrollValue });
|
send("wheelReport", { wheelY: invertedScrollValue });
|
||||||
|
|
||||||
|
// Apply blocking delay based of throttling settings
|
||||||
|
if (settings.scrollThrottling && !blockWheelEvent) {
|
||||||
|
setBlockWheelEvent(true);
|
||||||
|
setTimeout(() => setBlockWheelEvent(false), settings.scrollThrottling);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[send],
|
[send, blockWheelEvent, settings],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetMousePosition = useCallback(() => {
|
const resetMousePosition = useCallback(() => {
|
||||||
|
@ -356,13 +392,6 @@ export default function WebRTCVideo() {
|
||||||
let code = e.code;
|
let code = e.code;
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
|
|
||||||
// if (document.activeElement?.id !== "videoFocusTrap") {
|
|
||||||
// console.log("KEYUP: Not focusing on the video", document.activeElement);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log(document.activeElement);
|
|
||||||
|
|
||||||
if (!isKeyboardLedManagedByHost) {
|
if (!isKeyboardLedManagedByHost) {
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
|
@ -440,13 +469,15 @@ export default function WebRTCVideo() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
|
||||||
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
||||||
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
||||||
// Fix only works in chrome based browsers.
|
// Fix only works in chrome based browsers.
|
||||||
if (e.code === "Space") {
|
if (e.code === "Space") {
|
||||||
if (videoElm.current?.paused == true) {
|
if (videoElm.current.paused) {
|
||||||
console.log("Force playing video");
|
console.log("Force playing video");
|
||||||
videoElm.current?.play();
|
videoElm.current.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -455,7 +486,6 @@ export default function WebRTCVideo() {
|
||||||
(mediaStream: MediaStream) => {
|
(mediaStream: MediaStream) => {
|
||||||
if (!videoElm.current) return;
|
if (!videoElm.current) return;
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
// console.log("Adding stream to video element", videoElmRefValue);
|
|
||||||
videoElmRefValue.srcObject = mediaStream;
|
videoElmRefValue.srcObject = mediaStream;
|
||||||
updateVideoSizeStore(videoElmRefValue);
|
updateVideoSizeStore(videoElmRefValue);
|
||||||
},
|
},
|
||||||
|
@ -471,7 +501,6 @@ export default function WebRTCVideo() {
|
||||||
peerConnection.addEventListener(
|
peerConnection.addEventListener(
|
||||||
"track",
|
"track",
|
||||||
(e: RTCTrackEvent) => {
|
(e: RTCTrackEvent) => {
|
||||||
// console.log("Adding stream to video element");
|
|
||||||
addStreamToVideoElm(e.streams[0]);
|
addStreamToVideoElm(e.streams[0]);
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
|
@ -487,7 +516,6 @@ export default function WebRTCVideo() {
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateVideoStream() {
|
function updateVideoStream() {
|
||||||
if (!mediaStream) return;
|
if (!mediaStream) return;
|
||||||
console.log("Updating video stream from mediaStream");
|
|
||||||
// We set the as early as possible
|
// We set the as early as possible
|
||||||
addStreamToVideoElm(mediaStream);
|
addStreamToVideoElm(mediaStream);
|
||||||
},
|
},
|
||||||
|
@ -509,9 +537,6 @@ export default function WebRTCVideo() {
|
||||||
document.addEventListener("keydown", keyDownHandler, { signal });
|
document.addEventListener("keydown", keyDownHandler, { signal });
|
||||||
document.addEventListener("keyup", keyUpHandler, { signal });
|
document.addEventListener("keyup", keyUpHandler, { signal });
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
window.clearKeys = () => sendKeyboardEvent([], []);
|
|
||||||
window.addEventListener("blur", resetKeyboardState, { signal });
|
window.addEventListener("blur", resetKeyboardState, { signal });
|
||||||
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
||||||
|
|
||||||
|
@ -519,7 +544,7 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
|
[keyDownHandler, keyUpHandler, resetKeyboardState],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Video Event Listeners
|
// Setup Video Event Listeners
|
||||||
|
@ -541,38 +566,42 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[onVideoPlaying, videoKeyUpHandler],
|
||||||
absMouseMoveHandler,
|
|
||||||
resetMousePosition,
|
|
||||||
onVideoPlaying,
|
|
||||||
mouseWheelHandler,
|
|
||||||
videoKeyUpHandler,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Absolute Mouse Events
|
// Setup Mouse Events
|
||||||
useEffect(
|
useEffect(
|
||||||
function setAbsoluteMouseModeEventListeners() {
|
function setMouseModeEventListeners() {
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
if (!videoElmRefValue) return;
|
if (!videoElmRefValue) return;
|
||||||
|
const isRelativeMouseMode = (settings.mouseMode === "relative");
|
||||||
if (settings.mouseMode !== "absolute") return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = abortController.signal;
|
const signal = abortController.signal;
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler :absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||||
signal,
|
signal,
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset the mouse position when the window is blurred or the document is hidden
|
if (isRelativeMouseMode) {
|
||||||
const local = resetMousePosition;
|
videoElmRefValue.addEventListener("click",
|
||||||
window.addEventListener("blur", local, { signal });
|
() => {
|
||||||
document.addEventListener("visibilitychange", local, { signal });
|
if (isPointerLockPossible && !isPointerLockActive && !document.pointerLockElement) {
|
||||||
|
requestPointerLock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Reset the mouse position when the window is blurred or the document is hidden
|
||||||
|
window.addEventListener("blur", resetMousePosition, { signal });
|
||||||
|
document.addEventListener("visibilitychange", resetMousePosition, { signal });
|
||||||
|
}
|
||||||
|
|
||||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
||||||
|
|
||||||
|
@ -580,65 +609,18 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[absMouseMoveHandler, mouseWheelHandler, resetMousePosition, settings.mouseMode],
|
[absMouseMoveHandler, isPointerLockActive, isPointerLockPossible, mouseWheelHandler, relMouseMoveHandler, requestPointerLock, resetMousePosition, settings.mouseMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Relative Mouse Events
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
function setupRelativeMouseEventListeners() {
|
|
||||||
if (settings.mouseMode !== "relative") return;
|
|
||||||
// Relative mouse mode should only be active if the pointer lock is active and Pointer Lock is possible
|
|
||||||
|
|
||||||
const videoElmRefValue = videoElm.current;
|
|
||||||
if (!videoElmRefValue) return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const signal = abortController.signal;
|
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener("pointerdown", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener("pointerup", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener(
|
|
||||||
"click",
|
|
||||||
() => {
|
|
||||||
if (isPointerLockPossible && !document.pointerLockElement) {
|
|
||||||
requestPointerLock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ signal },
|
|
||||||
);
|
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
|
||||||
signal,
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
|
||||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[
|
|
||||||
settings.mouseMode,
|
|
||||||
relMouseMoveHandler,
|
|
||||||
mouseWheelHandler,
|
|
||||||
disableVideoFocusTrap,
|
|
||||||
requestPointerLock,
|
|
||||||
isPointerLockPossible,
|
|
||||||
isPointerLockActive,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasNoAutoPlayPermissions = useMemo(() => {
|
const hasNoAutoPlayPermissions = useMemo(() => {
|
||||||
if (peerConnection?.connectionState !== "connected") return false;
|
if (peerConnection?.connectionState !== "connected") return false;
|
||||||
if (isPlaying) return false;
|
if (isPlaying) return false;
|
||||||
if (hdmiError) return false;
|
if (hdmiError) return false;
|
||||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
|
}, [hdmiError, isPlaying, peerConnection?.connectionState, videoHeight, videoWidth]);
|
||||||
|
|
||||||
const showPointerLockBar = useMemo(() => {
|
const showPointerLockBar = useMemo(() => {
|
||||||
if (settings.mouseMode !== "relative") return false;
|
if (settings.mouseMode !== "relative") return false;
|
||||||
|
@ -648,15 +630,7 @@ export default function WebRTCVideo() {
|
||||||
if (!isPlaying) return false;
|
if (!isPlaying) return false;
|
||||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}, [
|
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
||||||
settings.mouseMode,
|
|
||||||
isPointerLockPossible,
|
|
||||||
isPointerLockActive,
|
|
||||||
isVideoLoading,
|
|
||||||
isPlaying,
|
|
||||||
videoHeight,
|
|
||||||
videoWidth,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||||
|
@ -686,10 +660,10 @@ export default function WebRTCVideo() {
|
||||||
<div className="relative grow overflow-hidden">
|
<div className="relative grow overflow-hidden">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
||||||
|
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
||||||
|
<PointerLockBar show={showPointerLockBar} />
|
||||||
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
|
||||||
<PointerLockBar show={showPointerLockBar} />
|
|
||||||
<video
|
<video
|
||||||
ref={videoElm}
|
ref={videoElm}
|
||||||
autoPlay={true}
|
autoPlay={true}
|
||||||
|
|
|
@ -314,6 +314,9 @@ interface SettingsState {
|
||||||
keyboardLedSync: KeyboardLedSync;
|
keyboardLedSync: KeyboardLedSync;
|
||||||
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||||
|
|
||||||
|
scrollThrottling: number;
|
||||||
|
setScrollThrottling: (value: number) => void;
|
||||||
|
|
||||||
showPressedKeys: boolean;
|
showPressedKeys: boolean;
|
||||||
setShowPressedKeys: (show: boolean) => void;
|
setShowPressedKeys: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
|
@ -354,6 +357,9 @@ export const useSettingsStore = create(
|
||||||
keyboardLedSync: "auto",
|
keyboardLedSync: "auto",
|
||||||
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||||
|
|
||||||
|
scrollThrottling: 0,
|
||||||
|
setScrollThrottling: value => set({ scrollThrottling: value }),
|
||||||
|
|
||||||
showPressedKeys: true,
|
showPressedKeys: true,
|
||||||
setShowPressedKeys: show => set({ showPressedKeys: show }),
|
setShowPressedKeys: show => set({ showPressedKeys: show }),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
|
||||||
|
|
||||||
import { Checkbox } from "@/components/Checkbox";
|
import { Checkbox } from "@/components/Checkbox";
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
|
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
|
|
||||||
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
export default function SettingsCtrlAltDelRoute() {
|
export default function SettingsCtrlAltDelRoute() {
|
||||||
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
|
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
|
||||||
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);
|
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
|
|
||||||
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
|
@ -26,6 +27,19 @@ export default function SettingsMouseRoute() {
|
||||||
|
|
||||||
const [jiggler, setJiggler] = useState(false);
|
const [jiggler, setJiggler] = useState(false);
|
||||||
|
|
||||||
|
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
|
||||||
|
const setScrollThrottling = useSettingsStore(
|
||||||
|
state => state.setScrollThrottling,
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollThrottlingOptions = [
|
||||||
|
{ value: "0", label: "Off" },
|
||||||
|
{ value: "10", label: "Low" },
|
||||||
|
{ value: "25", label: "Medium" },
|
||||||
|
{ value: "50", label: "High" },
|
||||||
|
{ value: "100", label: "Very High" },
|
||||||
|
];
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -65,6 +79,21 @@ export default function SettingsMouseRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title="Scroll Throttling"
|
||||||
|
description="Reduce the frequency of scroll events"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
className="max-w-[292px]"
|
||||||
|
value={scrollThrottling}
|
||||||
|
fullWidth
|
||||||
|
onChange={e => setScrollThrottling(parseInt(e.target.value))}
|
||||||
|
options={scrollThrottlingOptions}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Jiggler"
|
title="Jiggler"
|
||||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||||
|
|
Loading…
Reference in New Issue