real cursor shit
This commit is contained in:
@@ -63,6 +63,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const mouseCtrl = document.getElementById("mouseCtrl");
|
||||
const kbdCtrl = document.getElementById("kbdCtrl");
|
||||
const cursorCtrl = document.getElementById("cursorCtrl");
|
||||
const hideLocalCursorCtrl = document.getElementById("hideLocalCursorCtrl");
|
||||
const duplicationCtrl = document.getElementById("duplicationCtrl");
|
||||
const streamProfileSelect = document.getElementById("streamProfileSelect");
|
||||
const streamProfileDetail = document.getElementById("streamProfileDetail");
|
||||
@@ -76,6 +77,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
const codecMode = document.getElementById("codecMode");
|
||||
const canvas = document.getElementById("frameCanvas");
|
||||
const canvasContainer = document.getElementById("canvasContainer");
|
||||
const renderSurface = document.getElementById("renderSurface");
|
||||
const remoteCursor = document.getElementById("remoteCursor");
|
||||
const remoteCursorImage = document.getElementById("remoteCursorImage");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const agentFps = document.getElementById("agentFps");
|
||||
const viewerFps = document.getElementById("viewerFps");
|
||||
@@ -97,6 +101,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
let whepClient = null;
|
||||
let p2pClient = null;
|
||||
let webrtcActive = false;
|
||||
let lastRemoteCursor = null;
|
||||
let remoteCursorShape = null;
|
||||
let remoteCursorImageUrl = "";
|
||||
function getWebrtcMode() {
|
||||
return webrtcMode ? String(webrtcMode.value || "off") : "off";
|
||||
}
|
||||
@@ -462,6 +469,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
if (mouseCtrl && typeof settings.mouse === "boolean") mouseCtrl.checked = settings.mouse;
|
||||
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
|
||||
if (cursorCtrl && typeof settings.cursor === "boolean") cursorCtrl.checked = settings.cursor;
|
||||
if (hideLocalCursorCtrl && typeof settings.hideLocalCursor === "boolean") hideLocalCursorCtrl.checked = settings.hideLocalCursor;
|
||||
if (duplicationCtrl && typeof settings.duplication === "boolean") duplicationCtrl.checked = settings.duplication;
|
||||
if (softwareH264Ctrl && typeof settings.softwareH264 === "boolean") softwareH264Ctrl.checked = settings.softwareH264;
|
||||
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
||||
@@ -488,6 +496,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
mouse: !!mouseCtrl?.checked,
|
||||
keyboard: !!kbdCtrl?.checked,
|
||||
cursor: !!cursorCtrl?.checked,
|
||||
hideLocalCursor: !!hideLocalCursorCtrl?.checked,
|
||||
duplication: !!duplicationCtrl?.checked,
|
||||
softwareH264: !!softwareH264Ctrl?.checked,
|
||||
clipboardSync: !!clipboardSyncCtrl?.checked,
|
||||
@@ -540,6 +549,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
|
||||
function setStreamState(state, text) {
|
||||
streamState = state;
|
||||
if (state !== "streaming" && state !== "stalled") {
|
||||
lastRemoteCursor = null;
|
||||
renderRemoteCursor();
|
||||
}
|
||||
if (statusEl) {
|
||||
const icons = {
|
||||
connecting: '<i class="fa-solid fa-circle-notch fa-spin"></i>',
|
||||
@@ -1259,6 +1272,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
webrtcActive = !!active;
|
||||
if (canvas) canvas.style.display = active ? "none" : "block";
|
||||
if (webrtcVideo) webrtcVideo.style.display = active ? "block" : "none";
|
||||
renderRemoteCursor();
|
||||
if (!active && networkStats) {
|
||||
networkStats.textContent = "--";
|
||||
networkStats.title = "";
|
||||
@@ -1659,10 +1673,20 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
streamProfileSelect.appendChild(option);
|
||||
}
|
||||
if (streamProfileSelect.options.length === 1) {
|
||||
const manualHighResolutionProfiles = [
|
||||
{ maxHeight: 1440, fps: 30, label: "30 FPS - 1440p" },
|
||||
{ maxHeight: 1440, fps: 60, label: "60 FPS - 1440p" },
|
||||
{ maxHeight: 2160, fps: 30, label: "30 FPS - 2160p (4K)" },
|
||||
{ maxHeight: 2160, fps: 60, label: "60 FPS - 2160p (4K)" },
|
||||
];
|
||||
const existingProfiles = new Set(Array.from(streamProfileSelect.options, (option) => option.value));
|
||||
for (const profile of manualHighResolutionProfiles) {
|
||||
const value = `${profile.maxHeight}:${profile.fps}`;
|
||||
if (existingProfiles.has(value)) continue;
|
||||
const option = document.createElement("option");
|
||||
option.value = "1080:60";
|
||||
option.textContent = "60 FPS - 1080p";
|
||||
option.value = value;
|
||||
option.textContent = profile.label;
|
||||
option.title = "Manual high-resolution profile";
|
||||
streamProfileSelect.appendChild(option);
|
||||
}
|
||||
if (!setSelectValue(streamProfileSelect, savedStreamProfile || "auto") &&
|
||||
@@ -1817,14 +1841,69 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderRemoteCursor(message = lastRemoteCursor) {
|
||||
lastRemoteCursor = message;
|
||||
const image = message?.image;
|
||||
if (remoteCursorImage && image instanceof Uint8Array && image.byteLength > 0) {
|
||||
if (remoteCursorImageUrl) URL.revokeObjectURL(remoteCursorImageUrl);
|
||||
remoteCursorImageUrl = URL.createObjectURL(new Blob([image], { type: "image/png" }));
|
||||
remoteCursorImage.src = remoteCursorImageUrl;
|
||||
remoteCursorShape = {
|
||||
width: Math.max(1, Number(message.cursorWidth) || 1),
|
||||
height: Math.max(1, Number(message.cursorHeight) || 1),
|
||||
hotspotX: Math.max(0, Number(message.hotspotX) || 0),
|
||||
hotspotY: Math.max(0, Number(message.hotspotY) || 0),
|
||||
};
|
||||
}
|
||||
const hideLocalCursor = !!hideLocalCursorCtrl?.checked;
|
||||
const showRemoteCursor = !!cursorCtrl?.checked && !!message?.visible && !!remoteCursorShape &&
|
||||
(!mouseCtrl?.checked || hideLocalCursor);
|
||||
const localCursor = hideLocalCursor ? "none" : "default";
|
||||
canvas.style.cursor = localCursor;
|
||||
if (webrtcVideo) webrtcVideo.style.cursor = localCursor;
|
||||
if (!remoteCursor || !renderSurface || !showRemoteCursor) {
|
||||
if (remoteCursor) remoteCursor.hidden = true;
|
||||
return;
|
||||
}
|
||||
const surface = webrtcActive ? webrtcVideo : canvas;
|
||||
const sourceWidth = Number(message.width) || (surface === webrtcVideo ? webrtcVideo.videoWidth : canvas.width);
|
||||
const sourceHeight = Number(message.height) || (surface === webrtcVideo ? webrtcVideo.videoHeight : canvas.height);
|
||||
if (!sourceWidth || !sourceHeight) {
|
||||
remoteCursor.hidden = true;
|
||||
return;
|
||||
}
|
||||
const surfaceRect = surface.getBoundingClientRect();
|
||||
const containerRect = renderSurface.getBoundingClientRect();
|
||||
const scaleX = surfaceRect.width / sourceWidth;
|
||||
const scaleY = surfaceRect.height / sourceHeight;
|
||||
const x = surfaceRect.left - containerRect.left + (Number(message.x) || 0) * scaleX -
|
||||
remoteCursorShape.hotspotX * scaleX;
|
||||
const y = surfaceRect.top - containerRect.top + (Number(message.y) || 0) * scaleY -
|
||||
remoteCursorShape.hotspotY * scaleY;
|
||||
remoteCursor.style.width = `${remoteCursorShape.width * scaleX}px`;
|
||||
remoteCursor.style.height = `${remoteCursorShape.height * scaleY}px`;
|
||||
remoteCursor.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
||||
remoteCursor.hidden = false;
|
||||
}
|
||||
|
||||
function updateCursorPresentation() {
|
||||
renderRemoteCursor();
|
||||
}
|
||||
|
||||
function pushCaptureToggles() {
|
||||
if (cursorCtrl) {
|
||||
sendCmd("desktop_enable_cursor", { enabled: cursorCtrl.checked });
|
||||
updateCursorPresentation();
|
||||
sendCmd("desktop_enable_cursor", { enabled: !!cursorCtrl.checked });
|
||||
}
|
||||
if (duplicationCtrl && !duplicationCtrl.disabled) {
|
||||
sendCmd("desktop_set_duplication", { enabled: !!duplicationCtrl.checked });
|
||||
}
|
||||
}
|
||||
updateCursorPresentation();
|
||||
if (renderSurface && typeof ResizeObserver === "function") {
|
||||
new ResizeObserver(() => renderRemoteCursor()).observe(renderSurface);
|
||||
}
|
||||
|
||||
function pushPrivacyToggle() {
|
||||
if (!privacyCtrl) return;
|
||||
@@ -1844,6 +1923,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
|
||||
mouseCtrl.addEventListener("change", function () {
|
||||
pushInputToggles();
|
||||
updateCursorPresentation();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
canvasContainer.setAttribute("tabindex", "0");
|
||||
@@ -1876,6 +1956,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
pushCaptureToggles();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
if (hideLocalCursorCtrl) {
|
||||
hideLocalCursorCtrl.addEventListener("change", function () {
|
||||
updateCursorPresentation();
|
||||
sharedSettingsSaver.scheduleSave();
|
||||
});
|
||||
}
|
||||
if (duplicationCtrl) {
|
||||
duplicationCtrl.addEventListener("change", function () {
|
||||
pushCaptureToggles();
|
||||
@@ -2483,6 +2569,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
const msg = decodeMsgpack(buf);
|
||||
if (msg && msg.type === "desktop_cursor") {
|
||||
renderRemoteCursor(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "desktop_encoder_capabilities") {
|
||||
applyEncoderCapabilities(msg);
|
||||
return;
|
||||
@@ -2526,6 +2616,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
||||
}
|
||||
|
||||
const msg = decodeMsgpack(ev.data);
|
||||
if (msg && msg.type === "desktop_cursor") {
|
||||
renderRemoteCursor(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "desktop_encoder_capabilities") {
|
||||
applyEncoderCapabilities(msg);
|
||||
return;
|
||||
|
||||
@@ -187,6 +187,10 @@
|
||||
<option value="720:60">60 FPS - 720p</option>
|
||||
<option value="1080:30">30 FPS - 1080p</option>
|
||||
<option value="1080:60">60 FPS - 1080p</option>
|
||||
<option value="1440:30">30 FPS - 1440p</option>
|
||||
<option value="1440:60">60 FPS - 1440p</option>
|
||||
<option value="2160:30">30 FPS - 2160p (4K)</option>
|
||||
<option value="2160:60">60 FPS - 2160p (4K)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="streamProfileDetail" class="mb-2 text-xs text-slate-500">Checking hardware encoder profiles…</div>
|
||||
@@ -244,9 +248,12 @@
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200">
|
||||
<input type="checkbox" id="kbdCtrl" /> Keyboard
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show the remote cursor without compositing it into captured frames">
|
||||
<input type="checkbox" id="cursorCtrl" /> Cursor
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Hide your browser cursor over the remote desktop">
|
||||
<input type="checkbox" id="hideLocalCursorCtrl" /> Hide Local Cursor
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-slate-200 col-span-2" title="Sync clipboard between your browser and the remote machine">
|
||||
<input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync
|
||||
</label>
|
||||
@@ -274,6 +281,7 @@
|
||||
Client: <span id="clientLabel" class="text-slate-100">-</span>
|
||||
</div>
|
||||
<div
|
||||
id="renderSurface"
|
||||
class="relative flex-1 flex items-center justify-center p-4"
|
||||
style="min-height: 0; overflow: hidden"
|
||||
>
|
||||
@@ -304,6 +312,9 @@
|
||||
height: auto;
|
||||
"
|
||||
></video>
|
||||
<div id="remoteCursor" hidden aria-hidden="true" class="absolute pointer-events-none" style="left: 0; top: 0; z-index: 30; will-change: transform">
|
||||
<img id="remoteCursorImage" alt="" draggable="false" style="display: block; width: 100%; height: 100%" />
|
||||
</div>
|
||||
<section id="diagnosticsHud" class="rd-diagnostics-hud hidden" aria-label="Streaming diagnostics">
|
||||
<div class="rd-diagnostics-header">
|
||||
<div>
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
handleConsoleOutput,
|
||||
handleDesktopEncoderCapabilities,
|
||||
handleDesktopStreamStats,
|
||||
handleDesktopCursor,
|
||||
handleConsoleViewerMessage,
|
||||
handleConsoleViewerOpen,
|
||||
handlebackstageViewerMessage,
|
||||
@@ -658,6 +659,7 @@ async function startServer() {
|
||||
handleConsoleOutput: (clientId: string, payload: any) => handleConsoleOutput(clientId, payload),
|
||||
handleDesktopEncoderCapabilities: (clientId: string, payload: any) => handleDesktopEncoderCapabilities(clientId, payload),
|
||||
handleDesktopStreamStats: (clientId: string, payload: any) => handleDesktopStreamStats(clientId, payload),
|
||||
handleDesktopCursor: (clientId: string, payload: unknown) => handleDesktopCursor(clientId, payload),
|
||||
handleFileBrowserMessage: (clientId: string, payload: any) =>
|
||||
forwardFileBrowserMessage(clientId, payload, {
|
||||
pendingHttpDownloads,
|
||||
|
||||
@@ -9,6 +9,7 @@ export type MessageKind =
|
||||
| "command_result"
|
||||
| "desktop_encoder_capabilities"
|
||||
| "desktop_stream_stats"
|
||||
| "desktop_cursor"
|
||||
| "client_logs_result"
|
||||
| "screenshot_result"
|
||||
| "frame"
|
||||
@@ -224,6 +225,20 @@ export type FrameHeader = {
|
||||
|
||||
export type Frame = { type: "frame"; header: FrameHeader; data: Uint8Array };
|
||||
export type FrameAck = { type: "frame_ack" };
|
||||
export type DesktopCursor = {
|
||||
type: "desktop_cursor";
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
visible: boolean;
|
||||
cursorWidth?: number;
|
||||
cursorHeight?: number;
|
||||
hotspotX?: number;
|
||||
hotspotY?: number;
|
||||
image?: Uint8Array;
|
||||
};
|
||||
|
||||
export type DesktopStreamStats = {
|
||||
type: "desktop_stream_stats";
|
||||
fps: number;
|
||||
@@ -547,6 +562,7 @@ export type WireMessage =
|
||||
| Frame
|
||||
| FrameAck
|
||||
| DesktopStreamStats
|
||||
| DesktopCursor
|
||||
| Status
|
||||
| ConsoleOutput
|
||||
| FileListResult
|
||||
|
||||
@@ -229,6 +229,7 @@ type WsLifecycleDeps = {
|
||||
handleConsoleOutput: (clientId: string, payload: any) => void;
|
||||
handleDesktopEncoderCapabilities: (clientId: string, payload: any) => void;
|
||||
handleDesktopStreamStats: (clientId: string, payload: any) => void;
|
||||
handleDesktopCursor: (clientId: string, payload: unknown) => void;
|
||||
handleFileBrowserMessage: (clientId: string, payload: any) => void;
|
||||
handleProxyTunnelData: (clientId: string, connectionId: string, data: Uint8Array) => void;
|
||||
handleProxyTunnelClose: (clientId: string, connectionId: string) => void;
|
||||
@@ -808,6 +809,9 @@ export async function handleWebSocketMessage(
|
||||
case "desktop_encoder_capabilities":
|
||||
deps.handleDesktopEncoderCapabilities(client.id, payload);
|
||||
break;
|
||||
case "desktop_cursor":
|
||||
deps.handleDesktopCursor(client.id, payload);
|
||||
break;
|
||||
case "desktop_stream_stats":
|
||||
deps.handleDesktopStreamStats(client.id, payload);
|
||||
break;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
handleDesktopStreamStats,
|
||||
handleDesktopCursor,
|
||||
handleDesktopEncoderCapabilities,
|
||||
backstageStreamingState,
|
||||
rdStreamingState,
|
||||
@@ -218,6 +219,41 @@ describe("remote desktop viewer control", () => {
|
||||
expect(message.transport).toBe("webrtc");
|
||||
});
|
||||
|
||||
test("forwards cursor metadata without modifying video frames", () => {
|
||||
const clientId = `rd-cursor-${Date.now().toString(36)}`;
|
||||
createClient(clientId);
|
||||
const viewer = createMockWs({ clientId });
|
||||
handleRemoteDesktopViewerOpen(viewer as unknown as Parameters<typeof handleRemoteDesktopViewerOpen>[0]);
|
||||
|
||||
handleDesktopCursor(clientId, {
|
||||
type: "desktop_cursor",
|
||||
x: 640,
|
||||
y: 360,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
visible: true,
|
||||
cursorWidth: 32,
|
||||
cursorHeight: 32,
|
||||
hotspotX: 3,
|
||||
hotspotY: 4,
|
||||
image: new Uint8Array([137, 80, 78, 71]),
|
||||
});
|
||||
|
||||
expect(decodeMessage(viewer.sent.at(-1) as Uint8Array)).toEqual({
|
||||
type: "desktop_cursor",
|
||||
x: 640,
|
||||
y: 360,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
visible: true,
|
||||
cursorWidth: 32,
|
||||
cursorHeight: 32,
|
||||
hotspotX: 3,
|
||||
hotspotY: 4,
|
||||
image: new Uint8Array([137, 80, 78, 71]),
|
||||
});
|
||||
});
|
||||
|
||||
test("selects one mutually compatible codec across all viewer transports", () => {
|
||||
const clientId = `rd-codecs-${Date.now().toString(36)}`;
|
||||
createClient(clientId);
|
||||
|
||||
@@ -1568,6 +1568,33 @@ function releaseCanvasFrameAck(clientId: string, sessionId?: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDesktopCursor(clientId: string, payload: unknown) {
|
||||
const data = payload && typeof payload === "object" ? payload as Record<string, unknown> : {};
|
||||
const image = data.image instanceof Uint8Array && data.image.byteLength <= 256 * 1024
|
||||
? data.image
|
||||
: undefined;
|
||||
const cursor = {
|
||||
type: "desktop_cursor",
|
||||
x: Math.max(0, Math.floor(Number(data.x) || 0)),
|
||||
y: Math.max(0, Math.floor(Number(data.y) || 0)),
|
||||
width: Math.max(0, Math.floor(Number(data.width) || 0)),
|
||||
height: Math.max(0, Math.floor(Number(data.height) || 0)),
|
||||
visible: data.visible === true,
|
||||
...(image
|
||||
? {
|
||||
cursorWidth: Math.max(1, Math.floor(Number(data.cursorWidth) || 1)),
|
||||
cursorHeight: Math.max(1, Math.floor(Number(data.cursorHeight) || 1)),
|
||||
hotspotX: Math.max(0, Math.floor(Number(data.hotspotX) || 0)),
|
||||
hotspotY: Math.max(0, Math.floor(Number(data.hotspotY) || 0)),
|
||||
image,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, cursor, "rd-cursor");
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDesktopStreamStats(clientId: string, payload: any) {
|
||||
const stats = {
|
||||
type: "desktop_stream_stats",
|
||||
|
||||
@@ -11,6 +11,7 @@ describe("wsValidation", () => {
|
||||
expect(ALLOWED_CLIENT_MESSAGE_TYPES.has("hello")).toBe(true);
|
||||
expect(ALLOWED_CLIENT_MESSAGE_TYPES.has("ping")).toBe(true);
|
||||
expect(ALLOWED_CLIENT_MESSAGE_TYPES.has("frame")).toBe(true);
|
||||
expect(ALLOWED_CLIENT_MESSAGE_TYPES.has("desktop_cursor")).toBe(true);
|
||||
});
|
||||
|
||||
test("unknown client message types are rejected", () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"command_result",
|
||||
"desktop_encoder_capabilities",
|
||||
"desktop_stream_stats",
|
||||
"desktop_cursor",
|
||||
"client_logs_result",
|
||||
"screenshot_result",
|
||||
"command_progress",
|
||||
|
||||
@@ -22,6 +22,7 @@ test("WebRTC input stays inactive until start and then forwards mouse and keyboa
|
||||
|
||||
constructor(_url: string | URL) {
|
||||
super();
|
||||
Object.defineProperty(window, "__rdSocket", { value: this, configurable: true });
|
||||
queueMicrotask(() => this.dispatchEvent(new Event("open")));
|
||||
}
|
||||
|
||||
@@ -43,16 +44,39 @@ test("WebRTC input stays inactive until start and then forwards mouse and keyboa
|
||||
|
||||
await page.goto("/remotedesktop?clientId=webrtc-input-test");
|
||||
await expect(page.locator("#canvasContainer")).toHaveAttribute("tabindex", "0");
|
||||
await page.evaluate(async () => {
|
||||
const { encodeMsgpack } = await import("/assets/msgpack-helpers.js");
|
||||
const encoded = encodeMsgpack({
|
||||
type: "desktop_encoder_capabilities",
|
||||
display: 0,
|
||||
profiles: [
|
||||
{ maxHeight: 720, width: 1280, height: 720, fps: 60, label: "60 FPS - 720p" },
|
||||
{ maxHeight: 1080, width: 1920, height: 1080, fps: 60, label: "60 FPS - 1080p" },
|
||||
],
|
||||
codecs: [{ codec: "h264", transports: ["websocket", "webrtc"] }],
|
||||
selectedCodec: "h264",
|
||||
fallbackCodecs: ["h264"],
|
||||
transport: "websocket",
|
||||
});
|
||||
const data = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
|
||||
const socket = (window as typeof window & { __rdSocket: EventTarget }).__rdSocket;
|
||||
socket.dispatchEvent(new MessageEvent("message", { data }));
|
||||
});
|
||||
await expect(page.locator("#streamProfileSelect option[value='1440:60']")).toHaveCount(1);
|
||||
await expect(page.locator("#streamProfileSelect option[value='2160:60']")).toHaveCount(1);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const sent = (window as typeof window & { __rdSent: ArrayBuffer[] }).__rdSent;
|
||||
sent.length = 0;
|
||||
const mouseCtrl = document.querySelector<HTMLInputElement>("#mouseCtrl")!;
|
||||
const kbdCtrl = document.querySelector<HTMLInputElement>("#kbdCtrl")!;
|
||||
const cursorCtrl = document.querySelector<HTMLInputElement>("#cursorCtrl")!;
|
||||
mouseCtrl.checked = true;
|
||||
mouseCtrl.dispatchEvent(new Event("change"));
|
||||
kbdCtrl.checked = true;
|
||||
kbdCtrl.dispatchEvent(new Event("change"));
|
||||
const sent = (window as typeof window & { __rdSent: ArrayBuffer[] }).__rdSent;
|
||||
sent.length = 0;
|
||||
cursorCtrl.checked = true;
|
||||
cursorCtrl.dispatchEvent(new Event("change"));
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("#frameCanvas")!;
|
||||
const video = document.querySelector<HTMLVideoElement>("#webrtcVideo")!;
|
||||
canvas.style.display = "none";
|
||||
@@ -70,11 +94,78 @@ test("WebRTC input stays inactive until start and then forwards mouse and keyboa
|
||||
height: 540,
|
||||
toJSON() { return this; },
|
||||
});
|
||||
const renderSurface = document.querySelector<HTMLElement>("#renderSurface")!;
|
||||
renderSurface.getBoundingClientRect = video.getBoundingClientRect;
|
||||
canvas.getBoundingClientRect = video.getBoundingClientRect;
|
||||
|
||||
video.dispatchEvent(new MouseEvent("mousedown", { clientX: 480, clientY: 270, button: 0, bubbles: true }));
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const { encodeMsgpack } = await import("/assets/msgpack-helpers.js");
|
||||
const encoded = encodeMsgpack({
|
||||
type: "desktop_cursor",
|
||||
x: 960,
|
||||
y: 540,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
visible: true,
|
||||
cursorWidth: 32,
|
||||
cursorHeight: 32,
|
||||
hotspotX: 4,
|
||||
hotspotY: 6,
|
||||
image: Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="), (char) => char.charCodeAt(0)),
|
||||
});
|
||||
const data = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
|
||||
const socket = (window as typeof window & { __rdSocket: EventTarget }).__rdSocket;
|
||||
socket.dispatchEvent(new MessageEvent("message", { data }));
|
||||
});
|
||||
|
||||
await expect(page.locator("#frameCanvas")).toHaveCSS("cursor", "default");
|
||||
await expect(page.locator("#webrtcVideo")).toHaveCSS("cursor", "default");
|
||||
await expect(page.locator("#remoteCursor")).toBeHidden();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const mouseCtrl = document.querySelector<HTMLInputElement>("#mouseCtrl")!;
|
||||
mouseCtrl.checked = false;
|
||||
mouseCtrl.dispatchEvent(new Event("change"));
|
||||
});
|
||||
await expect(page.locator("#remoteCursor")).toBeVisible();
|
||||
await expect(page.locator("#remoteCursor")).toHaveCSS("transform", "matrix(1, 0, 0, 1, 478, 267)");
|
||||
await expect(page.locator("#frameCanvas")).toHaveCSS("cursor", "default");
|
||||
await expect(page.locator("#remoteCursor")).toHaveCSS("width", "16px");
|
||||
await expect(page.locator("#remoteCursor")).toHaveCSS("height", "16px");
|
||||
|
||||
await page.evaluate(() => {
|
||||
const mouseCtrl = document.querySelector<HTMLInputElement>("#mouseCtrl")!;
|
||||
const hideLocalCursorCtrl = document.querySelector<HTMLInputElement>("#hideLocalCursorCtrl")!;
|
||||
mouseCtrl.checked = true;
|
||||
mouseCtrl.dispatchEvent(new Event("change"));
|
||||
hideLocalCursorCtrl.checked = true;
|
||||
hideLocalCursorCtrl.dispatchEvent(new Event("change"));
|
||||
});
|
||||
await expect(page.locator("#frameCanvas")).toHaveCSS("cursor", "none");
|
||||
await expect(page.locator("#webrtcVideo")).toHaveCSS("cursor", "none");
|
||||
await expect(page.locator("#remoteCursor")).toBeVisible();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const hideLocalCursorCtrl = document.querySelector<HTMLInputElement>("#hideLocalCursorCtrl")!;
|
||||
hideLocalCursorCtrl.checked = false;
|
||||
hideLocalCursorCtrl.dispatchEvent(new Event("change"));
|
||||
});
|
||||
await expect(page.locator("#frameCanvas")).toHaveCSS("cursor", "default");
|
||||
await expect(page.locator("#remoteCursor")).toBeHidden();
|
||||
await expect.poll(async () => page.evaluate(async () => {
|
||||
const sent = (window as typeof window & { __rdSent: ArrayBuffer[] }).__rdSent;
|
||||
const { decodeMsgpack } = await import("/assets/msgpack-helpers.js");
|
||||
return sent.map((message) => decodeMsgpack(message))
|
||||
.filter((message) => message.type === "desktop_enable_cursor");
|
||||
})).toContainEqual(expect.objectContaining({
|
||||
type: "desktop_enable_cursor",
|
||||
enabled: true,
|
||||
}));
|
||||
|
||||
await expect.poll(async () => page.evaluate(async () => {
|
||||
const sent = (window as typeof window & { __rdSent: ArrayBuffer[] }).__rdSent;
|
||||
const { decodeMsgpack } = await import("/assets/msgpack-helpers.js");
|
||||
|
||||
Reference in New Issue
Block a user