@push.rocks/smartbrowser 2.0.10 → 3.0.0
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/.smartconfig.json +49 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +10 -5
- package/dist_ts_web/00_commitinfo_data.d.ts +8 -0
- package/dist_ts_web/00_commitinfo_data.js +9 -0
- package/dist_ts_web/classes.livebrowsercanvasrenderer.d.ts +114 -0
- package/dist_ts_web/classes.livebrowsercanvasrenderer.js +1159 -0
- package/dist_ts_web/index.d.ts +3 -0
- package/dist_ts_web/index.js +2 -0
- package/dist_ts_web/interfaces.livebrowsercanvas.d.ts +29 -0
- package/dist_ts_web/interfaces.livebrowsercanvas.js +2 -0
- package/license.md +19 -0
- package/package.json +32 -20
- package/readme.hints.md +25 -1
- package/readme.md +153 -36
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +10 -8
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/classes.livebrowsercanvasrenderer.ts +1388 -0
- package/ts_web/index.ts +27 -0
- package/ts_web/interfaces.livebrowsercanvas.ts +54 -0
- package/npmextra.json +0 -33
|
@@ -0,0 +1,1388 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ILiveBrowserFrame,
|
|
3
|
+
ILiveBrowserInputBase,
|
|
4
|
+
ILiveBrowserKeyInput,
|
|
5
|
+
ILiveBrowserModifierState,
|
|
6
|
+
ILiveBrowserMouseInput,
|
|
7
|
+
ILiveBrowserState,
|
|
8
|
+
ILiveBrowserViewport,
|
|
9
|
+
TLiveBrowserEvent,
|
|
10
|
+
} from '@push.rocks/smartpuppeteer';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ILiveBrowserCanvasError,
|
|
14
|
+
ILiveBrowserCanvasRendererOptions,
|
|
15
|
+
TLiveBrowserCanvasErrorCode,
|
|
16
|
+
} from './interfaces.livebrowsercanvas.js';
|
|
17
|
+
|
|
18
|
+
const maxInputQueueLength = 128;
|
|
19
|
+
const maxPendingAcknowledgements = 16;
|
|
20
|
+
const maxInputReleaseAttempts = 2;
|
|
21
|
+
const coordinateEdgeInset = 0.001;
|
|
22
|
+
const defaultOperationTimeoutMs = 10000;
|
|
23
|
+
|
|
24
|
+
class LiveBrowserOperationTimeoutError extends Error {}
|
|
25
|
+
|
|
26
|
+
interface IFrameWork {
|
|
27
|
+
frame: ILiveBrowserFrame;
|
|
28
|
+
runEpoch: number;
|
|
29
|
+
acknowledgementAttempted: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IDisplayedFrame extends ILiveBrowserInputBase {
|
|
33
|
+
sequence: number;
|
|
34
|
+
viewport: ILiveBrowserViewport;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TInputCommandKind = 'key' | 'mouse' | 'mouseMove' | 'text' | 'wheel';
|
|
38
|
+
|
|
39
|
+
interface IInputCommand {
|
|
40
|
+
kind: TInputCommandKind;
|
|
41
|
+
runEpoch: number;
|
|
42
|
+
execute: () => Promise<void>;
|
|
43
|
+
onAttempt?: () => void;
|
|
44
|
+
onSuccess?: () => void;
|
|
45
|
+
resolve: () => void;
|
|
46
|
+
reject: (errorArg: unknown) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IResizeFence {
|
|
50
|
+
minimumViewportRevision: number;
|
|
51
|
+
target: ILiveBrowserViewport;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const viewportEquals = (
|
|
55
|
+
firstArg: ILiveBrowserViewport,
|
|
56
|
+
secondArg: ILiveBrowserViewport,
|
|
57
|
+
): boolean => (
|
|
58
|
+
firstArg.width === secondArg.width
|
|
59
|
+
&& firstArg.height === secondArg.height
|
|
60
|
+
&& firstArg.deviceScaleFactor === secondArg.deviceScaleFactor
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const createModifiers = (
|
|
64
|
+
eventArg: MouseEvent | KeyboardEvent,
|
|
65
|
+
): ILiveBrowserModifierState => ({
|
|
66
|
+
alt: eventArg.altKey,
|
|
67
|
+
control: eventArg.ctrlKey,
|
|
68
|
+
meta: eventArg.metaKey,
|
|
69
|
+
shift: eventArg.shiftKey,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const createFrameAcknowledgementRequest = (frameArg: ILiveBrowserFrame) => ({
|
|
73
|
+
tabId: frameArg.tabId,
|
|
74
|
+
sequence: frameArg.sequence,
|
|
75
|
+
generation: frameArg.generation,
|
|
76
|
+
viewportRevision: frameArg.viewportRevision,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const createInputIdentity = (displayedFrameArg: IDisplayedFrame): ILiveBrowserInputBase => ({
|
|
80
|
+
tabId: displayedFrameArg.tabId,
|
|
81
|
+
generation: displayedFrameArg.generation,
|
|
82
|
+
viewportRevision: displayedFrameArg.viewportRevision,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const getErrorMessage = (errorArg: unknown): string => (
|
|
86
|
+
errorArg instanceof Error ? errorArg.message : String(errorArg)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Renders transport-neutral live browser frames and maps DOM input back to the
|
|
91
|
+
* currently displayed SmartPuppeteer frame identity.
|
|
92
|
+
*/
|
|
93
|
+
export class LiveBrowserCanvasRenderer {
|
|
94
|
+
private readonly canvas: HTMLCanvasElement;
|
|
95
|
+
private readonly canvasContext: CanvasRenderingContext2D;
|
|
96
|
+
private readonly client: ILiveBrowserCanvasRendererOptions['client'];
|
|
97
|
+
private readonly focusTarget: HTMLElement;
|
|
98
|
+
private readonly resizeTarget?: Element;
|
|
99
|
+
private readonly getDeviceScaleFactor: () => number;
|
|
100
|
+
private readonly operationTimeoutMs: number;
|
|
101
|
+
private readonly frameDecodeTimeoutMs: number;
|
|
102
|
+
private readonly onError?: ILiveBrowserCanvasRendererOptions['onError'];
|
|
103
|
+
private readonly onFrameRendered?: ILiveBrowserCanvasRendererOptions['onFrameRendered'];
|
|
104
|
+
private readonly initialCanvasWidth: number;
|
|
105
|
+
private readonly initialCanvasHeight: number;
|
|
106
|
+
|
|
107
|
+
private lifecycleTail: Promise<void> = Promise.resolve();
|
|
108
|
+
private runEpoch = 0;
|
|
109
|
+
private running = false;
|
|
110
|
+
private stopping = false;
|
|
111
|
+
private stopRequested = false;
|
|
112
|
+
private terminallyStopped = false;
|
|
113
|
+
private acceptingFrames = false;
|
|
114
|
+
private state?: ILiveBrowserState;
|
|
115
|
+
private unsubscribe?: () => void;
|
|
116
|
+
private listenerController?: AbortController;
|
|
117
|
+
private resizeObserver?: ResizeObserver;
|
|
118
|
+
private focusWasAdjusted = false;
|
|
119
|
+
private focusHadTabIndex = false;
|
|
120
|
+
private focusTabIndexValue: string | null = null;
|
|
121
|
+
|
|
122
|
+
private displayedFrame?: IDisplayedFrame;
|
|
123
|
+
private highestFrameSequence = -1;
|
|
124
|
+
private queuedFrame?: IFrameWork;
|
|
125
|
+
private frameProcessingPromise?: Promise<void>;
|
|
126
|
+
private acknowledgementPromises = new Set<Promise<void>>();
|
|
127
|
+
|
|
128
|
+
private inputBlocked = true;
|
|
129
|
+
private inputCommands: IInputCommand[] = [];
|
|
130
|
+
private inputProcessing = false;
|
|
131
|
+
private inputIdleResolvers: Array<() => void> = [];
|
|
132
|
+
private inputResetTail: Promise<void> = Promise.resolve();
|
|
133
|
+
private inputResetPromise?: Promise<void>;
|
|
134
|
+
private inputResetCount = 0;
|
|
135
|
+
private inputOverflowRecoveryPending = false;
|
|
136
|
+
private pressedKeys = new Map<string, ILiveBrowserKeyInput>();
|
|
137
|
+
private pressedMouseButtons = new Map<string, ILiveBrowserMouseInput>();
|
|
138
|
+
private capturedPointerIds = new Set<number>();
|
|
139
|
+
|
|
140
|
+
private pendingViewport?: ILiveBrowserViewport;
|
|
141
|
+
private inFlightViewport?: ILiveBrowserViewport;
|
|
142
|
+
private viewportProcessing = false;
|
|
143
|
+
private viewportProcessingPromise?: Promise<void>;
|
|
144
|
+
private resizeFence?: IResizeFence;
|
|
145
|
+
|
|
146
|
+
constructor(optionsArg: ILiveBrowserCanvasRendererOptions) {
|
|
147
|
+
this.canvas = optionsArg.canvas;
|
|
148
|
+
this.client = optionsArg.client;
|
|
149
|
+
this.focusTarget = optionsArg.focusTarget ?? this.canvas;
|
|
150
|
+
this.resizeTarget = optionsArg.resizeTarget;
|
|
151
|
+
this.getDeviceScaleFactor = optionsArg.getDeviceScaleFactor
|
|
152
|
+
?? (() => window.devicePixelRatio || 1);
|
|
153
|
+
this.operationTimeoutMs = this.validateTimeout(
|
|
154
|
+
optionsArg.operationTimeoutMs ?? defaultOperationTimeoutMs,
|
|
155
|
+
'operationTimeoutMs',
|
|
156
|
+
);
|
|
157
|
+
this.frameDecodeTimeoutMs = this.validateTimeout(
|
|
158
|
+
optionsArg.frameDecodeTimeoutMs ?? this.operationTimeoutMs,
|
|
159
|
+
'frameDecodeTimeoutMs',
|
|
160
|
+
);
|
|
161
|
+
this.onError = optionsArg.onError;
|
|
162
|
+
this.onFrameRendered = optionsArg.onFrameRendered;
|
|
163
|
+
this.initialCanvasWidth = this.canvas.width;
|
|
164
|
+
this.initialCanvasHeight = this.canvas.height;
|
|
165
|
+
|
|
166
|
+
const canvasContext = this.canvas.getContext('2d');
|
|
167
|
+
if (!canvasContext) {
|
|
168
|
+
throw new Error('LiveBrowserCanvasRenderer requires a 2D canvas context');
|
|
169
|
+
}
|
|
170
|
+
this.canvasContext = canvasContext;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public get isRunning(): boolean {
|
|
174
|
+
return this.running;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public start(): Promise<void> {
|
|
178
|
+
return this.enqueueLifecycle(async () => {
|
|
179
|
+
if (this.terminallyStopped) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
'Live browser renderer cannot restart after a terminal operation failure; create a new renderer and client',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (this.running) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const runEpoch = ++this.runEpoch;
|
|
189
|
+
this.running = true;
|
|
190
|
+
this.stopping = false;
|
|
191
|
+
this.stopRequested = false;
|
|
192
|
+
this.acceptingFrames = true;
|
|
193
|
+
this.inputBlocked = true;
|
|
194
|
+
this.highestFrameSequence = -1;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
this.makeFocusTargetFocusable();
|
|
198
|
+
this.installDomListeners(runEpoch);
|
|
199
|
+
this.unsubscribe = this.client.onEvent((eventArg) => {
|
|
200
|
+
this.handleEvent(eventArg, runEpoch);
|
|
201
|
+
});
|
|
202
|
+
this.applyState(this.client.getState(), runEpoch);
|
|
203
|
+
this.installResizeObserver(runEpoch);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await this.stopRun(runEpoch);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public stop(): Promise<void> {
|
|
212
|
+
return this.enqueueLifecycle(async () => {
|
|
213
|
+
if (!this.running) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await this.stopRun(this.runEpoch);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Sends arbitrary committed text, including text produced by an external IME.
|
|
222
|
+
*/
|
|
223
|
+
public async insertText(textArg: string): Promise<void> {
|
|
224
|
+
const displayedFrame = this.requireInputFrame();
|
|
225
|
+
const inputIdentity = createInputIdentity(displayedFrame);
|
|
226
|
+
await this.enqueueInputCommand({
|
|
227
|
+
kind: 'text',
|
|
228
|
+
runEpoch: this.runEpoch,
|
|
229
|
+
execute: async () => {
|
|
230
|
+
await this.client.insertText({
|
|
231
|
+
...inputIdentity,
|
|
232
|
+
text: textArg,
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Reads the configured stable resize target and requests the corresponding
|
|
240
|
+
* remote CSS viewport. Intrinsic canvas dimensions are never used here.
|
|
241
|
+
*/
|
|
242
|
+
public syncViewport(): void {
|
|
243
|
+
if (!this.running || this.stopping || this.stopRequested || !this.resizeTarget) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const width = Math.floor(this.resizeTarget.clientWidth);
|
|
247
|
+
const height = Math.floor(this.resizeTarget.clientHeight);
|
|
248
|
+
this.queueViewport(width, height, this.runEpoch);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private enqueueLifecycle(operationArg: () => Promise<void>): Promise<void> {
|
|
252
|
+
const operation = this.lifecycleTail.then(operationArg, operationArg);
|
|
253
|
+
this.lifecycleTail = operation.then(() => undefined, () => undefined);
|
|
254
|
+
return operation;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async stopRun(runEpochArg: number): Promise<void> {
|
|
258
|
+
this.stopping = true;
|
|
259
|
+
this.acceptingFrames = false;
|
|
260
|
+
this.inputBlocked = true;
|
|
261
|
+
this.pendingViewport = undefined;
|
|
262
|
+
this.releasePointerCaptures();
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
this.resizeObserver?.disconnect();
|
|
266
|
+
this.resizeObserver = undefined;
|
|
267
|
+
this.listenerController?.abort();
|
|
268
|
+
this.listenerController = undefined;
|
|
269
|
+
const unsubscribe = this.unsubscribe;
|
|
270
|
+
this.unsubscribe = undefined;
|
|
271
|
+
if (unsubscribe) {
|
|
272
|
+
try {
|
|
273
|
+
unsubscribe();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.reportError({
|
|
276
|
+
code: 'renderer_cleanup_failed',
|
|
277
|
+
message: `Could not unsubscribe the live browser renderer: ${getErrorMessage(error)}`,
|
|
278
|
+
cause: error,
|
|
279
|
+
}, runEpochArg);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (this.queuedFrame) {
|
|
284
|
+
const queuedFrame = this.queuedFrame;
|
|
285
|
+
this.queuedFrame = undefined;
|
|
286
|
+
await this.acknowledgeFrame(queuedFrame);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await this.viewportProcessingPromise;
|
|
290
|
+
await this.inputResetTail;
|
|
291
|
+
await this.waitForInputIdle();
|
|
292
|
+
await this.releasePressedInput(runEpochArg);
|
|
293
|
+
|
|
294
|
+
this.running = false;
|
|
295
|
+
await this.frameProcessingPromise;
|
|
296
|
+
await Promise.all([...this.acknowledgementPromises]);
|
|
297
|
+
} finally {
|
|
298
|
+
this.running = false;
|
|
299
|
+
this.acceptingFrames = false;
|
|
300
|
+
this.state = undefined;
|
|
301
|
+
this.displayedFrame = undefined;
|
|
302
|
+
this.resizeFence = undefined;
|
|
303
|
+
this.pendingViewport = undefined;
|
|
304
|
+
this.inFlightViewport = undefined;
|
|
305
|
+
this.viewportProcessing = false;
|
|
306
|
+
this.inputResetPromise = undefined;
|
|
307
|
+
this.inputBlocked = true;
|
|
308
|
+
this.pressedKeys.clear();
|
|
309
|
+
this.pressedMouseButtons.clear();
|
|
310
|
+
this.releasePointerCaptures();
|
|
311
|
+
const abandonedCommands = this.inputCommands.splice(0);
|
|
312
|
+
for (const command of abandonedCommands) {
|
|
313
|
+
command.reject(new Error('Live browser renderer stopped before dispatching input'));
|
|
314
|
+
}
|
|
315
|
+
const inputIdleResolvers = this.inputIdleResolvers;
|
|
316
|
+
this.inputIdleResolvers = [];
|
|
317
|
+
for (const resolver of inputIdleResolvers) {
|
|
318
|
+
resolver();
|
|
319
|
+
}
|
|
320
|
+
this.canvas.width = this.initialCanvasWidth;
|
|
321
|
+
this.canvas.height = this.initialCanvasHeight;
|
|
322
|
+
this.restoreFocusTarget();
|
|
323
|
+
this.stopRequested = false;
|
|
324
|
+
this.stopping = false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private installDomListeners(runEpochArg: number): void {
|
|
329
|
+
const listenerController = new AbortController();
|
|
330
|
+
const signal = listenerController.signal;
|
|
331
|
+
this.listenerController = listenerController;
|
|
332
|
+
|
|
333
|
+
this.canvas.addEventListener('pointerdown', (eventArg) => {
|
|
334
|
+
this.handlePointerDown(eventArg, runEpochArg);
|
|
335
|
+
}, { signal });
|
|
336
|
+
this.canvas.addEventListener('pointermove', (eventArg) => {
|
|
337
|
+
this.handlePointerMove(eventArg, runEpochArg);
|
|
338
|
+
}, { signal });
|
|
339
|
+
this.canvas.addEventListener('pointerup', (eventArg) => {
|
|
340
|
+
this.handlePointerUp(eventArg, runEpochArg);
|
|
341
|
+
}, { signal });
|
|
342
|
+
this.canvas.addEventListener('pointercancel', (eventArg) => {
|
|
343
|
+
this.releasePointerCapture(eventArg.pointerId);
|
|
344
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
345
|
+
}, { signal });
|
|
346
|
+
this.canvas.addEventListener('lostpointercapture', (eventArg) => {
|
|
347
|
+
this.capturedPointerIds.delete(eventArg.pointerId);
|
|
348
|
+
if (this.pressedMouseButtons.size > 0) {
|
|
349
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
350
|
+
}
|
|
351
|
+
}, { signal });
|
|
352
|
+
this.canvas.addEventListener('contextmenu', (eventArg) => {
|
|
353
|
+
eventArg.preventDefault();
|
|
354
|
+
}, { signal });
|
|
355
|
+
this.canvas.addEventListener('wheel', (eventArg) => {
|
|
356
|
+
this.handleWheel(eventArg, runEpochArg);
|
|
357
|
+
}, { passive: false, signal });
|
|
358
|
+
|
|
359
|
+
this.focusTarget.addEventListener('keydown', (eventArg) => {
|
|
360
|
+
this.handleKey(eventArg, 'down', runEpochArg);
|
|
361
|
+
}, { signal });
|
|
362
|
+
this.focusTarget.addEventListener('keyup', (eventArg) => {
|
|
363
|
+
this.handleKey(eventArg, 'up', runEpochArg);
|
|
364
|
+
}, { signal });
|
|
365
|
+
this.focusTarget.addEventListener('compositionend', (eventArg) => {
|
|
366
|
+
if (!this.inputBlocked && eventArg.data) {
|
|
367
|
+
void this.insertText(eventArg.data).catch(() => undefined);
|
|
368
|
+
}
|
|
369
|
+
}, { signal });
|
|
370
|
+
|
|
371
|
+
window.addEventListener('blur', () => {
|
|
372
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
373
|
+
}, { signal });
|
|
374
|
+
window.addEventListener('resize', () => {
|
|
375
|
+
this.syncViewport();
|
|
376
|
+
}, { signal });
|
|
377
|
+
document.addEventListener('visibilitychange', () => {
|
|
378
|
+
if (document.visibilityState === 'hidden') {
|
|
379
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
380
|
+
}
|
|
381
|
+
}, { signal });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private installResizeObserver(runEpochArg: number): void {
|
|
385
|
+
if (!this.resizeTarget) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.resizeObserver = new ResizeObserver((entriesArg) => {
|
|
389
|
+
const matchingEntry = entriesArg.find((entryArg) => entryArg.target === this.resizeTarget);
|
|
390
|
+
if (!matchingEntry) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this.queueViewport(
|
|
394
|
+
Math.floor(matchingEntry.contentRect.width),
|
|
395
|
+
Math.floor(matchingEntry.contentRect.height),
|
|
396
|
+
runEpochArg,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
this.resizeObserver.observe(this.resizeTarget);
|
|
400
|
+
this.syncViewport();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private handleEvent(eventArg: TLiveBrowserEvent, runEpochArg: number): void {
|
|
404
|
+
if (eventArg.type === 'frame') {
|
|
405
|
+
this.queueFrame(eventArg.frame, runEpochArg);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (!this.running || runEpochArg !== this.runEpoch) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (eventArg.type === 'state') {
|
|
412
|
+
this.applyState(eventArg.state, runEpochArg);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.reportError({
|
|
416
|
+
code: 'remote_browser_error',
|
|
417
|
+
message: `${eventArg.error.code}: ${eventArg.error.message}`,
|
|
418
|
+
cause: eventArg.error,
|
|
419
|
+
}, runEpochArg);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private applyState(stateArg: ILiveBrowserState, runEpochArg: number): void {
|
|
423
|
+
if (!this.running || runEpochArg !== this.runEpoch) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
this.state = stateArg;
|
|
427
|
+
|
|
428
|
+
if (this.displayedFrame && this.isStateAheadOfDisplayedFrame(stateArg, this.displayedFrame)) {
|
|
429
|
+
this.displayedFrame = undefined;
|
|
430
|
+
this.clearCanvas();
|
|
431
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.updateInputAvailability();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private isStateAheadOfDisplayedFrame(
|
|
438
|
+
stateArg: ILiveBrowserState,
|
|
439
|
+
displayedFrameArg: IDisplayedFrame,
|
|
440
|
+
): boolean {
|
|
441
|
+
if (stateArg.status !== 'running' || stateArg.activeTabId !== displayedFrameArg.tabId) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
const activeTab = stateArg.tabs.find((tabArg) => tabArg.id === displayedFrameArg.tabId);
|
|
445
|
+
if (!activeTab || activeTab.status !== 'open') {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
return stateArg.viewportRevision > displayedFrameArg.viewportRevision
|
|
449
|
+
|| activeTab.generation > displayedFrameArg.generation;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private queueFrame(frameArg: ILiveBrowserFrame, runEpochArg: number): void {
|
|
453
|
+
const frameWork: IFrameWork = {
|
|
454
|
+
frame: frameArg,
|
|
455
|
+
runEpoch: runEpochArg,
|
|
456
|
+
acknowledgementAttempted: false,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
if (!this.running || !this.acceptingFrames || runEpochArg !== this.runEpoch) {
|
|
460
|
+
void this.acknowledgeFrame(frameWork);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.highestFrameSequence = Math.max(this.highestFrameSequence, frameArg.sequence);
|
|
465
|
+
if (this.queuedFrame) {
|
|
466
|
+
const supersededFrame = this.queuedFrame;
|
|
467
|
+
this.queuedFrame = undefined;
|
|
468
|
+
void this.acknowledgeFrame(supersededFrame);
|
|
469
|
+
}
|
|
470
|
+
this.queuedFrame = frameWork;
|
|
471
|
+
|
|
472
|
+
this.ensureFrameProcessor();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private ensureFrameProcessor(): void {
|
|
476
|
+
if (this.frameProcessingPromise || !this.queuedFrame) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const processingPromise = this.processFrameQueue().finally(() => {
|
|
480
|
+
if (this.frameProcessingPromise === processingPromise) {
|
|
481
|
+
this.frameProcessingPromise = undefined;
|
|
482
|
+
if (this.queuedFrame) {
|
|
483
|
+
this.ensureFrameProcessor();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
this.frameProcessingPromise = processingPromise;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private async processFrameQueue(): Promise<void> {
|
|
491
|
+
while (this.queuedFrame) {
|
|
492
|
+
const frameWork = this.queuedFrame;
|
|
493
|
+
this.queuedFrame = undefined;
|
|
494
|
+
await this.processFrame(frameWork);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private async processFrame(frameWorkArg: IFrameWork): Promise<void> {
|
|
499
|
+
const { frame, runEpoch } = frameWorkArg;
|
|
500
|
+
let imageBitmap: ImageBitmap | undefined;
|
|
501
|
+
try {
|
|
502
|
+
if (!this.canRenderFrame(frame, runEpoch)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const frameBytes = new Uint8Array(frame.data.byteLength);
|
|
507
|
+
frameBytes.set(frame.data);
|
|
508
|
+
imageBitmap = await this.decodeFrame(new Blob([frameBytes.buffer], {
|
|
509
|
+
type: frame.mimeType,
|
|
510
|
+
}));
|
|
511
|
+
|
|
512
|
+
if (!this.canRenderFrame(frame, runEpoch) || frame.sequence !== this.highestFrameSequence) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (imageBitmap.width !== frame.width || imageBitmap.height !== frame.height) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`decoded frame dimensions ${imageBitmap.width}x${imageBitmap.height} do not match ${frame.width}x${frame.height}`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.canvas.width = frame.width;
|
|
522
|
+
this.canvas.height = frame.height;
|
|
523
|
+
this.canvasContext.clearRect(0, 0, frame.width, frame.height);
|
|
524
|
+
this.canvasContext.drawImage(imageBitmap, 0, 0, frame.width, frame.height);
|
|
525
|
+
this.displayedFrame = {
|
|
526
|
+
tabId: frame.tabId,
|
|
527
|
+
sequence: frame.sequence,
|
|
528
|
+
generation: frame.generation,
|
|
529
|
+
viewportRevision: frame.viewportRevision,
|
|
530
|
+
viewport: { ...frame.viewport },
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
if (
|
|
534
|
+
this.resizeFence
|
|
535
|
+
&& !this.viewportProcessing
|
|
536
|
+
&& frame.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
537
|
+
&& viewportEquals(frame.viewport, this.resizeFence.target)
|
|
538
|
+
) {
|
|
539
|
+
this.resizeFence = undefined;
|
|
540
|
+
}
|
|
541
|
+
this.updateInputAvailability();
|
|
542
|
+
this.callFrameRendered(frame, runEpoch);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.reportError({
|
|
545
|
+
code: 'frame_render_failed',
|
|
546
|
+
message: `Could not render live browser frame: ${getErrorMessage(error)}`,
|
|
547
|
+
cause: error,
|
|
548
|
+
frame,
|
|
549
|
+
}, runEpoch);
|
|
550
|
+
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
551
|
+
this.requestStop(runEpoch);
|
|
552
|
+
}
|
|
553
|
+
} finally {
|
|
554
|
+
imageBitmap?.close();
|
|
555
|
+
await this.acknowledgeFrame(frameWorkArg);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private canRenderFrame(frameArg: ILiveBrowserFrame, runEpochArg: number): boolean {
|
|
560
|
+
if (
|
|
561
|
+
!this.running
|
|
562
|
+
|| !this.acceptingFrames
|
|
563
|
+
|| runEpochArg !== this.runEpoch
|
|
564
|
+
|| this.viewportProcessing
|
|
565
|
+
) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
const state = this.state;
|
|
569
|
+
if (!state || state.status !== 'running' || state.activeTabId !== frameArg.tabId) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
const activeTab = state.tabs.find((tabArg) => tabArg.id === frameArg.tabId);
|
|
573
|
+
if (!activeTab || activeTab.status !== 'open') {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
if (
|
|
577
|
+
state.viewportRevision > frameArg.viewportRevision
|
|
578
|
+
|| activeTab.generation > frameArg.generation
|
|
579
|
+
) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
if (this.resizeFence) {
|
|
583
|
+
return frameArg.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
584
|
+
&& viewportEquals(frameArg.viewport, this.resizeFence.target);
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private acknowledgeFrame(frameWorkArg: IFrameWork): Promise<void> {
|
|
590
|
+
if (frameWorkArg.acknowledgementAttempted) {
|
|
591
|
+
return Promise.resolve();
|
|
592
|
+
}
|
|
593
|
+
frameWorkArg.acknowledgementAttempted = true;
|
|
594
|
+
|
|
595
|
+
if (this.acknowledgementPromises.size >= maxPendingAcknowledgements) {
|
|
596
|
+
this.reportError({
|
|
597
|
+
code: 'frame_acknowledgement_failed',
|
|
598
|
+
message: `Live browser frame acknowledgement capacity of ${maxPendingAcknowledgements} was reached`,
|
|
599
|
+
}, frameWorkArg.runEpoch);
|
|
600
|
+
this.requestStop(frameWorkArg.runEpoch);
|
|
601
|
+
return Promise.resolve();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const acknowledgement = this.runClientOperation(
|
|
605
|
+
() => this.client.acknowledgeFrame(createFrameAcknowledgementRequest(frameWorkArg.frame)),
|
|
606
|
+
'frame acknowledgement',
|
|
607
|
+
)
|
|
608
|
+
.then(() => undefined)
|
|
609
|
+
.catch((error) => {
|
|
610
|
+
this.reportError({
|
|
611
|
+
code: 'frame_acknowledgement_failed',
|
|
612
|
+
message: `Could not acknowledge live browser frame: ${getErrorMessage(error)}`,
|
|
613
|
+
cause: error,
|
|
614
|
+
frame: frameWorkArg.frame,
|
|
615
|
+
}, frameWorkArg.runEpoch);
|
|
616
|
+
this.requestStop(frameWorkArg.runEpoch);
|
|
617
|
+
});
|
|
618
|
+
let trackedAcknowledgement: Promise<void>;
|
|
619
|
+
trackedAcknowledgement = acknowledgement.finally(() => {
|
|
620
|
+
this.acknowledgementPromises.delete(trackedAcknowledgement);
|
|
621
|
+
});
|
|
622
|
+
this.acknowledgementPromises.add(trackedAcknowledgement);
|
|
623
|
+
return trackedAcknowledgement;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private handlePointerDown(eventArg: PointerEvent, runEpochArg: number): void {
|
|
627
|
+
if (eventArg.pointerType && eventArg.pointerType !== 'mouse') {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const button = this.getMouseButton(eventArg.button);
|
|
631
|
+
if (!button) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const displayedFrame = this.getInputFrame();
|
|
635
|
+
if (!displayedFrame || runEpochArg !== this.runEpoch) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
eventArg.preventDefault();
|
|
639
|
+
const coordinates = this.mapCoordinates(eventArg, displayedFrame.viewport);
|
|
640
|
+
if (!coordinates) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
this.focusTarget.focus({ preventScroll: true });
|
|
644
|
+
try {
|
|
645
|
+
this.canvas.setPointerCapture(eventArg.pointerId);
|
|
646
|
+
this.capturedPointerIds.add(eventArg.pointerId);
|
|
647
|
+
} catch {
|
|
648
|
+
// Pointer capture may already have been released by the browser.
|
|
649
|
+
}
|
|
650
|
+
const input: ILiveBrowserMouseInput = {
|
|
651
|
+
...createInputIdentity(displayedFrame),
|
|
652
|
+
type: 'down',
|
|
653
|
+
...coordinates,
|
|
654
|
+
button,
|
|
655
|
+
buttons: eventArg.buttons,
|
|
656
|
+
clickCount: Math.min(3, Math.max(1, eventArg.detail || 1)),
|
|
657
|
+
modifiers: createModifiers(eventArg),
|
|
658
|
+
};
|
|
659
|
+
void this.enqueueInputCommand({
|
|
660
|
+
kind: 'mouse',
|
|
661
|
+
runEpoch: runEpochArg,
|
|
662
|
+
execute: async () => this.client.dispatchMouse(input),
|
|
663
|
+
onAttempt: () => this.pressedMouseButtons.set(button, input),
|
|
664
|
+
}).catch(() => undefined);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private handlePointerMove(eventArg: PointerEvent, runEpochArg: number): void {
|
|
668
|
+
if (eventArg.pointerType && eventArg.pointerType !== 'mouse') {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const displayedFrame = this.getInputFrame();
|
|
672
|
+
if (!displayedFrame || runEpochArg !== this.runEpoch) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
eventArg.preventDefault();
|
|
676
|
+
const coordinates = this.mapCoordinates(eventArg, displayedFrame.viewport);
|
|
677
|
+
if (!coordinates) {
|
|
678
|
+
this.releasePointerCapture(eventArg.pointerId);
|
|
679
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const input: ILiveBrowserMouseInput = {
|
|
683
|
+
...createInputIdentity(displayedFrame),
|
|
684
|
+
type: 'move',
|
|
685
|
+
...coordinates,
|
|
686
|
+
button: 'none',
|
|
687
|
+
buttons: eventArg.buttons,
|
|
688
|
+
modifiers: createModifiers(eventArg),
|
|
689
|
+
};
|
|
690
|
+
void this.enqueueInputCommand({
|
|
691
|
+
kind: 'mouseMove',
|
|
692
|
+
runEpoch: runEpochArg,
|
|
693
|
+
execute: async () => this.client.dispatchMouse(input),
|
|
694
|
+
}).catch(() => undefined);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private handlePointerUp(eventArg: PointerEvent, runEpochArg: number): void {
|
|
698
|
+
if (eventArg.pointerType && eventArg.pointerType !== 'mouse') {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const button = this.getMouseButton(eventArg.button);
|
|
702
|
+
const displayedFrame = this.getInputFrame();
|
|
703
|
+
if (!button || !displayedFrame || runEpochArg !== this.runEpoch) {
|
|
704
|
+
this.releasePointerCapture(eventArg.pointerId);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
eventArg.preventDefault();
|
|
708
|
+
const coordinates = this.mapCoordinates(eventArg, displayedFrame.viewport);
|
|
709
|
+
if (!coordinates) {
|
|
710
|
+
this.releasePointerCapture(eventArg.pointerId);
|
|
711
|
+
void this.scheduleInputReset(false, runEpochArg);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const input: ILiveBrowserMouseInput = {
|
|
715
|
+
...createInputIdentity(displayedFrame),
|
|
716
|
+
type: 'up',
|
|
717
|
+
...coordinates,
|
|
718
|
+
button,
|
|
719
|
+
buttons: eventArg.buttons,
|
|
720
|
+
clickCount: Math.min(3, Math.max(1, eventArg.detail || 1)),
|
|
721
|
+
modifiers: createModifiers(eventArg),
|
|
722
|
+
};
|
|
723
|
+
void this.enqueueInputCommand({
|
|
724
|
+
kind: 'mouse',
|
|
725
|
+
runEpoch: runEpochArg,
|
|
726
|
+
execute: async () => this.client.dispatchMouse(input),
|
|
727
|
+
onSuccess: () => this.pressedMouseButtons.delete(button),
|
|
728
|
+
}).catch(() => undefined);
|
|
729
|
+
this.releasePointerCapture(eventArg.pointerId);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private handleWheel(eventArg: WheelEvent, runEpochArg: number): void {
|
|
733
|
+
const displayedFrame = this.getInputFrame();
|
|
734
|
+
if (!displayedFrame || runEpochArg !== this.runEpoch) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
eventArg.preventDefault();
|
|
738
|
+
const coordinates = this.mapCoordinates(eventArg, displayedFrame.viewport);
|
|
739
|
+
if (!coordinates) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const multiplier = eventArg.deltaMode === WheelEvent.DOM_DELTA_LINE
|
|
743
|
+
? 16
|
|
744
|
+
: eventArg.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
|
745
|
+
? displayedFrame.viewport.height
|
|
746
|
+
: 1;
|
|
747
|
+
const clampDelta = (valueArg: number) => Math.max(-1000000, Math.min(1000000, valueArg));
|
|
748
|
+
void this.enqueueInputCommand({
|
|
749
|
+
kind: 'wheel',
|
|
750
|
+
runEpoch: runEpochArg,
|
|
751
|
+
execute: async () => this.client.dispatchWheel({
|
|
752
|
+
...createInputIdentity(displayedFrame),
|
|
753
|
+
...coordinates,
|
|
754
|
+
deltaX: clampDelta(eventArg.deltaX * multiplier),
|
|
755
|
+
deltaY: clampDelta(eventArg.deltaY * multiplier),
|
|
756
|
+
modifiers: createModifiers(eventArg),
|
|
757
|
+
}),
|
|
758
|
+
}).catch(() => undefined);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private handleKey(
|
|
762
|
+
eventArg: KeyboardEvent,
|
|
763
|
+
typeArg: 'down' | 'up',
|
|
764
|
+
runEpochArg: number,
|
|
765
|
+
): void {
|
|
766
|
+
const displayedFrame = this.getInputFrame();
|
|
767
|
+
if (
|
|
768
|
+
!displayedFrame
|
|
769
|
+
|| runEpochArg !== this.runEpoch
|
|
770
|
+
|| eventArg.isComposing
|
|
771
|
+
|| eventArg.key === 'Process'
|
|
772
|
+
) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
eventArg.preventDefault();
|
|
776
|
+
const text = typeArg === 'down'
|
|
777
|
+
&& eventArg.key.length === 1
|
|
778
|
+
&& !eventArg.metaKey
|
|
779
|
+
&& (!eventArg.ctrlKey || eventArg.altKey)
|
|
780
|
+
? eventArg.key
|
|
781
|
+
: undefined;
|
|
782
|
+
const input: ILiveBrowserKeyInput = {
|
|
783
|
+
...createInputIdentity(displayedFrame),
|
|
784
|
+
type: typeArg,
|
|
785
|
+
key: eventArg.key,
|
|
786
|
+
code: eventArg.code || undefined,
|
|
787
|
+
text,
|
|
788
|
+
windowsVirtualKeyCode: eventArg.keyCode || undefined,
|
|
789
|
+
autoRepeat: eventArg.repeat,
|
|
790
|
+
isKeypad: eventArg.location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD,
|
|
791
|
+
location: eventArg.location,
|
|
792
|
+
modifiers: createModifiers(eventArg),
|
|
793
|
+
};
|
|
794
|
+
const keyIdentity = eventArg.code || eventArg.key;
|
|
795
|
+
void this.enqueueInputCommand({
|
|
796
|
+
kind: 'key',
|
|
797
|
+
runEpoch: runEpochArg,
|
|
798
|
+
execute: async () => this.client.dispatchKey(input),
|
|
799
|
+
onAttempt: typeArg === 'down'
|
|
800
|
+
? () => this.pressedKeys.set(keyIdentity, input)
|
|
801
|
+
: undefined,
|
|
802
|
+
onSuccess: () => {
|
|
803
|
+
if (typeArg === 'up') {
|
|
804
|
+
this.pressedKeys.delete(keyIdentity);
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
}).catch(() => undefined);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private mapCoordinates(
|
|
811
|
+
eventArg: MouseEvent,
|
|
812
|
+
viewportArg: ILiveBrowserViewport,
|
|
813
|
+
): { x: number; y: number } | undefined {
|
|
814
|
+
const canvasBounds = this.canvas.getBoundingClientRect();
|
|
815
|
+
if (canvasBounds.width <= 0 || canvasBounds.height <= 0) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
const x = ((eventArg.clientX - canvasBounds.left) / canvasBounds.width) * viewportArg.width;
|
|
819
|
+
const y = ((eventArg.clientY - canvasBounds.top) / canvasBounds.height) * viewportArg.height;
|
|
820
|
+
return {
|
|
821
|
+
x: Math.max(0, Math.min(viewportArg.width - coordinateEdgeInset, x)),
|
|
822
|
+
y: Math.max(0, Math.min(viewportArg.height - coordinateEdgeInset, y)),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private getMouseButton(buttonArg: number): ILiveBrowserMouseInput['button'] | undefined {
|
|
827
|
+
return buttonArg === 0
|
|
828
|
+
? 'left'
|
|
829
|
+
: buttonArg === 1
|
|
830
|
+
? 'middle'
|
|
831
|
+
: buttonArg === 2
|
|
832
|
+
? 'right'
|
|
833
|
+
: buttonArg === 3
|
|
834
|
+
? 'back'
|
|
835
|
+
: buttonArg === 4
|
|
836
|
+
? 'forward'
|
|
837
|
+
: undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private getInputFrame(): IDisplayedFrame | undefined {
|
|
841
|
+
return this.inputBlocked || !this.isDisplayedFrameCurrent()
|
|
842
|
+
? undefined
|
|
843
|
+
: this.displayedFrame;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private requireInputFrame(): IDisplayedFrame {
|
|
847
|
+
const displayedFrame = this.getInputFrame();
|
|
848
|
+
if (!displayedFrame) {
|
|
849
|
+
throw new Error('Live browser input is unavailable until a current frame is displayed');
|
|
850
|
+
}
|
|
851
|
+
return displayedFrame;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private isDisplayedFrameCurrent(): boolean {
|
|
855
|
+
const state = this.state;
|
|
856
|
+
const displayedFrame = this.displayedFrame;
|
|
857
|
+
if (!state || !displayedFrame || state.status !== 'running') {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
if (
|
|
861
|
+
state.activeTabId !== displayedFrame.tabId
|
|
862
|
+
|| state.viewportRevision > displayedFrame.viewportRevision
|
|
863
|
+
) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
const activeTab = state.tabs.find((tabArg) => tabArg.id === displayedFrame.tabId);
|
|
867
|
+
return Boolean(
|
|
868
|
+
activeTab
|
|
869
|
+
&& activeTab.status === 'open'
|
|
870
|
+
&& activeTab.generation <= displayedFrame.generation,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private enqueueInputCommand(commandArg: Omit<IInputCommand, 'reject' | 'resolve'>): Promise<void> {
|
|
875
|
+
if (!this.running || this.inputBlocked || commandArg.runEpoch !== this.runEpoch) {
|
|
876
|
+
return Promise.reject(new Error('Live browser input is currently blocked'));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return new Promise<void>((resolve, reject) => {
|
|
880
|
+
const command: IInputCommand = {
|
|
881
|
+
...commandArg,
|
|
882
|
+
resolve,
|
|
883
|
+
reject,
|
|
884
|
+
};
|
|
885
|
+
const lastCommand = this.inputCommands.at(-1);
|
|
886
|
+
if (command.kind === 'mouseMove' && lastCommand?.kind === 'mouseMove') {
|
|
887
|
+
lastCommand.resolve();
|
|
888
|
+
this.inputCommands[this.inputCommands.length - 1] = command;
|
|
889
|
+
} else if (this.inputCommands.length >= maxInputQueueLength) {
|
|
890
|
+
if (command.kind === 'mouseMove') {
|
|
891
|
+
resolve();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const error = new Error('Live browser input queue reached its capacity');
|
|
895
|
+
this.reportError({
|
|
896
|
+
code: 'input_queue_capacity_exceeded',
|
|
897
|
+
message: error.message,
|
|
898
|
+
cause: error,
|
|
899
|
+
}, command.runEpoch);
|
|
900
|
+
reject(error);
|
|
901
|
+
this.recoverFromInputOverflow(command.runEpoch);
|
|
902
|
+
return;
|
|
903
|
+
} else {
|
|
904
|
+
this.inputCommands.push(command);
|
|
905
|
+
}
|
|
906
|
+
this.processInputCommands();
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private processInputCommands(): void {
|
|
911
|
+
if (this.inputProcessing) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this.inputProcessing = true;
|
|
915
|
+
void (async () => {
|
|
916
|
+
try {
|
|
917
|
+
while (this.inputCommands.length > 0) {
|
|
918
|
+
const command = this.inputCommands.shift()!;
|
|
919
|
+
if (command.runEpoch !== this.runEpoch) {
|
|
920
|
+
command.resolve();
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
command.onAttempt?.();
|
|
925
|
+
await this.runClientOperation(command.execute, 'input dispatch');
|
|
926
|
+
command.onSuccess?.();
|
|
927
|
+
command.resolve();
|
|
928
|
+
} catch (error) {
|
|
929
|
+
this.reportError({
|
|
930
|
+
code: 'input_dispatch_failed',
|
|
931
|
+
message: `Could not dispatch live browser input: ${getErrorMessage(error)}`,
|
|
932
|
+
cause: error,
|
|
933
|
+
}, command.runEpoch);
|
|
934
|
+
command.reject(error);
|
|
935
|
+
const abandonedCommands = this.inputCommands.splice(0);
|
|
936
|
+
for (const abandonedCommand of abandonedCommands) {
|
|
937
|
+
abandonedCommand.reject(
|
|
938
|
+
new Error('Live browser input was abandoned after a dispatch failure'),
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (!this.stopping) {
|
|
942
|
+
void this.scheduleInputReset(false, command.runEpoch);
|
|
943
|
+
}
|
|
944
|
+
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
945
|
+
this.requestStop(command.runEpoch);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} finally {
|
|
950
|
+
this.inputProcessing = false;
|
|
951
|
+
if (this.inputCommands.length > 0) {
|
|
952
|
+
this.processInputCommands();
|
|
953
|
+
} else {
|
|
954
|
+
const resolvers = this.inputIdleResolvers;
|
|
955
|
+
this.inputIdleResolvers = [];
|
|
956
|
+
for (const resolver of resolvers) {
|
|
957
|
+
resolver();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
})();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
private waitForInputIdle(): Promise<void> {
|
|
965
|
+
if (!this.inputProcessing && this.inputCommands.length === 0) {
|
|
966
|
+
return Promise.resolve();
|
|
967
|
+
}
|
|
968
|
+
return new Promise<void>((resolve) => {
|
|
969
|
+
this.inputIdleResolvers.push(resolve);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private scheduleInputReset(
|
|
974
|
+
clearDisplayArg: boolean,
|
|
975
|
+
runEpochArg: number,
|
|
976
|
+
): Promise<void> {
|
|
977
|
+
if (runEpochArg !== this.runEpoch || this.stopping) {
|
|
978
|
+
return Promise.resolve();
|
|
979
|
+
}
|
|
980
|
+
this.inputBlocked = true;
|
|
981
|
+
this.releasePointerCaptures();
|
|
982
|
+
if (clearDisplayArg) {
|
|
983
|
+
this.displayedFrame = undefined;
|
|
984
|
+
this.clearCanvas();
|
|
985
|
+
}
|
|
986
|
+
if (this.inputResetPromise) {
|
|
987
|
+
return this.inputResetPromise;
|
|
988
|
+
}
|
|
989
|
+
this.inputResetCount++;
|
|
990
|
+
let resetOperation: Promise<void>;
|
|
991
|
+
resetOperation = this.inputResetTail.then(async () => {
|
|
992
|
+
await this.waitForInputIdle();
|
|
993
|
+
await this.releasePressedInput(runEpochArg);
|
|
994
|
+
}).finally(() => {
|
|
995
|
+
this.inputResetCount--;
|
|
996
|
+
if (this.inputResetPromise === resetOperation) {
|
|
997
|
+
this.inputResetPromise = undefined;
|
|
998
|
+
}
|
|
999
|
+
this.updateInputAvailability();
|
|
1000
|
+
});
|
|
1001
|
+
this.inputResetPromise = resetOperation;
|
|
1002
|
+
this.inputResetTail = resetOperation.then(() => undefined, () => undefined);
|
|
1003
|
+
return resetOperation;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private recoverFromInputOverflow(runEpochArg: number): void {
|
|
1007
|
+
if (this.inputOverflowRecoveryPending) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
this.inputOverflowRecoveryPending = true;
|
|
1011
|
+
this.inputBlocked = true;
|
|
1012
|
+
void this.scheduleInputReset(false, runEpochArg).finally(() => {
|
|
1013
|
+
this.inputOverflowRecoveryPending = false;
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private async releasePressedInput(runEpochArg: number): Promise<void> {
|
|
1018
|
+
for (const [keyIdentity, pressedKey] of [...this.pressedKeys.entries()]) {
|
|
1019
|
+
const released = await this.tryInputRelease(
|
|
1020
|
+
() => this.client.dispatchKey({
|
|
1021
|
+
...pressedKey,
|
|
1022
|
+
type: 'up',
|
|
1023
|
+
text: undefined,
|
|
1024
|
+
autoRepeat: false,
|
|
1025
|
+
modifiers: {},
|
|
1026
|
+
}),
|
|
1027
|
+
runEpochArg,
|
|
1028
|
+
);
|
|
1029
|
+
if (released && this.pressedKeys.get(keyIdentity) === pressedKey) {
|
|
1030
|
+
this.pressedKeys.delete(keyIdentity);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (const [button, pressedButton] of [...this.pressedMouseButtons.entries()]) {
|
|
1034
|
+
const released = await this.tryInputRelease(
|
|
1035
|
+
() => this.client.dispatchMouse({
|
|
1036
|
+
...pressedButton,
|
|
1037
|
+
type: 'up',
|
|
1038
|
+
buttons: 0,
|
|
1039
|
+
clickCount: 1,
|
|
1040
|
+
modifiers: {},
|
|
1041
|
+
}),
|
|
1042
|
+
runEpochArg,
|
|
1043
|
+
);
|
|
1044
|
+
if (released && this.pressedMouseButtons.get(button) === pressedButton) {
|
|
1045
|
+
this.pressedMouseButtons.delete(button);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private async tryInputRelease(
|
|
1051
|
+
operationArg: () => Promise<void>,
|
|
1052
|
+
runEpochArg: number,
|
|
1053
|
+
): Promise<boolean> {
|
|
1054
|
+
let lastError: unknown;
|
|
1055
|
+
for (let attempt = 0; attempt < maxInputReleaseAttempts; attempt++) {
|
|
1056
|
+
try {
|
|
1057
|
+
await this.runClientOperation(
|
|
1058
|
+
operationArg,
|
|
1059
|
+
'input release',
|
|
1060
|
+
);
|
|
1061
|
+
return true;
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
lastError = error;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
this.reportInputReleaseError(lastError, runEpochArg);
|
|
1067
|
+
this.requestStop(runEpochArg);
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private reportInputReleaseError(errorArg: unknown, runEpochArg: number): void {
|
|
1072
|
+
this.reportError({
|
|
1073
|
+
code: 'input_dispatch_failed',
|
|
1074
|
+
message: `Could not release live browser input: ${getErrorMessage(errorArg)}`,
|
|
1075
|
+
cause: errorArg,
|
|
1076
|
+
}, runEpochArg);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private queueViewport(widthArg: number, heightArg: number, runEpochArg: number): void {
|
|
1080
|
+
if (
|
|
1081
|
+
!this.running
|
|
1082
|
+
|| this.stopping
|
|
1083
|
+
|| this.stopRequested
|
|
1084
|
+
|| runEpochArg !== this.runEpoch
|
|
1085
|
+
|| widthArg <= 0
|
|
1086
|
+
|| heightArg <= 0
|
|
1087
|
+
) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const deviceScaleFactor = this.getDeviceScaleFactor();
|
|
1091
|
+
if (!Number.isFinite(deviceScaleFactor) || deviceScaleFactor <= 0) {
|
|
1092
|
+
this.reportError({
|
|
1093
|
+
code: 'viewport_update_failed',
|
|
1094
|
+
message: 'Live browser device scale factor must be a positive finite number',
|
|
1095
|
+
}, runEpochArg);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const viewport: ILiveBrowserViewport = {
|
|
1099
|
+
width: widthArg,
|
|
1100
|
+
height: heightArg,
|
|
1101
|
+
deviceScaleFactor,
|
|
1102
|
+
};
|
|
1103
|
+
if (this.pendingViewport && viewportEquals(viewport, this.pendingViewport)) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (this.inFlightViewport && viewportEquals(viewport, this.inFlightViewport)) {
|
|
1107
|
+
this.pendingViewport = undefined;
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (
|
|
1111
|
+
!this.inFlightViewport
|
|
1112
|
+
&& this.resizeFence
|
|
1113
|
+
&& viewportEquals(viewport, this.resizeFence.target)
|
|
1114
|
+
) {
|
|
1115
|
+
this.pendingViewport = undefined;
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (
|
|
1119
|
+
!this.inFlightViewport
|
|
1120
|
+
&& !this.resizeFence
|
|
1121
|
+
&& this.state
|
|
1122
|
+
&& viewportEquals(viewport, this.state.viewport)
|
|
1123
|
+
) {
|
|
1124
|
+
this.pendingViewport = undefined;
|
|
1125
|
+
this.updateInputAvailability();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
this.pendingViewport = viewport;
|
|
1129
|
+
this.inputBlocked = true;
|
|
1130
|
+
this.ensureViewportProcessor(runEpochArg);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private ensureViewportProcessor(runEpochArg: number): void {
|
|
1134
|
+
if (this.viewportProcessingPromise || !this.pendingViewport) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const processingPromise = this.processViewportQueue(runEpochArg).finally(() => {
|
|
1138
|
+
if (this.viewportProcessingPromise === processingPromise) {
|
|
1139
|
+
this.viewportProcessingPromise = undefined;
|
|
1140
|
+
if (
|
|
1141
|
+
this.pendingViewport
|
|
1142
|
+
&& this.running
|
|
1143
|
+
&& !this.stopping
|
|
1144
|
+
&& runEpochArg === this.runEpoch
|
|
1145
|
+
) {
|
|
1146
|
+
this.ensureViewportProcessor(runEpochArg);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
this.viewportProcessingPromise = processingPromise;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private async processViewportQueue(runEpochArg: number): Promise<void> {
|
|
1154
|
+
this.viewportProcessing = true;
|
|
1155
|
+
try {
|
|
1156
|
+
while (
|
|
1157
|
+
this.pendingViewport
|
|
1158
|
+
&& this.running
|
|
1159
|
+
&& !this.stopping
|
|
1160
|
+
&& runEpochArg === this.runEpoch
|
|
1161
|
+
) {
|
|
1162
|
+
const viewport = this.pendingViewport;
|
|
1163
|
+
this.pendingViewport = undefined;
|
|
1164
|
+
this.inFlightViewport = viewport;
|
|
1165
|
+
if (!this.resizeFence && this.state && viewportEquals(viewport, this.state.viewport)) {
|
|
1166
|
+
this.inFlightViewport = undefined;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
await this.scheduleInputReset(true, runEpochArg);
|
|
1171
|
+
if (!this.running || this.stopping || runEpochArg !== this.runEpoch) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const currentRevision = this.state?.viewportRevision ?? 0;
|
|
1175
|
+
const previousMinimumRevision = this.resizeFence?.minimumViewportRevision ?? currentRevision;
|
|
1176
|
+
try {
|
|
1177
|
+
await this.runClientOperation(
|
|
1178
|
+
() => this.client.setViewport(viewport),
|
|
1179
|
+
'viewport update',
|
|
1180
|
+
);
|
|
1181
|
+
if (!this.running || this.stopping || runEpochArg !== this.runEpoch) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
this.resizeFence = {
|
|
1185
|
+
target: viewport,
|
|
1186
|
+
minimumViewportRevision: Math.max(currentRevision, previousMinimumRevision) + 1,
|
|
1187
|
+
};
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (runEpochArg === this.runEpoch) {
|
|
1190
|
+
this.resizeFence = undefined;
|
|
1191
|
+
this.reportError({
|
|
1192
|
+
code: 'viewport_update_failed',
|
|
1193
|
+
message: `Could not update live browser viewport: ${getErrorMessage(error)}`,
|
|
1194
|
+
cause: error,
|
|
1195
|
+
}, runEpochArg);
|
|
1196
|
+
if (error instanceof LiveBrowserOperationTimeoutError) {
|
|
1197
|
+
this.requestStop(runEpochArg);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
} finally {
|
|
1201
|
+
if (this.inFlightViewport && viewportEquals(this.inFlightViewport, viewport)) {
|
|
1202
|
+
this.inFlightViewport = undefined;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
} finally {
|
|
1207
|
+
this.viewportProcessing = false;
|
|
1208
|
+
this.updateInputAvailability();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private updateInputAvailability(): void {
|
|
1213
|
+
this.inputBlocked = !(
|
|
1214
|
+
this.running
|
|
1215
|
+
&& this.acceptingFrames
|
|
1216
|
+
&& !this.viewportProcessing
|
|
1217
|
+
&& !this.resizeFence
|
|
1218
|
+
&& this.inputResetCount === 0
|
|
1219
|
+
&& this.pressedKeys.size === 0
|
|
1220
|
+
&& this.pressedMouseButtons.size === 0
|
|
1221
|
+
&& this.isDisplayedFrameCurrent()
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private makeFocusTargetFocusable(): void {
|
|
1226
|
+
this.focusWasAdjusted = false;
|
|
1227
|
+
if (this.focusTarget.tabIndex >= 0) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
this.focusHadTabIndex = this.focusTarget.hasAttribute('tabindex');
|
|
1231
|
+
this.focusTabIndexValue = this.focusTarget.getAttribute('tabindex');
|
|
1232
|
+
this.focusTarget.setAttribute('tabindex', '0');
|
|
1233
|
+
this.focusWasAdjusted = true;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
private restoreFocusTarget(): void {
|
|
1237
|
+
if (!this.focusWasAdjusted) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (this.focusHadTabIndex && this.focusTabIndexValue !== null) {
|
|
1241
|
+
this.focusTarget.setAttribute('tabindex', this.focusTabIndexValue);
|
|
1242
|
+
} else {
|
|
1243
|
+
this.focusTarget.removeAttribute('tabindex');
|
|
1244
|
+
}
|
|
1245
|
+
this.focusWasAdjusted = false;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private clearCanvas(): void {
|
|
1249
|
+
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private callFrameRendered(frameArg: ILiveBrowserFrame, runEpochArg: number): void {
|
|
1253
|
+
if (!this.onFrameRendered || runEpochArg !== this.runEpoch || !this.running) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
this.onFrameRendered(frameArg);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
this.reportError({
|
|
1260
|
+
code: 'renderer_callback_failed',
|
|
1261
|
+
message: `Live browser frame callback failed: ${getErrorMessage(error)}`,
|
|
1262
|
+
cause: error,
|
|
1263
|
+
frame: frameArg,
|
|
1264
|
+
}, runEpochArg);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private reportError(errorArg: ILiveBrowserCanvasError, runEpochArg: number): void {
|
|
1269
|
+
if (!this.onError || runEpochArg !== this.runEpoch) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
this.onError(errorArg);
|
|
1274
|
+
} catch {
|
|
1275
|
+
// Consumer error reporting must not break renderer cleanup.
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
private requestStop(runEpochArg: number): void {
|
|
1280
|
+
if (
|
|
1281
|
+
runEpochArg !== this.runEpoch
|
|
1282
|
+
|| (!this.running && !this.stopping)
|
|
1283
|
+
) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
this.terminallyStopped = true;
|
|
1287
|
+
this.acceptingFrames = false;
|
|
1288
|
+
this.inputBlocked = true;
|
|
1289
|
+
this.pendingViewport = undefined;
|
|
1290
|
+
if (this.stopping || this.stopRequested) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
this.stopRequested = true;
|
|
1294
|
+
void this.stop();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private validateTimeout(timeoutArg: number, nameArg: string): number {
|
|
1298
|
+
if (!Number.isFinite(timeoutArg) || timeoutArg <= 0) {
|
|
1299
|
+
throw new Error(`${nameArg} must be a positive finite number`);
|
|
1300
|
+
}
|
|
1301
|
+
return timeoutArg;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
private runClientOperation<T>(
|
|
1305
|
+
operationArg: () => Promise<T>,
|
|
1306
|
+
operationNameArg: string,
|
|
1307
|
+
): Promise<T> {
|
|
1308
|
+
let operation: Promise<T>;
|
|
1309
|
+
try {
|
|
1310
|
+
operation = operationArg();
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
return Promise.reject(error);
|
|
1313
|
+
}
|
|
1314
|
+
return this.withTimeout(operation, this.operationTimeoutMs, operationNameArg);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
private withTimeout<T>(
|
|
1318
|
+
operationArg: Promise<T>,
|
|
1319
|
+
timeoutMsArg: number,
|
|
1320
|
+
operationNameArg: string,
|
|
1321
|
+
): Promise<T> {
|
|
1322
|
+
return new Promise<T>((resolve, reject) => {
|
|
1323
|
+
let settled = false;
|
|
1324
|
+
const timeout = globalThis.setTimeout(() => {
|
|
1325
|
+
if (!settled) {
|
|
1326
|
+
settled = true;
|
|
1327
|
+
reject(new LiveBrowserOperationTimeoutError(
|
|
1328
|
+
`${operationNameArg} timed out after ${timeoutMsArg}ms`,
|
|
1329
|
+
));
|
|
1330
|
+
}
|
|
1331
|
+
}, timeoutMsArg);
|
|
1332
|
+
operationArg.then(
|
|
1333
|
+
(valueArg) => {
|
|
1334
|
+
if (!settled) {
|
|
1335
|
+
settled = true;
|
|
1336
|
+
globalThis.clearTimeout(timeout);
|
|
1337
|
+
resolve(valueArg);
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
(errorArg) => {
|
|
1341
|
+
if (!settled) {
|
|
1342
|
+
settled = true;
|
|
1343
|
+
globalThis.clearTimeout(timeout);
|
|
1344
|
+
reject(errorArg);
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
);
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
private async decodeFrame(blobArg: Blob): Promise<ImageBitmap> {
|
|
1352
|
+
let decodeOperation: Promise<ImageBitmap>;
|
|
1353
|
+
try {
|
|
1354
|
+
decodeOperation = createImageBitmap(blobArg);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
try {
|
|
1359
|
+
return await this.withTimeout(
|
|
1360
|
+
decodeOperation,
|
|
1361
|
+
this.frameDecodeTimeoutMs,
|
|
1362
|
+
'frame decode',
|
|
1363
|
+
);
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
void decodeOperation.then((lateBitmapArg) => lateBitmapArg.close(), () => undefined);
|
|
1366
|
+
throw error;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private releasePointerCapture(pointerIdArg: number): void {
|
|
1371
|
+
this.capturedPointerIds.delete(pointerIdArg);
|
|
1372
|
+
try {
|
|
1373
|
+
if (this.canvas.hasPointerCapture(pointerIdArg)) {
|
|
1374
|
+
this.canvas.releasePointerCapture(pointerIdArg);
|
|
1375
|
+
}
|
|
1376
|
+
} catch {
|
|
1377
|
+
// Pointer capture may already have been released by the browser.
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private releasePointerCaptures(): void {
|
|
1382
|
+
const pointerIds = [...this.capturedPointerIds];
|
|
1383
|
+
this.capturedPointerIds.clear();
|
|
1384
|
+
for (const pointerId of pointerIds) {
|
|
1385
|
+
this.releasePointerCapture(pointerId);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|