Adds VNC Console page v1.0
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export type VncConnectionState =
|
||||
| "IDLE"
|
||||
| "CONNECTING_WS"
|
||||
| "CONNECTING_VNC"
|
||||
| "CONNECTED"
|
||||
| "FIRST_FRAME"
|
||||
| "DISCONNECTED"
|
||||
| "ERROR";
|
||||
|
||||
export type UseVncConsoleOptions = {
|
||||
websocketUrl?: string;
|
||||
defaultHost?: string;
|
||||
defaultPort?: number;
|
||||
};
|
||||
|
||||
export type ConnectVncInput = {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const DEFAULT_WEBSOCKET_URL = "ws://localhost:18450/ws/vnc";
|
||||
const DEFAULT_HOST = "198.19.0.176";
|
||||
const DEFAULT_PORT = 5900;
|
||||
|
||||
export function useVncConsole(options: UseVncConsoleOptions = {}) {
|
||||
const websocketUrl = options.websocketUrl ?? DEFAULT_WEBSOCKET_URL;
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
|
||||
const rgbaRef = useRef<Uint8ClampedArray | null>(null);
|
||||
const framebufferRef = useRef({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const [state, setState] = useState<VncConnectionState>("IDLE");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [host, setHost] = useState(options.defaultHost ?? DEFAULT_HOST);
|
||||
const [port, setPort] = useState(options.defaultPort ?? DEFAULT_PORT);
|
||||
const [password, setPassword] = useState("");
|
||||
const [frameSize, setFrameSize] = useState({ width: 0, height: 0 });
|
||||
const [lastFrameAt, setLastFrameAt] = useState<string | null>(null);
|
||||
|
||||
const closeSocket = useCallback(() => {
|
||||
const ws = wsRef.current;
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "disconnect" }));
|
||||
ws.close();
|
||||
} else if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
wsRef.current = null;
|
||||
}, []);
|
||||
|
||||
const clearFrame = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = ctxRef.current;
|
||||
|
||||
if (canvas && ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
rgbaRef.current = null;
|
||||
framebufferRef.current = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
setFrameSize({ width: 0, height: 0 });
|
||||
setLastFrameAt(null);
|
||||
}, []);
|
||||
|
||||
const drawFrame = useCallback((buffer: ArrayBuffer) => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = new DataView(buffer);
|
||||
const width = view.getInt32(0);
|
||||
const height = view.getInt32(4);
|
||||
const pixels = new Uint8ClampedArray(buffer, 8);
|
||||
|
||||
framebufferRef.current = {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
if (!ctxRef.current) {
|
||||
ctxRef.current = canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
desynchronized: true,
|
||||
});
|
||||
}
|
||||
|
||||
const ctx = ctxRef.current;
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
rgbaRef.current = new Uint8ClampedArray(width * height * 4);
|
||||
setFrameSize({ width, height });
|
||||
}
|
||||
|
||||
if (!rgbaRef.current || rgbaRef.current.length !== width * height * 4) {
|
||||
rgbaRef.current = new Uint8ClampedArray(width * height * 4);
|
||||
}
|
||||
|
||||
const rgba = rgbaRef.current;
|
||||
|
||||
for (let index = 0; index < pixels.length; index += 4) {
|
||||
rgba[index] = pixels[index + 2];
|
||||
rgba[index + 1] = pixels[index + 1];
|
||||
rgba[index + 2] = pixels[index];
|
||||
rgba[index + 3] = 255;
|
||||
}
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
imageData.data.set(rgba);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
setLastFrameAt(new Date().toLocaleTimeString());
|
||||
}, []);
|
||||
|
||||
const connect = useCallback(
|
||||
async (input?: Partial<ConnectVncInput>) => {
|
||||
const nextHost = input?.host ?? host;
|
||||
const nextPort = input?.port ?? port;
|
||||
const nextPassword = input?.password ?? password;
|
||||
|
||||
if (!nextHost.trim()) {
|
||||
setError("Host VNC em falta.");
|
||||
setState("ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setState("CONNECTING_WS");
|
||||
|
||||
closeSocket();
|
||||
|
||||
const socket = new WebSocket(websocketUrl);
|
||||
wsRef.current = socket;
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
socket.onopen = () => {
|
||||
setState("CONNECTING_VNC");
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
host: nextHost,
|
||||
port: nextPort,
|
||||
password: nextPassword,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
if (typeof event.data === "string") {
|
||||
const message = JSON.parse(event.data) as {
|
||||
type?: string;
|
||||
state?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
if (message.type === "state") {
|
||||
if (message.state === "CONNECTING") {
|
||||
setState("CONNECTING_VNC");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.state === "CONNECTED") {
|
||||
setState("CONNECTED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.state === "FIRST_FRAME") {
|
||||
setState("FIRST_FRAME");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.state === "DISCONNECTED") {
|
||||
setState("DISCONNECTED");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === "error") {
|
||||
setError(message.message ?? "Erro VNC desconhecido.");
|
||||
setState("ERROR");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
drawFrame(event.data);
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
setError("Erro na ligação WebSocket da consola VNC.");
|
||||
setState("ERROR");
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
wsRef.current = null;
|
||||
clearFrame();
|
||||
|
||||
setState((current) =>
|
||||
current === "ERROR" ? "ERROR" : "DISCONNECTED",
|
||||
);
|
||||
};
|
||||
},
|
||||
[closeSocket, drawFrame, host, password, port, websocketUrl],
|
||||
);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
closeSocket();
|
||||
clearFrame();
|
||||
setState("DISCONNECTED");
|
||||
}, [clearFrame, closeSocket]);
|
||||
|
||||
const sendClick = useCallback((clientX: number, clientY: number) => {
|
||||
const canvas = canvasRef.current;
|
||||
const ws = wsRef.current;
|
||||
const framebuffer = framebufferRef.current;
|
||||
|
||||
if (!canvas || !ws || ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!framebuffer.width || !framebuffer.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
const relativeX = (clientX - rect.left) / rect.width;
|
||||
const relativeY = (clientY - rect.top) / rect.height;
|
||||
|
||||
const x = Math.max(
|
||||
0,
|
||||
Math.min(framebuffer.width - 1, Math.round(relativeX * framebuffer.width)),
|
||||
);
|
||||
|
||||
const y = Math.max(
|
||||
0,
|
||||
Math.min(framebuffer.height - 1, Math.round(relativeY * framebuffer.height)),
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "click",
|
||||
x,
|
||||
y,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleCanvasPointerDown = useCallback(
|
||||
(event: React.PointerEvent) => {
|
||||
event.preventDefault();
|
||||
sendClick(event.clientX, event.clientY);
|
||||
},
|
||||
[sendClick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
closeSocket();
|
||||
};
|
||||
}, [closeSocket]);
|
||||
|
||||
return {
|
||||
canvasRef,
|
||||
state,
|
||||
error,
|
||||
host,
|
||||
setHost,
|
||||
port,
|
||||
setPort,
|
||||
password,
|
||||
setPassword,
|
||||
frameSize,
|
||||
lastFrameAt,
|
||||
connect,
|
||||
disconnect,
|
||||
handleCanvasPointerDown,
|
||||
connected: state === "CONNECTED" || state === "FIRST_FRAME",
|
||||
connecting: state === "CONNECTING_WS" || state === "CONNECTING_VNC",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user