// 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 { 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((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 { 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, 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, }; }