@omercnet/paseo-shared-browser 1.2.2 → 2.0.0-next.249.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OVERVIEW.md +2 -2
- package/README.md +316 -27
- package/REMOTE_BROWSER_RESEARCH.md +395 -0
- package/STREAMING_DESIGN.md +196 -0
- package/client/browser-auto-capture-density.test.ts +102 -0
- package/client/browser-auto-capture-density.ts +30 -0
- package/client/browser-canvas-input.test.ts +198 -0
- package/client/browser-canvas-input.ts +202 -0
- package/client/browser-canvas-keyboard-web.test.ts +113 -0
- package/client/browser-canvas-keyboard-web.ts +230 -0
- package/client/browser-canvas-keyboard.test.ts +123 -0
- package/client/browser-canvas-keyboard.ts +126 -0
- package/client/browser-canvas-layout.test.ts +109 -0
- package/client/browser-canvas-layout.ts +51 -0
- package/client/browser-canvas-viewport-web.test.ts +77 -0
- package/client/browser-canvas-viewport-web.ts +63 -0
- package/client/browser-canvas-viewport.tsx +152 -0
- package/client/browser-capture-cadence.test.ts +9 -0
- package/client/browser-capture-cadence.ts +6 -0
- package/client/browser-capture-density-controls.tsx +75 -0
- package/client/browser-chrome.tsx +234 -0
- package/client/browser-client-presence.ts +184 -0
- package/client/browser-compose-controls.test.tsx +275 -0
- package/client/browser-compose-controls.tsx +174 -0
- package/client/browser-control-button.tsx +109 -0
- package/client/browser-control-dialog.tsx +71 -0
- package/client/browser-control-request.test.ts +27 -0
- package/client/browser-control-request.ts +57 -0
- package/client/browser-control-tooltip-web.ts +10 -0
- package/client/browser-dialog-body.tsx +22 -0
- package/client/browser-emulation-mode.test.ts +75 -0
- package/client/browser-emulation-mode.ts +98 -0
- package/client/browser-fill-viewport.test.ts +45 -0
- package/client/browser-fill-viewport.ts +28 -0
- package/client/browser-frame-buffer.test.ts +157 -0
- package/client/browser-frame-buffer.ts +148 -0
- package/client/browser-frame-image.tsx +62 -0
- package/client/browser-image-load.test.ts +23 -0
- package/client/browser-image-load.ts +26 -0
- package/client/browser-input-queue.test.ts +1254 -0
- package/client/browser-input-queue.ts +519 -0
- package/client/browser-menu-placement.test.ts +90 -0
- package/client/browser-menu-placement.ts +78 -0
- package/client/browser-panel-styles.ts +384 -0
- package/client/browser-quality-controls.test.ts +26 -0
- package/client/browser-quality-controls.tsx +139 -0
- package/client/browser-resolution-picker.tsx +189 -0
- package/client/browser-tabs.tsx +117 -0
- package/client/browser-toolbar-menu-web.test.ts +185 -0
- package/client/browser-toolbar-menu-web.ts +159 -0
- package/client/browser-toolbar-menu.tsx +255 -0
- package/client/browser-toolbar-submenu.tsx +93 -0
- package/client/browser-video-bytes.test.ts +44 -0
- package/client/browser-video-currency.ts +45 -0
- package/client/browser-video-decoder.test.ts +473 -0
- package/client/browser-video-decoder.ts +311 -0
- package/client/browser-video-surface.tsx +21 -0
- package/client/browser-video-visibility.test.ts +45 -0
- package/client/browser-viewer-recovery.test.ts +111 -0
- package/client/browser-viewer-recovery.ts +78 -0
- package/client/browser.tsx +1650 -1579
- package/client/favorite-star.tsx +23 -0
- package/client/frame-lifecycle.ts +62 -59
- package/client/sidebar.tsx +1 -2
- package/client/use-browser-auto-capture-density.test.ts +104 -0
- package/client/use-browser-auto-capture-density.ts +43 -0
- package/client/use-browser-canvas-input.ts +375 -0
- package/client/use-browser-capture-density.ts +87 -0
- package/client/use-browser-emulation-mode.ts +56 -0
- package/client/use-browser-fill-viewport.test.ts +108 -0
- package/client/use-browser-fill-viewport.ts +62 -0
- package/client/use-browser-frame-buffer.ts +66 -0
- package/client/use-browser-image-capture.ts +139 -0
- package/client/use-browser-scoped-mutation.test.ts +118 -0
- package/client/use-browser-scoped-mutation.ts +59 -0
- package/client/use-browser-video.test.ts +1060 -0
- package/client/use-browser-video.ts +481 -0
- package/client/use-browser-viewer-recovery.ts +26 -0
- package/client/use-resolution-favorites.ts +90 -0
- package/client/web.test.ts +125 -0
- package/client/web.ts +405 -0
- package/index.client.tsx +0 -10
- package/index.server.ts +32 -4
- package/npm-shrinkwrap.json +9 -9
- package/package.json +10 -5
- package/paseo-plugin.json +1 -1
- package/scripts/prepare-runtime.mjs +1 -1
- package/server/agent-browser-runtime.ts +1601 -314
- package/server/browser-cursor.test.ts +64 -0
- package/server/browser-cursor.ts +69 -0
- package/server/browser-frame-context.ts +22 -0
- package/server/browser-frame-epoch.test.ts +95 -0
- package/server/browser-frame-rejection.ts +17 -0
- package/server/browser-gesture.ts +120 -0
- package/server/browser-operation-diagnostics.test.ts +79 -0
- package/server/browser-operation-diagnostics.ts +103 -0
- package/server/browser-policy.ts +1316 -151
- package/server/browser-video-input-authority.test.ts +211 -0
- package/server/browser-video-policy.test.ts +564 -0
- package/server/browser-video-socket.test.ts +368 -0
- package/server/browser.ts +151 -16
- package/server/capture-transport-policy.test.ts +105 -0
- package/server/capture-transport-policy.ts +154 -0
- package/server/capture-transport-runtime.test.ts +213 -0
- package/server/daemon-launch-chain.test.ts +70 -0
- package/server/daemon-shutdown.test.ts +289 -0
- package/server/discrete-input-freshness.test.ts +364 -0
- package/server/discrete-input-native-lifetime.test.ts +150 -0
- package/server/display-loss-recovery.test.ts +294 -0
- package/server/emulation-restoration.test.ts +262 -0
- package/server/input-cleanup.ts +23 -0
- package/server/input-generation.test.ts +31 -0
- package/server/input-generation.ts +36 -0
- package/server/jpeg-capture-demand.test.ts +76 -0
- package/server/jpeg-capture-demand.ts +98 -0
- package/server/jpeg-frame.ts +29 -0
- package/server/keyboard-input.test.ts +61 -0
- package/server/keyboard-input.ts +104 -0
- package/server/live-input-policy.test.ts +958 -0
- package/server/live-input-runtime.test.ts +732 -0
- package/server/live-input-supervisor.test.ts +147 -0
- package/server/mcp-entry.ts +44 -2
- package/server/native-video-backpressure.test.ts +228 -0
- package/server/native-video-buffer.test.ts +141 -0
- package/server/native-video-buffer.ts +114 -0
- package/server/native-video-capture.test.ts +599 -0
- package/server/native-video-capture.ts +497 -0
- package/server/native-video-extension-source.test.ts +201 -0
- package/server/native-video-extension-source.ts +364 -0
- package/server/native-video-extension.test.ts +41 -0
- package/server/native-video-extension.ts +83 -0
- package/server/native-video-packet.ts +9 -0
- package/server/native-video-settings.test.ts +245 -0
- package/server/navigation-budget.ts +8 -0
- package/server/navigation-commit.ts +125 -0
- package/server/navigation-policy.test.ts +153 -0
- package/server/process-identity.ts +58 -0
- package/server/reattach-held-input.test.ts +143 -0
- package/server/runtime-frame-reuse.test.ts +295 -0
- package/server/runtime-jpeg-demand.test.ts +200 -0
- package/server/runtime-jpeg-shutdown.test.ts +210 -0
- package/server/runtime-jpeg-source-order.test.ts +201 -0
- package/server/runtime-launch-environment.test.ts +111 -0
- package/server/runtime-navigation.test.ts +327 -0
- package/server/runtime-owner-display.test.ts +152 -0
- package/server/runtime-owner.ts +340 -31
- package/server/runtime-page-state.test.ts +249 -0
- package/server/runtime-protocol.ts +32 -2
- package/server/runtime-target-retention.test.ts +181 -0
- package/server/runtime-video-opt-in.test.ts +82 -0
- package/server/runtime-video.test.ts +444 -0
- package/server/screencast-source-time.ts +46 -0
- package/server/stale-wheel-admission.test.ts +136 -0
- package/server/supervisor-client.ts +28 -4
- package/server/supervisor-heartbeat.test.ts +80 -0
- package/server/supervisor.ts +553 -55
- package/server/tab-close-confirmation.test.ts +97 -0
- package/server/video-source-recovery.test.ts +20 -0
- package/server/video-source-recovery.ts +64 -0
- package/server/viewer-capture-lifetime.ts +51 -0
- package/server/virtual-display.test.ts +184 -0
- package/server/virtual-display.ts +236 -0
- package/shared/browser-display-preferences.ts +46 -0
- package/shared/browser-video.ts +83 -0
- package/shared/browser.ts +286 -65
- package/shared/capture-density.ts +16 -0
- package/shared/capture-settings.ts +17 -0
- package/shared/device-presets.ts +90 -0
- package/shared/resolution-menu.test.ts +120 -0
- package/shared/resolution-menu.ts +72 -0
- package/shared/video-settings.test.ts +38 -0
- package/shared/video-settings.ts +34 -0
- package/shared/viewport-limits.ts +5 -0
package/server/runtime-owner.ts
CHANGED
|
@@ -1,26 +1,105 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { browserGestureKeySchema, MAX_BROWSER_TABS } from "../shared/browser";
|
|
5
|
+
import { videoBitrateSchema, videoFrameRateSchema } from "../shared/video-settings";
|
|
6
|
+
import {
|
|
7
|
+
AgentBrowserRuntime,
|
|
8
|
+
type AgentBrowserRuntimeOptions,
|
|
9
|
+
type BrowserViewport,
|
|
10
|
+
type RuntimeTarget,
|
|
11
|
+
} from "./agent-browser-runtime";
|
|
12
|
+
import { CdpUnknownOutcomeError } from "./cdp";
|
|
5
13
|
import { resolveBrowserRuntimeRoot } from "./runtime-path";
|
|
6
14
|
import type { JsonValue } from "./runtime-protocol";
|
|
7
15
|
import type { RuntimeOwner } from "./supervisor";
|
|
16
|
+
import {
|
|
17
|
+
createPrivateVirtualDisplay,
|
|
18
|
+
PrivateDisplayError,
|
|
19
|
+
type PrivateVirtualDisplay,
|
|
20
|
+
} from "./virtual-display";
|
|
8
21
|
|
|
9
22
|
const DEFAULT_BROWSER_URL = "https://example.com/";
|
|
10
23
|
|
|
11
24
|
interface OwnedRuntime {
|
|
12
25
|
runtimeId: string;
|
|
13
26
|
runtime: AgentBrowserRuntime;
|
|
27
|
+
rootTargetId: string;
|
|
28
|
+
runtimeOptions: AgentBrowserRuntimeOptions;
|
|
29
|
+
tabs: Map<string, Promise<AgentBrowserRuntime>>;
|
|
30
|
+
tabOrder: string[];
|
|
31
|
+
display: PrivateVirtualDisplay | null;
|
|
32
|
+
/** Bounded, secret-free reason an opted-in private display is not in use. */
|
|
33
|
+
displayNotice: string | null;
|
|
14
34
|
}
|
|
15
35
|
interface RuntimeOwnerOptions {
|
|
16
36
|
initialUrl?: string;
|
|
17
37
|
headed?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Trusted opt-in for a private Linux Xvfb display. When omitted, each runtime creation reads
|
|
40
|
+
* PASEO_SHARED_BROWSER_XVFB from this process's environment and enables it only for "1".
|
|
41
|
+
* Explicit false overrides the environment.
|
|
42
|
+
*/
|
|
43
|
+
virtualDisplay?: boolean;
|
|
44
|
+
/** Encoded video is opt-in; defaults to PASEO_SHARED_BROWSER_VIDEO=1 in the daemon environment. */
|
|
45
|
+
nativeVideo?: boolean;
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
function paseoHome(): string {
|
|
21
49
|
return process.env.PASEO_HOME ?? join(homedir(), ".paseo");
|
|
22
50
|
}
|
|
23
51
|
|
|
52
|
+
/** Retain omission for legacy/native held-key inference; reject malformed explicit masks. */
|
|
53
|
+
function optionalMouseModifiers(data: Record<string, JsonValue>): [] | [number] {
|
|
54
|
+
return data.modifiers === undefined
|
|
55
|
+
? []
|
|
56
|
+
: [browserGestureKeySchema.shape.modifiers.parse(data.modifiers)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Retain known tab positions across unordered CDP snapshots; append newly discovered pages. */
|
|
60
|
+
async function orderedTargets(owned: OwnedRuntime): Promise<RuntimeTarget[]> {
|
|
61
|
+
const targets = await owned.runtime.targets();
|
|
62
|
+
const byId = new Map(targets.map((target) => [target.targetId, target]));
|
|
63
|
+
const surviving = owned.tabOrder.filter((targetId) => byId.has(targetId));
|
|
64
|
+
const known = new Set(surviving);
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
if (!known.has(target.targetId)) {
|
|
67
|
+
surviving.push(target.targetId);
|
|
68
|
+
known.add(target.targetId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
owned.tabOrder = surviving;
|
|
72
|
+
return surviving.map((targetId) => byId.get(targetId)!);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Give each page its own CDP attachment and media state within one Chromium profile. */
|
|
76
|
+
async function tabRuntime(owned: OwnedRuntime, targetId: string): Promise<AgentBrowserRuntime> {
|
|
77
|
+
const existing = owned.tabs.get(targetId);
|
|
78
|
+
if (existing) return existing;
|
|
79
|
+
|
|
80
|
+
const attaching = (async () => {
|
|
81
|
+
const targets = await owned.runtime.targets();
|
|
82
|
+
if (!targets.some((target) => target.targetId === targetId)) {
|
|
83
|
+
throw new Error("Browser tab is no longer available");
|
|
84
|
+
}
|
|
85
|
+
const runtime = new AgentBrowserRuntime(owned.runtimeOptions);
|
|
86
|
+
try {
|
|
87
|
+
await runtime.attachTarget(targetId);
|
|
88
|
+
return runtime;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
await runtime.shutdown().catch(() => undefined);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
owned.tabs.set(targetId, attaching);
|
|
95
|
+
try {
|
|
96
|
+
return await attaching;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (owned.tabs.get(targetId) === attaching) owned.tabs.delete(targetId);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
24
103
|
export async function createRuntimeOwner(
|
|
25
104
|
options: RuntimeOwnerOptions = {},
|
|
26
105
|
): Promise<RuntimeOwner<OwnedRuntime>> {
|
|
@@ -48,97 +127,327 @@ export async function createRuntimeOwner(
|
|
|
48
127
|
return {
|
|
49
128
|
async create(workspaceId) {
|
|
50
129
|
const hash = createHash("sha256").update(workspaceId).digest("hex");
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
130
|
+
// Explicit headed mode uses the caller's real display. An opted-in hidden Linux mode
|
|
131
|
+
// gets a private virtual mouse so native pointer media survive emulation.
|
|
132
|
+
let display: PrivateVirtualDisplay | null = null;
|
|
133
|
+
let displayNotice: string | null = null;
|
|
134
|
+
const wantsDisplay = options.virtualDisplay ?? process.env.PASEO_SHARED_BROWSER_XVFB === "1";
|
|
135
|
+
if (!options.headed && wantsDisplay) {
|
|
136
|
+
try {
|
|
137
|
+
display = await createPrivateVirtualDisplay();
|
|
138
|
+
if (!display) displayNotice = "Private display unavailable: Xvfb was not found.";
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// The helper has finished cleanup and no Chromium was constructed yet.
|
|
141
|
+
displayNotice = `Private display unavailable: ${
|
|
142
|
+
error instanceof PrivateDisplayError ? error.message : "unexpected startup failure"
|
|
143
|
+
}.`;
|
|
144
|
+
}
|
|
145
|
+
if (displayNotice) {
|
|
146
|
+
// The detached supervisor discards stdio; the notice also reaches the viewer state.
|
|
147
|
+
console.warn(`[shared-browser] ${displayNotice} Using headless mode.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let runtime: AgentBrowserRuntime | null = null;
|
|
151
|
+
try {
|
|
152
|
+
const runtimeOptions: AgentBrowserRuntimeOptions = {
|
|
153
|
+
binaryPath,
|
|
154
|
+
executablePath,
|
|
155
|
+
profilePath: join(root, "profiles", hash),
|
|
156
|
+
ipcDirectory,
|
|
157
|
+
session: `ws-${hash.slice(0, 16)}`,
|
|
158
|
+
initialUrl: options.initialUrl ?? DEFAULT_BROWSER_URL,
|
|
159
|
+
headed: display ? true : (options.headed ?? false),
|
|
160
|
+
nativeVideo: options.nativeVideo ?? process.env.PASEO_SHARED_BROWSER_VIDEO === "1",
|
|
161
|
+
...(display ? { launchEnvironment: display.launchEnvironment } : {}),
|
|
162
|
+
};
|
|
163
|
+
runtime = new AgentBrowserRuntime(runtimeOptions);
|
|
164
|
+
await runtime.launch();
|
|
165
|
+
display?.assertAvailable();
|
|
166
|
+
const identity = await runtime.identity();
|
|
167
|
+
return {
|
|
168
|
+
runtimeId: randomUUID(),
|
|
169
|
+
runtime,
|
|
170
|
+
rootTargetId: identity.targetId,
|
|
171
|
+
runtimeOptions,
|
|
172
|
+
tabs: new Map(),
|
|
173
|
+
tabOrder: [identity.targetId],
|
|
174
|
+
display,
|
|
175
|
+
displayNotice,
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
let cleanupFailure: unknown = null;
|
|
179
|
+
try {
|
|
180
|
+
await runtime?.shutdown();
|
|
181
|
+
} catch (failure) {
|
|
182
|
+
cleanupFailure = failure;
|
|
183
|
+
} finally {
|
|
184
|
+
await display?.stop();
|
|
185
|
+
}
|
|
186
|
+
// An unconfirmed cleanup must not hide why creation failed.
|
|
187
|
+
if (cleanupFailure)
|
|
188
|
+
throw new AggregateError(
|
|
189
|
+
[error, cleanupFailure],
|
|
190
|
+
`Runtime creation failed (${error instanceof Error ? error.message : String(error)}) and its cleanup was not confirmed`,
|
|
191
|
+
);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
62
194
|
},
|
|
63
195
|
async request(owned, operation, input) {
|
|
196
|
+
owned.display?.assertAvailable();
|
|
64
197
|
const data =
|
|
65
198
|
input && typeof input === "object" && !Array.isArray(input)
|
|
66
199
|
? (input as Record<string, JsonValue>)
|
|
67
200
|
: {};
|
|
201
|
+
if (operation === "tabs.list") {
|
|
202
|
+
return (await orderedTargets(owned)).map(({ targetId, title, url }) => ({
|
|
203
|
+
targetId,
|
|
204
|
+
title,
|
|
205
|
+
url,
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
if (operation === "tabs.create") {
|
|
209
|
+
if ((await orderedTargets(owned)).length >= MAX_BROWSER_TABS)
|
|
210
|
+
throw new Error("Browser tab limit reached");
|
|
211
|
+
const targetId = await owned.runtime.createTarget();
|
|
212
|
+
owned.tabOrder.push(targetId);
|
|
213
|
+
return { targetId };
|
|
214
|
+
}
|
|
215
|
+
if (operation === "tabs.close") {
|
|
216
|
+
const targetId = String(data.targetId);
|
|
217
|
+
const targets = await orderedTargets(owned);
|
|
218
|
+
if (!targets.some((target) => target.targetId === targetId)) {
|
|
219
|
+
throw new Error("Browser tab is no longer available");
|
|
220
|
+
}
|
|
221
|
+
if (targets.length <= 1) {
|
|
222
|
+
throw new Error("The last browser tab cannot be closed");
|
|
223
|
+
}
|
|
224
|
+
const controller = owned.tabs.get(targetId);
|
|
225
|
+
if (controller) await (await controller).shutdown();
|
|
226
|
+
if (owned.rootTargetId === targetId) {
|
|
227
|
+
const successor = targets.find((target) => target.targetId !== targetId)!;
|
|
228
|
+
await owned.runtime.selectTarget(successor.targetId, false);
|
|
229
|
+
owned.rootTargetId = successor.targetId;
|
|
230
|
+
}
|
|
231
|
+
await owned.runtime.closeTarget(targetId);
|
|
232
|
+
// Target.closeTarget can acknowledge before Chromium drops the page from
|
|
233
|
+
// its target list. Success requires observing its absence; the close is
|
|
234
|
+
// never replayed, and exhaustion is reported rather than assumed.
|
|
235
|
+
let gone = false;
|
|
236
|
+
for (let attempt = 0; attempt < 20 && !gone; attempt++) {
|
|
237
|
+
gone = !(await owned.runtime.targets()).some((target) => target.targetId === targetId);
|
|
238
|
+
if (gone) break;
|
|
239
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
240
|
+
setTimeout(resolve, 50);
|
|
241
|
+
await promise;
|
|
242
|
+
}
|
|
243
|
+
owned.tabs.delete(targetId);
|
|
244
|
+
if (!gone) throw new CdpUnknownOutcomeError("Browser tab close could not be confirmed");
|
|
245
|
+
owned.tabOrder = owned.tabOrder.filter((id) => id !== targetId);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const targetId = typeof data.targetId === "string" ? data.targetId : null;
|
|
249
|
+
// Keep the launch page on its original CDP session. A second session on
|
|
250
|
+
// the same target can reset emulation and interrupt its video source.
|
|
251
|
+
const runtime =
|
|
252
|
+
targetId && (targetId !== owned.rootTargetId || owned.tabs.has(targetId))
|
|
253
|
+
? await tabRuntime(owned, targetId)
|
|
254
|
+
: owned.runtime;
|
|
255
|
+
const gestureId = typeof data.gestureId === "string" ? data.gestureId : undefined;
|
|
68
256
|
switch (operation) {
|
|
69
|
-
case "identity":
|
|
70
|
-
|
|
257
|
+
case "identity": {
|
|
258
|
+
const identity = await runtime.identity();
|
|
259
|
+
return {
|
|
260
|
+
...identity,
|
|
261
|
+
...(owned.displayNotice ? { displayNotice: owned.displayNotice } : {}),
|
|
262
|
+
} as unknown as JsonValue;
|
|
263
|
+
}
|
|
71
264
|
case "state":
|
|
72
|
-
return
|
|
265
|
+
if (data.timeoutMs === undefined) return runtime.state() as unknown as JsonValue;
|
|
266
|
+
if (typeof data.timeoutMs !== "number")
|
|
267
|
+
throw new Error("Invalid browser metadata timeout");
|
|
268
|
+
return runtime.state({ timeoutMs: data.timeoutMs }) as unknown as JsonValue;
|
|
73
269
|
case "navigate":
|
|
74
|
-
await
|
|
270
|
+
await runtime.navigate(String(data.url));
|
|
75
271
|
return null;
|
|
76
272
|
case "back":
|
|
77
|
-
await
|
|
273
|
+
await runtime.back();
|
|
78
274
|
return null;
|
|
79
275
|
case "forward":
|
|
80
|
-
await
|
|
276
|
+
await runtime.forward();
|
|
81
277
|
return null;
|
|
82
278
|
case "reload":
|
|
83
|
-
await
|
|
279
|
+
await runtime.reload();
|
|
84
280
|
return null;
|
|
85
281
|
case "emulate":
|
|
86
|
-
await
|
|
282
|
+
await runtime.emulate(data as unknown as BrowserViewport);
|
|
283
|
+
return null;
|
|
284
|
+
case "capture.density":
|
|
285
|
+
await runtime.setCaptureDensity(Number(data.density), Number(data.deviceScaleFactor));
|
|
87
286
|
return null;
|
|
88
287
|
case "screencast.start":
|
|
89
|
-
await
|
|
288
|
+
await runtime.startScreencast(Number(data.quality));
|
|
90
289
|
return null;
|
|
91
290
|
case "screencast.stop":
|
|
92
|
-
await
|
|
291
|
+
await runtime.stopScreencast();
|
|
292
|
+
return null;
|
|
293
|
+
case "video.read":
|
|
294
|
+
return (await runtime.readVideo({
|
|
295
|
+
quality: data.quality === "low" || data.quality === "medium" ? data.quality : "high",
|
|
296
|
+
streamId: typeof data.streamId === "string" ? data.streamId : null,
|
|
297
|
+
requestKeyFrame: data.requestKeyFrame === true,
|
|
298
|
+
...(data.bitrate === undefined
|
|
299
|
+
? {}
|
|
300
|
+
: { bitrate: videoBitrateSchema.parse(data.bitrate) }),
|
|
301
|
+
...(data.fps === undefined ? {} : { fps: videoFrameRateSchema.parse(data.fps) }),
|
|
302
|
+
afterSequence: Number(data.afterSequence ?? 0),
|
|
303
|
+
waitMs: Number(data.waitMs ?? 250),
|
|
304
|
+
})) as unknown as JsonValue;
|
|
305
|
+
case "video.stop":
|
|
306
|
+
await runtime.stopVideo();
|
|
307
|
+
return null;
|
|
308
|
+
case "video.invalidate":
|
|
309
|
+
runtime.invalidateQueuedVideoFrames();
|
|
93
310
|
return null;
|
|
94
311
|
case "frame":
|
|
95
|
-
return (await
|
|
312
|
+
return (await runtime.frame(
|
|
96
313
|
Number(data.maxBytes),
|
|
97
314
|
Number(data.quality),
|
|
98
315
|
Number(data.waitMs),
|
|
99
316
|
)) as unknown as JsonValue;
|
|
100
317
|
case "mouse.move":
|
|
101
|
-
await
|
|
318
|
+
await runtime.mouseMove(
|
|
319
|
+
Number(data.x),
|
|
320
|
+
Number(data.y),
|
|
321
|
+
gestureId,
|
|
322
|
+
...optionalMouseModifiers(data),
|
|
323
|
+
);
|
|
102
324
|
return null;
|
|
103
325
|
case "mouse.down":
|
|
104
|
-
await
|
|
326
|
+
await runtime.mouseDown(
|
|
105
327
|
Number(data.x),
|
|
106
328
|
Number(data.y),
|
|
107
329
|
String(data.button) as "left" | "middle" | "right",
|
|
108
330
|
Number(data.clickCount),
|
|
331
|
+
gestureId,
|
|
332
|
+
...optionalMouseModifiers(data),
|
|
109
333
|
);
|
|
110
334
|
return null;
|
|
111
335
|
case "mouse.up":
|
|
112
|
-
await
|
|
336
|
+
await runtime.mouseUp(
|
|
113
337
|
Number(data.x),
|
|
114
338
|
Number(data.y),
|
|
115
339
|
String(data.button) as "left" | "middle" | "right",
|
|
116
340
|
Number(data.clickCount),
|
|
341
|
+
gestureId,
|
|
342
|
+
...optionalMouseModifiers(data),
|
|
117
343
|
);
|
|
118
344
|
return null;
|
|
119
345
|
case "mouse.wheel":
|
|
120
|
-
await
|
|
346
|
+
await runtime.wheel(
|
|
121
347
|
Number(data.x),
|
|
122
348
|
Number(data.y),
|
|
123
349
|
Number(data.deltaX),
|
|
124
350
|
Number(data.deltaY),
|
|
351
|
+
gestureId,
|
|
352
|
+
...optionalMouseModifiers(data),
|
|
125
353
|
);
|
|
126
354
|
return null;
|
|
355
|
+
case "mouse.leave":
|
|
356
|
+
if (!gestureId) throw new Error("Gesture identity is required");
|
|
357
|
+
await runtime.mouseLeave(gestureId);
|
|
358
|
+
return null;
|
|
359
|
+
case "input.begin":
|
|
360
|
+
if (!gestureId) throw new Error("Gesture identity is required");
|
|
361
|
+
if (typeof data.expectedInputGeneration !== "string") {
|
|
362
|
+
throw new Error("Expected native input generation is required");
|
|
363
|
+
}
|
|
364
|
+
await runtime.beginLiveInput(gestureId, data.expectedInputGeneration);
|
|
365
|
+
return null;
|
|
366
|
+
case "input.check":
|
|
367
|
+
if (!gestureId) throw new Error("Gesture identity is required");
|
|
368
|
+
await runtime.assertLiveInput(gestureId);
|
|
369
|
+
return null;
|
|
370
|
+
case "input.end":
|
|
371
|
+
if (!gestureId) throw new Error("Gesture identity is required");
|
|
372
|
+
await runtime.endLiveInput(gestureId);
|
|
373
|
+
return null;
|
|
374
|
+
case "cursor":
|
|
375
|
+
if (!gestureId) throw new Error("Gesture identity is required");
|
|
376
|
+
return runtime.cursorAt(Number(data.x), Number(data.y), gestureId);
|
|
377
|
+
case "touch": {
|
|
378
|
+
if (!gestureId || !Array.isArray(data.points) || data.points.length > 5)
|
|
379
|
+
throw new Error("Invalid touch input");
|
|
380
|
+
const points = data.points.map((value) => {
|
|
381
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
382
|
+
throw new Error("Invalid touch point");
|
|
383
|
+
const id = value.id;
|
|
384
|
+
if (
|
|
385
|
+
typeof id !== "number" ||
|
|
386
|
+
typeof value.x !== "number" ||
|
|
387
|
+
typeof value.y !== "number"
|
|
388
|
+
)
|
|
389
|
+
throw new Error("Invalid touch point numbers");
|
|
390
|
+
if (!Number.isSafeInteger(id) || id < 0 || id > 2_147_483_647)
|
|
391
|
+
throw new Error("Invalid touch identifier");
|
|
392
|
+
return { x: Number(value.x), y: Number(value.y), id };
|
|
393
|
+
});
|
|
394
|
+
if (new Set(points.map((point) => point.id)).size !== points.length)
|
|
395
|
+
throw new Error("Duplicate touch identifiers");
|
|
396
|
+
const type = data.type;
|
|
397
|
+
if (type !== "start" && type !== "move" && type !== "end" && type !== "cancel")
|
|
398
|
+
throw new Error("Invalid touch phase");
|
|
399
|
+
if ((type === "end" || type === "cancel") !== (points.length === 0))
|
|
400
|
+
throw new Error("Invalid touch phase points");
|
|
401
|
+
const nativeType =
|
|
402
|
+
type === "start"
|
|
403
|
+
? "touchStart"
|
|
404
|
+
: type === "move"
|
|
405
|
+
? "touchMove"
|
|
406
|
+
: type === "end"
|
|
407
|
+
? "touchEnd"
|
|
408
|
+
: "touchCancel";
|
|
409
|
+
await runtime.touch(nativeType, points, gestureId);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
case "input.key":
|
|
413
|
+
if (!gestureId) throw new Error("Gesture identity is required");
|
|
414
|
+
await runtime.dispatchKey(browserGestureKeySchema.parse(data.event), gestureId);
|
|
415
|
+
return null;
|
|
416
|
+
case "input.text":
|
|
417
|
+
if (
|
|
418
|
+
!gestureId ||
|
|
419
|
+
typeof data.text !== "string" ||
|
|
420
|
+
data.text.length < 1 ||
|
|
421
|
+
data.text.length > 16_000
|
|
422
|
+
) {
|
|
423
|
+
throw new Error("Invalid committed text input");
|
|
424
|
+
}
|
|
425
|
+
await runtime.insertText(data.text, gestureId);
|
|
426
|
+
return null;
|
|
127
427
|
case "text.insert":
|
|
128
|
-
await
|
|
428
|
+
await runtime.insertText(String(data.text), gestureId);
|
|
129
429
|
return null;
|
|
130
430
|
case "key.down":
|
|
131
|
-
await
|
|
431
|
+
await runtime.keyDown(String(data.key), String(data.key), gestureId);
|
|
132
432
|
return null;
|
|
133
433
|
case "key.up":
|
|
134
|
-
await
|
|
434
|
+
await runtime.keyUp(String(data.key), String(data.key), gestureId);
|
|
135
435
|
return null;
|
|
136
436
|
default:
|
|
137
437
|
throw new Error(`Unknown browser runtime operation: ${operation}`);
|
|
138
438
|
}
|
|
139
439
|
},
|
|
140
440
|
async stop(owned) {
|
|
141
|
-
|
|
441
|
+
try {
|
|
442
|
+
await Promise.allSettled(
|
|
443
|
+
[...owned.tabs.values()].map(async (controller) =>
|
|
444
|
+
(await controller).shutdown().catch(() => undefined),
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
await owned.runtime.shutdown();
|
|
448
|
+
} finally {
|
|
449
|
+
await owned.display?.stop();
|
|
450
|
+
}
|
|
142
451
|
},
|
|
143
452
|
};
|
|
144
453
|
}
|