134 lines
4.1 KiB
TypeScript
134 lines
4.1 KiB
TypeScript
// 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,
|
||
};
|
||
}
|