Files
aiops/frontend/src/components/server/rdp-dialog/rdp-utils.ts
T

134 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// RDP 远程桌面弹窗 — 纯函数(视口计算 + 位图绘制 + 二进制帧解析)
import type { KeyboardEvent } from "react";
import type {
KeyModifierState,
RdpFrame,
RdpRawFrame,
} from "./types";
import {
RDP_BINARY_HEADER,
RDP_DEFAULT_HEIGHT,
RDP_DEFAULT_WIDTH,
RDP_HEADER_HEIGHT,
RDP_MAX_HEIGHT,
RDP_MAX_WIDTH,
RDP_MIN_HEIGHT,
RDP_MIN_WIDTH,
} from "./types";
/** 将分辨率约束到 [min, max] 且为偶数(RDP 协议要求偶数宽高)。 */
export function clampRdpSize(value: number, min: number, max: number) {
const clamped = Math.max(min, Math.min(max, value));
return clamped % 2 === 0 ? clamped : clamped - 1;
}
/** 根据窗口大小 + 全屏状态计算远程桌面最佳分辨率。 */
export function calcRdpViewport(fullscreen: boolean) {
if (!fullscreen) {
// 13 寸屏默认保留外层操作空间,最大化时再提升远端分辨率。 // Logan.Li
return {
width: Math.min(
RDP_DEFAULT_WIDTH,
clampRdpSize(
Math.floor(window.innerWidth * 0.78),
RDP_MIN_WIDTH,
RDP_DEFAULT_WIDTH,
),
),
height: Math.min(
RDP_DEFAULT_HEIGHT,
clampRdpSize(
Math.floor(window.innerHeight - 180),
RDP_MIN_HEIGHT,
RDP_DEFAULT_HEIGHT,
),
),
};
}
const maxWidth = window.innerWidth;
const maxHeight = window.innerHeight - RDP_HEADER_HEIGHT;
return {
width: clampRdpSize(Math.floor(maxWidth), RDP_MIN_WIDTH, RDP_MAX_WIDTH),
height: clampRdpSize(
Math.floor(maxHeight),
RDP_MIN_HEIGHT,
RDP_MAX_HEIGHT,
),
};
}
/** 将 dataURL/bitmap 绘制到 canvas 上下文(先试 createImageBitmap,失败回退 Image.onload)。 */
export async function paintBitmap(
ctx: CanvasRenderingContext2D,
frame: RdpFrame,
): Promise<void> {
const dx = frame.x || 0;
const dy = frame.y || 0;
const dw = frame.width || 0;
const dh = frame.height || 0;
try {
if ("createImageBitmap" in window && frame.image) {
const blob = await fetch(frame.image).then((resp) => resp.blob());
const bitmap = await createImageBitmap(blob);
ctx.drawImage(bitmap, dx, dy, dw || bitmap.width, dh || bitmap.height);
bitmap.close();
return;
}
} catch {
// dataURL 在个别浏览器上可能解码失败,回退到 Image.onload 路径 // Logan.Li
}
await new Promise<void>((resolve, reject) => {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, dx, dy, dw || img.width, dh || img.height);
resolve();
};
img.onerror = () => reject(new Error("bitmap decode failed"));
img.src = frame.image || "";
});
}
/** 解析 RDP 二进制帧(magic=RDPR,4 字节 + x/y/width/height 各 4 字节 + RGBA 像素)。 */
export async function parseRawFrame(
data: Blob | ArrayBuffer,
): Promise<RdpRawFrame | null> {
const buffer = data instanceof Blob ? await data.arrayBuffer() : data;
if (buffer.byteLength < RDP_BINARY_HEADER) return null;
const view = new DataView(buffer);
const magic =
String.fromCharCode(view.getUint8(0)) +
String.fromCharCode(view.getUint8(1)) +
String.fromCharCode(view.getUint8(2)) +
String.fromCharCode(view.getUint8(3));
if (magic !== "RDPR") return null;
const x = view.getUint32(4, true);
const y = view.getUint32(8, true);
const width = view.getUint32(12, true);
const height = view.getUint32(16, true);
const bytes = width * height * 4;
if (width <= 0 || height <= 0 || buffer.byteLength < RDP_BINARY_HEADER + bytes)
return null;
return {
x,
y,
width,
height,
rgba: new Uint8ClampedArray(buffer, RDP_BINARY_HEADER, bytes),
};
}
/** 从 keydown/keyup 事件提取修饰键状态(keyup 经常丢失 ctrlKey/metaKey,用 ref 补全)。 */
export function readModifiers(
e: KeyboardEvent<HTMLElement>,
fallback: KeyModifierState,
): KeyModifierState {
// keyup 事件经常丢失 ctrlKey/metaKey,使用本地状态保证组合键释放也能发到远端。 // Logan.Li
return {
ctrl: fallback.ctrl || e.ctrlKey,
alt: fallback.alt || e.altKey,
shift: fallback.shift || e.shiftKey,
meta: fallback.meta || e.metaKey,
};
}