fix webrtc input
This commit is contained in:
@@ -1513,17 +1513,24 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanvasPoint(e) {
|
function getRenderPoint(e) {
|
||||||
let rect = canvas.getBoundingClientRect();
|
const surface = e.currentTarget;
|
||||||
|
let rect = surface.getBoundingClientRect();
|
||||||
if (!rect.width || !rect.height) {
|
if (!rect.width || !rect.height) {
|
||||||
rect = canvasContainer?.getBoundingClientRect() || rect;
|
rect = canvasContainer?.getBoundingClientRect() || rect;
|
||||||
}
|
}
|
||||||
if (!rect.width || !rect.height || !canvas.width || !canvas.height) return null;
|
const targetW = surface === webrtcVideo
|
||||||
let x = ((e.clientX - rect.left) / rect.width) * canvas.width;
|
? webrtcVideo.videoWidth
|
||||||
let y = ((e.clientY - rect.top) / rect.height) * canvas.height;
|
: canvas.width;
|
||||||
|
const targetH = surface === webrtcVideo
|
||||||
|
? webrtcVideo.videoHeight
|
||||||
|
: canvas.height;
|
||||||
|
if (!rect.width || !rect.height || !targetW || !targetH) return null;
|
||||||
|
let x = ((e.clientX - rect.left) / rect.width) * targetW;
|
||||||
|
let y = ((e.clientY - rect.top) / rect.height) * targetH;
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
||||||
x = Math.max(0, Math.min(canvas.width - 1, Math.floor(x)));
|
x = Math.max(0, Math.min(targetW - 1, Math.floor(x)));
|
||||||
y = Math.max(0, Math.min(canvas.height - 1, Math.floor(y)));
|
y = Math.max(0, Math.min(targetH - 1, Math.floor(y)));
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1543,59 +1550,60 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener("mousemove", function (e) {
|
for (const inputSurface of [canvas, webrtcVideo]) {
|
||||||
if (!mouseCtrl.checked) return;
|
if (!inputSurface) continue;
|
||||||
const pt = getCanvasPoint(e);
|
inputSurface.addEventListener("mousemove", function (e) {
|
||||||
if (!pt) return;
|
if (!mouseCtrl.checked) return;
|
||||||
pendingMove = pt;
|
const pt = getRenderPoint(e);
|
||||||
if (!moveTimer) {
|
if (!pt) return;
|
||||||
flushMouseMove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
canvas.addEventListener("mousedown", function (e) {
|
|
||||||
if (!mouseCtrl.checked) return;
|
|
||||||
canvas.focus({ preventScroll: true });
|
|
||||||
const pt = getCanvasPoint(e);
|
|
||||||
if (pt) {
|
|
||||||
pendingMove = pt;
|
pendingMove = pt;
|
||||||
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
if (!moveTimer) {
|
||||||
sendCmd("backstage_mouse_move", pt);
|
flushMouseMove();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
sendCmd("backstage_mouse_down", { button: e.button, ...(pt || {}) });
|
inputSurface.addEventListener("mousedown", function (e) {
|
||||||
e.preventDefault();
|
canvasContainer.focus({ preventScroll: true });
|
||||||
});
|
if (!mouseCtrl.checked) return;
|
||||||
canvas.addEventListener("mouseup", function (e) {
|
const pt = getRenderPoint(e);
|
||||||
if (!mouseCtrl.checked) return;
|
if (pt) {
|
||||||
const pt = getCanvasPoint(e);
|
pendingMove = pt;
|
||||||
if (pt) {
|
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
||||||
pendingMove = pt;
|
sendCmd("backstage_mouse_move", pt);
|
||||||
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
}
|
||||||
sendCmd("backstage_mouse_move", pt);
|
|
||||||
}
|
}
|
||||||
}
|
sendCmd("backstage_mouse_down", { button: e.button, ...(pt || {}) });
|
||||||
sendCmd("backstage_mouse_up", { button: e.button, ...(pt || {}) });
|
e.preventDefault();
|
||||||
e.preventDefault();
|
});
|
||||||
});
|
inputSurface.addEventListener("mouseup", function (e) {
|
||||||
canvas.addEventListener("contextmenu", function (e) {
|
if (!mouseCtrl.checked) return;
|
||||||
e.preventDefault();
|
const pt = getRenderPoint(e);
|
||||||
});
|
if (pt) {
|
||||||
|
pendingMove = pt;
|
||||||
canvas.addEventListener("wheel", function (e) {
|
if (ws.bufferedAmount <= inputBackpressureBytes) {
|
||||||
if (!mouseCtrl.checked) return;
|
sendCmd("backstage_mouse_move", pt);
|
||||||
const pt = getCanvasPoint(e);
|
}
|
||||||
if (!pt) return;
|
}
|
||||||
const delta = Math.max(-120, Math.min(120, Math.round(-e.deltaY)));
|
sendCmd("backstage_mouse_up", { button: e.button, ...(pt || {}) });
|
||||||
sendCmd("backstage_mouse_wheel", { delta, x: pt.x, y: pt.y });
|
e.preventDefault();
|
||||||
e.preventDefault();
|
});
|
||||||
}, { passive: false });
|
inputSurface.addEventListener("contextmenu", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
canvas.setAttribute("tabindex", "0");
|
});
|
||||||
canvas.addEventListener("click", function () {
|
inputSurface.addEventListener("wheel", function (e) {
|
||||||
canvas.focus({ preventScroll: true });
|
if (!mouseCtrl.checked) return;
|
||||||
});
|
const pt = getRenderPoint(e);
|
||||||
|
if (!pt) return;
|
||||||
|
const delta = Math.max(-120, Math.min(120, Math.round(-e.deltaY)));
|
||||||
|
sendCmd("backstage_mouse_wheel", { delta, x: pt.x, y: pt.y });
|
||||||
|
e.preventDefault();
|
||||||
|
}, { passive: false });
|
||||||
|
inputSurface.addEventListener("click", function () {
|
||||||
|
canvasContainer.focus({ preventScroll: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
canvasContainer.setAttribute("tabindex", "0");
|
||||||
const kbdCapture = createKeyboardCapture({
|
const kbdCapture = createKeyboardCapture({
|
||||||
container: canvas,
|
container: canvasContainer,
|
||||||
sendKeyDown: (e) => sendCmd("backstage_key_down", { key: e.key, code: e.code }),
|
sendKeyDown: (e) => sendCmd("backstage_key_down", { key: e.key, code: e.code }),
|
||||||
sendKeyUp: (e) => sendCmd("backstage_key_up", { key: e.key, code: e.code }),
|
sendKeyUp: (e) => sendCmd("backstage_key_up", { key: e.key, code: e.code }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1844,8 +1844,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
pushInputToggles();
|
pushInputToggles();
|
||||||
sharedSettingsSaver.scheduleSave();
|
sharedSettingsSaver.scheduleSave();
|
||||||
});
|
});
|
||||||
|
canvasContainer.setAttribute("tabindex", "0");
|
||||||
const kbdCapture = createKeyboardCapture({
|
const kbdCapture = createKeyboardCapture({
|
||||||
container: canvas,
|
container: canvasContainer,
|
||||||
sendKeyDown: (e) => sendCmd("key_down", { key: e.key, code: e.code }),
|
sendKeyDown: (e) => sendCmd("key_down", { key: e.key, code: e.code }),
|
||||||
sendKeyUp: (e) => sendCmd("key_up", { key: e.key, code: e.code }),
|
sendKeyUp: (e) => sendCmd("key_up", { key: e.key, code: e.code }),
|
||||||
onTextInput: (e) => sendCmd("text_input", { text: e.key }),
|
onTextInput: (e) => sendCmd("text_input", { text: e.key }),
|
||||||
@@ -2644,13 +2645,18 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanvasPoint(e) {
|
function getRenderPoint(e) {
|
||||||
let rect = canvas.getBoundingClientRect();
|
const surface = e.currentTarget;
|
||||||
|
let rect = surface.getBoundingClientRect();
|
||||||
if (!rect.width || !rect.height) {
|
if (!rect.width || !rect.height) {
|
||||||
rect = canvasContainer?.getBoundingClientRect() || rect;
|
rect = canvasContainer?.getBoundingClientRect() || rect;
|
||||||
}
|
}
|
||||||
const targetW = canvas.width || frameWidth;
|
const targetW = surface === webrtcVideo
|
||||||
const targetH = canvas.height || frameHeight;
|
? (webrtcVideo.videoWidth || frameWidth)
|
||||||
|
: (canvas.width || frameWidth);
|
||||||
|
const targetH = surface === webrtcVideo
|
||||||
|
? (webrtcVideo.videoHeight || frameHeight)
|
||||||
|
: (canvas.height || frameHeight);
|
||||||
if (!rect.width || !rect.height || !targetW || !targetH) return null;
|
if (!rect.width || !rect.height || !targetW || !targetH) return null;
|
||||||
let x = ((e.clientX - rect.left) / rect.width) * targetW;
|
let x = ((e.clientX - rect.left) / rect.width) * targetW;
|
||||||
let y = ((e.clientY - rect.top) / rect.height) * targetH;
|
let y = ((e.clientY - rect.top) / rect.height) * targetH;
|
||||||
@@ -2690,54 +2696,55 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener("mousemove", function (e) {
|
for (const inputSurface of [canvas, webrtcVideo]) {
|
||||||
if (!mouseCtrl.checked) return;
|
if (!inputSurface) continue;
|
||||||
const pt = getCanvasPoint(e);
|
inputSurface.addEventListener("mousemove", function (e) {
|
||||||
if (!pt) return;
|
if (!mouseCtrl.checked) return;
|
||||||
pendingMove = pt;
|
const pt = getRenderPoint(e);
|
||||||
if (!moveTimer) {
|
if (!pt) return;
|
||||||
flushMouseMove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
canvas.addEventListener("mousedown", function (e) {
|
|
||||||
if (!mouseCtrl.checked) return;
|
|
||||||
canvas.focus({ preventScroll: true });
|
|
||||||
const pt = getCanvasPoint(e);
|
|
||||||
if (pt) {
|
|
||||||
pendingMove = pt;
|
pendingMove = pt;
|
||||||
smoothPoint = { x: pt.x, y: pt.y };
|
if (!moveTimer) {
|
||||||
sendCmd("mouse_move", pt);
|
flushMouseMove();
|
||||||
}
|
}
|
||||||
sendCmd("mouse_down", { button: e.button, ...(pt || {}) });
|
});
|
||||||
e.preventDefault();
|
inputSurface.addEventListener("mousedown", function (e) {
|
||||||
});
|
canvasContainer.focus({ preventScroll: true });
|
||||||
canvas.addEventListener("mouseup", function (e) {
|
if (!mouseCtrl.checked) return;
|
||||||
if (!mouseCtrl.checked) return;
|
const pt = getRenderPoint(e);
|
||||||
const pt = getCanvasPoint(e);
|
if (pt) {
|
||||||
if (pt) {
|
pendingMove = pt;
|
||||||
pendingMove = pt;
|
smoothPoint = { x: pt.x, y: pt.y };
|
||||||
smoothPoint = { x: pt.x, y: pt.y };
|
sendCmd("mouse_move", pt);
|
||||||
sendCmd("mouse_move", pt);
|
}
|
||||||
}
|
sendCmd("mouse_down", { button: e.button, ...(pt || {}) });
|
||||||
sendCmd("mouse_up", { button: e.button, ...(pt || {}) });
|
e.preventDefault();
|
||||||
e.preventDefault();
|
});
|
||||||
});
|
inputSurface.addEventListener("mouseup", function (e) {
|
||||||
canvas.addEventListener("contextmenu", function (e) {
|
if (!mouseCtrl.checked) return;
|
||||||
e.preventDefault();
|
const pt = getRenderPoint(e);
|
||||||
});
|
if (pt) {
|
||||||
canvas.addEventListener("wheel", function (e) {
|
pendingMove = pt;
|
||||||
if (!mouseCtrl.checked) return;
|
smoothPoint = { x: pt.x, y: pt.y };
|
||||||
const pt = getCanvasPoint(e);
|
sendCmd("mouse_move", pt);
|
||||||
if (!pt) return;
|
}
|
||||||
const delta = Math.max(-120, Math.min(120, Math.round(-e.deltaY)));
|
sendCmd("mouse_up", { button: e.button, ...(pt || {}) });
|
||||||
sendCmd("mouse_wheel", { delta, x: pt.x, y: pt.y });
|
e.preventDefault();
|
||||||
e.preventDefault();
|
});
|
||||||
}, { passive: false });
|
inputSurface.addEventListener("contextmenu", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
canvas.setAttribute("tabindex", "0");
|
});
|
||||||
canvas.addEventListener("click", function () {
|
inputSurface.addEventListener("wheel", function (e) {
|
||||||
canvas.focus({ preventScroll: true });
|
if (!mouseCtrl.checked) return;
|
||||||
});
|
const pt = getRenderPoint(e);
|
||||||
|
if (!pt) return;
|
||||||
|
const delta = Math.max(-120, Math.min(120, Math.round(-e.deltaY)));
|
||||||
|
sendCmd("mouse_wheel", { delta, x: pt.x, y: pt.y });
|
||||||
|
e.preventDefault();
|
||||||
|
}, { passive: false });
|
||||||
|
inputSurface.addEventListener("click", function () {
|
||||||
|
canvasContainer.focus({ preventScroll: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function stopOnExit() {
|
function stopOnExit() {
|
||||||
sharedSettingsSaver.saveNow();
|
sharedSettingsSaver.saveNow();
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { login } from "./helpers";
|
||||||
|
|
||||||
|
test("WebRTC video forwards mouse and keyboard input", async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const sent: ArrayBuffer[] = [];
|
||||||
|
Object.defineProperty(window, "__rdSent", { value: sent, configurable: true });
|
||||||
|
|
||||||
|
class FakeWebSocket extends EventTarget {
|
||||||
|
static readonly CONNECTING = 0;
|
||||||
|
static readonly OPEN = 1;
|
||||||
|
static readonly CLOSING = 2;
|
||||||
|
static readonly CLOSED = 3;
|
||||||
|
readonly CONNECTING = 0;
|
||||||
|
readonly OPEN = 1;
|
||||||
|
readonly CLOSING = 2;
|
||||||
|
readonly CLOSED = 3;
|
||||||
|
readyState = FakeWebSocket.OPEN;
|
||||||
|
bufferedAmount = 0;
|
||||||
|
binaryType: BinaryType = "blob";
|
||||||
|
|
||||||
|
constructor(_url: string | URL) {
|
||||||
|
super();
|
||||||
|
queueMicrotask(() => this.dispatchEvent(new Event("open")));
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: ArrayBuffer | ArrayBufferView | Blob | string) {
|
||||||
|
if (data instanceof ArrayBuffer) sent.push(data);
|
||||||
|
else if (ArrayBuffer.isView(data)) {
|
||||||
|
sent.push(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.readyState = FakeWebSocket.CLOSED;
|
||||||
|
this.dispatchEvent(new CloseEvent("close"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, "WebSocket", { value: FakeWebSocket, configurable: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/remotedesktop?clientId=webrtc-input-test");
|
||||||
|
await expect(page.locator("#canvasContainer")).toHaveAttribute("tabindex", "0");
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const mouseCtrl = document.querySelector<HTMLInputElement>("#mouseCtrl")!;
|
||||||
|
const kbdCtrl = document.querySelector<HTMLInputElement>("#kbdCtrl")!;
|
||||||
|
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;
|
||||||
|
const canvas = document.querySelector<HTMLCanvasElement>("#frameCanvas")!;
|
||||||
|
const video = document.querySelector<HTMLVideoElement>("#webrtcVideo")!;
|
||||||
|
canvas.style.display = "none";
|
||||||
|
video.style.display = "block";
|
||||||
|
Object.defineProperty(video, "videoWidth", { value: 1920, configurable: true });
|
||||||
|
Object.defineProperty(video, "videoHeight", { value: 1080, configurable: true });
|
||||||
|
video.getBoundingClientRect = () => ({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 960,
|
||||||
|
bottom: 540,
|
||||||
|
width: 960,
|
||||||
|
height: 540,
|
||||||
|
toJSON() { return this; },
|
||||||
|
});
|
||||||
|
|
||||||
|
video.dispatchEvent(new MouseEvent("mousemove", { clientX: 480, clientY: 270, bubbles: true }));
|
||||||
|
video.dispatchEvent(new MouseEvent("mousedown", { clientX: 480, clientY: 270, button: 0, bubbles: true }));
|
||||||
|
video.dispatchEvent(new MouseEvent("mouseup", { clientX: 480, clientY: 270, button: 0, bubbles: true }));
|
||||||
|
video.dispatchEvent(new WheelEvent("wheel", { clientX: 480, clientY: 270, deltaY: -80, bubbles: true }));
|
||||||
|
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
|
||||||
|
window.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", bubbles: 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");
|
||||||
|
return sent.map((message) => decodeMsgpack(message));
|
||||||
|
})).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ type: "mouse_move", x: 960, y: 540 }),
|
||||||
|
expect.objectContaining({ type: "mouse_down", button: 0, x: 960, y: 540 }),
|
||||||
|
expect.objectContaining({ type: "mouse_up", button: 0, x: 960, y: 540 }),
|
||||||
|
expect.objectContaining({ type: "mouse_wheel", delta: 80, x: 960, y: 540 }),
|
||||||
|
expect.objectContaining({ type: "key_down", key: "Enter", code: "Enter" }),
|
||||||
|
expect.objectContaining({ type: "key_up", key: "Enter", code: "Enter" }),
|
||||||
|
]));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user