mirror of https://github.com/jetkvm/kvm.git
257 lines
7.2 KiB
TypeScript
257 lines
7.2 KiB
TypeScript
import { useCallback, useEffect, useMemo } from "react";
|
|
|
|
import { useRTCStore } from "@/hooks/stores";
|
|
|
|
import {
|
|
CancelKeyboardMacroReportMessage,
|
|
HID_RPC_VERSION,
|
|
HandshakeMessage,
|
|
KeyboardMacroStep,
|
|
KeyboardMacroReportMessage,
|
|
KeyboardReportMessage,
|
|
KeypressKeepAliveMessage,
|
|
KeypressReportMessage,
|
|
MouseReportMessage,
|
|
PointerReportMessage,
|
|
RpcMessage,
|
|
unmarshalHidRpcMessage,
|
|
} from "./hidRpc";
|
|
|
|
const KEEPALIVE_MESSAGE = new KeypressKeepAliveMessage();
|
|
|
|
interface sendMessageParams {
|
|
ignoreHandshakeState?: boolean;
|
|
useUnreliableChannel?: boolean;
|
|
requireOrdered?: boolean;
|
|
}
|
|
|
|
export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
|
const {
|
|
rpcHidChannel,
|
|
rpcHidUnreliableChannel,
|
|
rpcHidUnreliableNonOrderedChannel,
|
|
setRpcHidProtocolVersion,
|
|
rpcHidProtocolVersion, hidRpcDisabled,
|
|
} = useRTCStore();
|
|
|
|
const rpcHidReady = useMemo(() => {
|
|
if (hidRpcDisabled) return false;
|
|
return rpcHidChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
|
|
}, [rpcHidChannel, rpcHidProtocolVersion, hidRpcDisabled]);
|
|
|
|
const rpcHidUnreliableReady = useMemo(() => {
|
|
return (
|
|
rpcHidUnreliableChannel?.readyState === "open" && rpcHidProtocolVersion !== null
|
|
);
|
|
}, [rpcHidUnreliableChannel, rpcHidProtocolVersion]);
|
|
|
|
const rpcHidUnreliableNonOrderedReady = useMemo(() => {
|
|
return (
|
|
rpcHidUnreliableNonOrderedChannel?.readyState === "open" &&
|
|
rpcHidProtocolVersion !== null
|
|
);
|
|
}, [rpcHidUnreliableNonOrderedChannel, rpcHidProtocolVersion]);
|
|
|
|
const rpcHidStatus = useMemo(() => {
|
|
if (hidRpcDisabled) return "disabled";
|
|
|
|
if (!rpcHidChannel) return "N/A";
|
|
if (rpcHidChannel.readyState !== "open") return rpcHidChannel.readyState;
|
|
if (!rpcHidProtocolVersion) return "handshaking";
|
|
return `ready (v${rpcHidProtocolVersion}${rpcHidUnreliableReady ? "+u" : ""})`;
|
|
}, [rpcHidChannel, rpcHidUnreliableReady, rpcHidProtocolVersion, hidRpcDisabled]);
|
|
|
|
const sendMessage = useCallback(
|
|
(
|
|
message: RpcMessage,
|
|
{
|
|
ignoreHandshakeState,
|
|
useUnreliableChannel,
|
|
requireOrdered = true,
|
|
}: sendMessageParams = {},
|
|
) => {
|
|
if (hidRpcDisabled) return;
|
|
if (rpcHidChannel?.readyState !== "open") return;
|
|
if (!rpcHidReady && !ignoreHandshakeState) return;
|
|
|
|
let data: Uint8Array | undefined;
|
|
try {
|
|
data = message.marshal();
|
|
} catch (e) {
|
|
console.error("Failed to send HID RPC message", e);
|
|
}
|
|
if (!data) return;
|
|
|
|
if (useUnreliableChannel) {
|
|
if (requireOrdered && rpcHidUnreliableReady) {
|
|
rpcHidUnreliableChannel?.send(data as unknown as ArrayBuffer);
|
|
} else if (!requireOrdered && rpcHidUnreliableNonOrderedReady) {
|
|
rpcHidUnreliableNonOrderedChannel?.send(data as unknown as ArrayBuffer);
|
|
}
|
|
return;
|
|
}
|
|
|
|
rpcHidChannel?.send(data as unknown as ArrayBuffer);
|
|
},
|
|
[
|
|
rpcHidChannel,
|
|
rpcHidUnreliableChannel,
|
|
hidRpcDisabled, rpcHidUnreliableNonOrderedChannel,
|
|
rpcHidReady,
|
|
rpcHidUnreliableReady,
|
|
rpcHidUnreliableNonOrderedReady,
|
|
],
|
|
);
|
|
|
|
const reportKeyboardEvent = useCallback(
|
|
(keys: number[], modifier: number) => {
|
|
sendMessage(new KeyboardReportMessage(keys, modifier));
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const reportKeypressEvent = useCallback(
|
|
(key: number, press: boolean) => {
|
|
sendMessage(new KeypressReportMessage(key, press));
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const reportAbsMouseEvent = useCallback(
|
|
(x: number, y: number, buttons: number) => {
|
|
sendMessage(new PointerReportMessage(x, y, buttons), {
|
|
useUnreliableChannel: true,
|
|
});
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const reportRelMouseEvent = useCallback(
|
|
(dx: number, dy: number, buttons: number) => {
|
|
sendMessage(new MouseReportMessage(dx, dy, buttons));
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const reportKeyboardMacroEvent = useCallback(
|
|
(steps: KeyboardMacroStep[]) => {
|
|
sendMessage(new KeyboardMacroReportMessage(false, steps.length, steps));
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const cancelOngoingKeyboardMacro = useCallback(
|
|
() => {
|
|
sendMessage(new CancelKeyboardMacroReportMessage());
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
const reportKeypressKeepAlive = useCallback(() => {
|
|
sendMessage(KEEPALIVE_MESSAGE);
|
|
}, [sendMessage]);
|
|
|
|
const sendHandshake = useCallback(() => {
|
|
if (hidRpcDisabled) return;
|
|
if (rpcHidProtocolVersion) return;
|
|
if (!rpcHidChannel) return;
|
|
|
|
sendMessage(new HandshakeMessage(HID_RPC_VERSION), { ignoreHandshakeState: true });
|
|
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage, hidRpcDisabled]);
|
|
|
|
const handleHandshake = useCallback(
|
|
(message: HandshakeMessage) => {
|
|
if (hidRpcDisabled) return;
|
|
|
|
if (!message.version) {
|
|
console.error("Received handshake message without version", message);
|
|
return;
|
|
}
|
|
|
|
if (message.version > HID_RPC_VERSION) {
|
|
// we assume that the UI is always using the latest version of the HID RPC protocol
|
|
// so we can't support this
|
|
// TODO: use capabilities to determine rather than version number
|
|
console.error("Server is using a newer HID RPC version than the client", message);
|
|
return;
|
|
}
|
|
|
|
setRpcHidProtocolVersion(message.version);
|
|
},
|
|
[setRpcHidProtocolVersion, hidRpcDisabled],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!rpcHidChannel) return;
|
|
if (hidRpcDisabled) return;
|
|
|
|
// send handshake message
|
|
sendHandshake();
|
|
|
|
const messageHandler = (e: MessageEvent) => {
|
|
if (typeof e.data === "string") {
|
|
console.warn("Received string data in HID RPC message handler", e.data);
|
|
return;
|
|
}
|
|
|
|
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
|
if (!message) {
|
|
console.warn("Received invalid HID RPC message", e.data);
|
|
return;
|
|
}
|
|
|
|
console.debug("Received HID RPC message", message);
|
|
switch (message.constructor) {
|
|
case HandshakeMessage:
|
|
handleHandshake(message as HandshakeMessage);
|
|
break;
|
|
default:
|
|
// not all events are handled here, the rest are handled by the onHidRpcMessage callback
|
|
break;
|
|
}
|
|
|
|
onHidRpcMessage?.(message);
|
|
};
|
|
|
|
const openHandler = () => {
|
|
console.warn("HID RPC channel opened");
|
|
sendHandshake();
|
|
};
|
|
|
|
const closeHandler = () => {
|
|
console.warn("HID RPC channel closed");
|
|
setRpcHidProtocolVersion(null);
|
|
};
|
|
|
|
rpcHidChannel.addEventListener("message", messageHandler);
|
|
rpcHidChannel.addEventListener("close", closeHandler);
|
|
rpcHidChannel.addEventListener("open", openHandler);
|
|
|
|
return () => {
|
|
rpcHidChannel.removeEventListener("message", messageHandler);
|
|
rpcHidChannel.removeEventListener("close", closeHandler);
|
|
rpcHidChannel.removeEventListener("open", openHandler);
|
|
};
|
|
}, [
|
|
rpcHidChannel,
|
|
onHidRpcMessage,
|
|
setRpcHidProtocolVersion,
|
|
sendHandshake,
|
|
handleHandshake,
|
|
hidRpcDisabled,
|
|
]);
|
|
|
|
return {
|
|
reportKeyboardEvent,
|
|
reportKeypressEvent,
|
|
reportAbsMouseEvent,
|
|
reportRelMouseEvent,
|
|
reportKeyboardMacroEvent,
|
|
cancelOngoingKeyboardMacro,
|
|
reportKeypressKeepAlive,
|
|
rpcHidProtocolVersion,
|
|
rpcHidReady,
|
|
rpcHidStatus,
|
|
};
|
|
}
|