@push.rocks/smartbrowser 4.0.2 → 4.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/classes.livebrowsercanvasrenderer.d.ts +61 -5
- package/dist_ts_web/classes.livebrowsercanvasrenderer.js +475 -134
- package/dist_ts_web/interfaces.livebrowsercanvas.d.ts +27 -1
- package/package.json +3 -3
- package/readme.hints.md +8 -6
- package/readme.md +10 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.livebrowsercanvasrenderer.ts +558 -144
- package/ts_web/interfaces.livebrowsercanvas.ts +29 -1
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ILiveBrowserMouseInput,
|
|
7
7
|
ILiveBrowserState,
|
|
8
8
|
ILiveBrowserViewport,
|
|
9
|
+
ILiveBrowserWheelInput,
|
|
9
10
|
TLiveBrowserEvent,
|
|
10
11
|
} from '@push.rocks/smartpuppeteer';
|
|
11
12
|
|
|
@@ -13,9 +14,15 @@ import type {
|
|
|
13
14
|
ILiveBrowserCanvasError,
|
|
14
15
|
ILiveBrowserCanvasOperationOptions,
|
|
15
16
|
ILiveBrowserCanvasRendererOptions,
|
|
17
|
+
ILiveBrowserCanvasRendererStatistics,
|
|
16
18
|
} from './interfaces.livebrowsercanvas.js';
|
|
17
19
|
|
|
18
20
|
const maxInputQueueLength = 128;
|
|
21
|
+
const maxInFlightInputCommands = 4;
|
|
22
|
+
const maxQueuedCoalescableCommands = 32;
|
|
23
|
+
const maxWheelDelta = 1000000;
|
|
24
|
+
const animationFrameFallbackMs = 16;
|
|
25
|
+
const resizeDebounceMs = 100;
|
|
19
26
|
const maxPendingAcknowledgements = 16;
|
|
20
27
|
const maxInputReleaseAttempts = 2;
|
|
21
28
|
const maxFrameDimension = 12288;
|
|
@@ -45,20 +52,67 @@ interface IDisplayedFrame extends ILiveBrowserInputBase {
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
type TInputCommandKind = 'key' | 'mouse' | 'mouseMove' | 'text' | 'wheel';
|
|
55
|
+
type TCoalescableInputCommandKind = 'mouseMove' | 'wheel';
|
|
48
56
|
|
|
49
57
|
interface IInputCommand {
|
|
50
58
|
kind: TInputCommandKind;
|
|
51
59
|
runEpoch: number;
|
|
52
60
|
execute: (optionsArg: ILiveBrowserCanvasOperationOptions) => Promise<void>;
|
|
61
|
+
/** Mutable merged wheel payload that execute() reads at dispatch time. */
|
|
62
|
+
wheelInput?: ILiveBrowserWheelInput;
|
|
53
63
|
onAttempt?: () => void;
|
|
54
64
|
onSuccess?: () => void;
|
|
55
65
|
resolve: () => void;
|
|
56
66
|
reject: (errorArg: unknown) => void;
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
interface IPendingWheel extends ILiveBrowserWheelInput {
|
|
70
|
+
runEpoch: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface IPendingDraw {
|
|
74
|
+
bitmap: ImageBitmap;
|
|
75
|
+
frame: ILiveBrowserFrame;
|
|
76
|
+
runEpoch: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface IScheduledFrame {
|
|
80
|
+
animationFrameId?: number;
|
|
81
|
+
timeoutId?: ReturnType<typeof setTimeout>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isCoalescableInputCommand = (
|
|
85
|
+
kindArg: TInputCommandKind,
|
|
86
|
+
): kindArg is TCoalescableInputCommandKind => kindArg === 'mouseMove' || kindArg === 'wheel';
|
|
87
|
+
|
|
88
|
+
const clampWheelDelta = (valueArg: number): number => (
|
|
89
|
+
Math.max(-maxWheelDelta, Math.min(maxWheelDelta, valueArg))
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const mergeWheelInput = (
|
|
93
|
+
targetArg: ILiveBrowserWheelInput,
|
|
94
|
+
sourceArg: ILiveBrowserWheelInput,
|
|
95
|
+
): void => {
|
|
96
|
+
targetArg.x = sourceArg.x;
|
|
97
|
+
targetArg.y = sourceArg.y;
|
|
98
|
+
targetArg.deltaX = clampWheelDelta(targetArg.deltaX + sourceArg.deltaX);
|
|
99
|
+
targetArg.deltaY = clampWheelDelta(targetArg.deltaY + sourceArg.deltaY);
|
|
100
|
+
targetArg.modifiers = sourceArg.modifiers;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const inputIdentityEquals = (
|
|
104
|
+
firstArg: ILiveBrowserInputBase,
|
|
105
|
+
secondArg: ILiveBrowserInputBase,
|
|
106
|
+
): boolean => (
|
|
107
|
+
firstArg.tabId === secondArg.tabId
|
|
108
|
+
&& firstArg.generation === secondArg.generation
|
|
109
|
+
&& firstArg.viewportRevision === secondArg.viewportRevision
|
|
110
|
+
);
|
|
111
|
+
|
|
59
112
|
interface IResizeFence {
|
|
60
113
|
minimumViewportRevision: number;
|
|
61
114
|
target: ILiveBrowserViewport;
|
|
115
|
+
negotiated?: boolean;
|
|
62
116
|
}
|
|
63
117
|
|
|
64
118
|
const viewportEquals = (
|
|
@@ -102,7 +156,8 @@ const getErrorMessage = (errorArg: unknown): string => (
|
|
|
102
156
|
*/
|
|
103
157
|
export class LiveBrowserCanvasRenderer {
|
|
104
158
|
private readonly canvas: HTMLCanvasElement;
|
|
105
|
-
private readonly canvasContext
|
|
159
|
+
private readonly canvasContext?: CanvasRenderingContext2D;
|
|
160
|
+
private readonly bitmapContext?: ImageBitmapRenderingContext;
|
|
106
161
|
private readonly client: ILiveBrowserCanvasRendererOptions['client'];
|
|
107
162
|
private readonly focusTarget: HTMLElement;
|
|
108
163
|
private readonly resizeTarget?: Element;
|
|
@@ -135,17 +190,22 @@ export class LiveBrowserCanvasRenderer {
|
|
|
135
190
|
private highestFrameSequence = -1;
|
|
136
191
|
private queuedFrame?: IFrameWork;
|
|
137
192
|
private frameProcessingPromise?: Promise<void>;
|
|
193
|
+
private pendingDraw?: IPendingDraw;
|
|
194
|
+
private drawSchedule?: IScheduledFrame;
|
|
138
195
|
private acknowledgementPromises = new Set<Promise<void>>();
|
|
139
196
|
private rawDecodeSettlementPromises = new Set<Promise<void>>();
|
|
140
197
|
|
|
141
198
|
private inputBlocked = true;
|
|
142
199
|
private inputCommands: IInputCommand[] = [];
|
|
143
|
-
private
|
|
200
|
+
private inFlightInputCommands = 0;
|
|
201
|
+
private inFlightWheelCommands = 0;
|
|
144
202
|
private inputIdleResolvers: Array<() => void> = [];
|
|
145
203
|
private inputResetTail: Promise<void> = Promise.resolve();
|
|
146
204
|
private inputResetPromise?: Promise<void>;
|
|
147
205
|
private inputResetCount = 0;
|
|
148
|
-
private
|
|
206
|
+
private pendingWheel?: IPendingWheel;
|
|
207
|
+
private wheelFlushSchedule?: IScheduledFrame;
|
|
208
|
+
private wheelFrameElapsed = true;
|
|
149
209
|
private pressedKeys = new Map<string, ILiveBrowserKeyInput>();
|
|
150
210
|
private pressedMouseButtons = new Map<string, ILiveBrowserMouseInput>();
|
|
151
211
|
private capturedPointerIds = new Set<number>();
|
|
@@ -155,6 +215,18 @@ export class LiveBrowserCanvasRenderer {
|
|
|
155
215
|
private viewportProcessing = false;
|
|
156
216
|
private viewportProcessingPromise?: Promise<void>;
|
|
157
217
|
private resizeFence?: IResizeFence;
|
|
218
|
+
private lastRequestedViewport?: ILiveBrowserViewport;
|
|
219
|
+
private resizeDebounceTimer?: ReturnType<typeof setTimeout>;
|
|
220
|
+
private pendingResizeSize?: { width: number; height: number };
|
|
221
|
+
|
|
222
|
+
private readonly statistics: Omit<ILiveBrowserCanvasRendererStatistics, 'inputCommandsInFlight'> = {
|
|
223
|
+
framesReceived: 0,
|
|
224
|
+
framesDecoded: 0,
|
|
225
|
+
framesSkipped: 0,
|
|
226
|
+
inputCommandsEnqueued: 0,
|
|
227
|
+
inputCommandsCoalesced: 0,
|
|
228
|
+
lastInputRoundTripMs: 0,
|
|
229
|
+
};
|
|
158
230
|
|
|
159
231
|
constructor(optionsArg: ILiveBrowserCanvasRendererOptions) {
|
|
160
232
|
this.canvas = optionsArg.canvas;
|
|
@@ -176,11 +248,17 @@ export class LiveBrowserCanvasRenderer {
|
|
|
176
248
|
this.initialCanvasWidth = this.canvas.width;
|
|
177
249
|
this.initialCanvasHeight = this.canvas.height;
|
|
178
250
|
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
251
|
+
const bitmapContext = this.canvas.getContext('bitmaprenderer');
|
|
252
|
+
if (bitmapContext) {
|
|
253
|
+
this.bitmapContext = bitmapContext;
|
|
254
|
+
} else {
|
|
255
|
+
// A canvas that already owns a 2D context keeps the 2D drawing path.
|
|
256
|
+
const canvasContext = this.canvas.getContext('2d');
|
|
257
|
+
if (!canvasContext) {
|
|
258
|
+
throw new Error('LiveBrowserCanvasRenderer requires a bitmaprenderer or 2D canvas context');
|
|
259
|
+
}
|
|
260
|
+
this.canvasContext = canvasContext;
|
|
182
261
|
}
|
|
183
|
-
this.canvasContext = canvasContext;
|
|
184
262
|
}
|
|
185
263
|
|
|
186
264
|
public get isRunning(): boolean {
|
|
@@ -191,6 +269,16 @@ export class LiveBrowserCanvasRenderer {
|
|
|
191
269
|
return this.suspended;
|
|
192
270
|
}
|
|
193
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Cumulative frame and input counters for this renderer instance across runs.
|
|
274
|
+
*/
|
|
275
|
+
public getStatistics(): ILiveBrowserCanvasRendererStatistics {
|
|
276
|
+
return {
|
|
277
|
+
...this.statistics,
|
|
278
|
+
inputCommandsInFlight: this.inFlightInputCommands,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
194
282
|
public start(): Promise<void> {
|
|
195
283
|
return this.enqueueLifecycle(async () => {
|
|
196
284
|
if (this.running) {
|
|
@@ -299,6 +387,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
299
387
|
this.interruptionRequested = false;
|
|
300
388
|
this.acceptingFrames = true;
|
|
301
389
|
this.inputBlocked = true;
|
|
390
|
+
this.wheelFrameElapsed = true;
|
|
302
391
|
this.state = undefined;
|
|
303
392
|
this.displayedFrame = undefined;
|
|
304
393
|
this.highestFrameSequence = -1;
|
|
@@ -335,6 +424,8 @@ export class LiveBrowserCanvasRenderer {
|
|
|
335
424
|
this.queuedFrame = undefined;
|
|
336
425
|
this.displayedFrame = undefined;
|
|
337
426
|
this.resizeFence = undefined;
|
|
427
|
+
this.discardPendingDraw();
|
|
428
|
+
this.discardPendingWheel();
|
|
338
429
|
this.releasePointerCaptures();
|
|
339
430
|
this.clearCanvas();
|
|
340
431
|
|
|
@@ -352,7 +443,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
352
443
|
}
|
|
353
444
|
this.pressedKeys.clear();
|
|
354
445
|
this.pressedMouseButtons.clear();
|
|
355
|
-
this.inputOverflowRecoveryPending = false;
|
|
356
446
|
}
|
|
357
447
|
|
|
358
448
|
private async finishRun(runEpochArg: number, suspendArg: boolean): Promise<void> {
|
|
@@ -363,6 +453,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
363
453
|
try {
|
|
364
454
|
this.resizeObserver?.disconnect();
|
|
365
455
|
this.resizeObserver = undefined;
|
|
456
|
+
this.clearResizeDebounce();
|
|
366
457
|
this.listenerController?.abort();
|
|
367
458
|
this.listenerController = undefined;
|
|
368
459
|
const unsubscribe = this.unsubscribe;
|
|
@@ -402,8 +493,12 @@ export class LiveBrowserCanvasRenderer {
|
|
|
402
493
|
this.highestFrameSequence = -1;
|
|
403
494
|
this.queuedFrame = undefined;
|
|
404
495
|
this.frameProcessingPromise = undefined;
|
|
496
|
+
this.discardPendingDraw();
|
|
497
|
+
this.discardPendingWheel();
|
|
498
|
+
this.clearResizeDebounce();
|
|
405
499
|
this.acknowledgementPromises.clear();
|
|
406
500
|
this.resizeFence = undefined;
|
|
501
|
+
this.lastRequestedViewport = undefined;
|
|
407
502
|
this.pendingViewport = undefined;
|
|
408
503
|
this.inFlightViewport = undefined;
|
|
409
504
|
this.viewportProcessing = false;
|
|
@@ -498,19 +593,37 @@ export class LiveBrowserCanvasRenderer {
|
|
|
498
593
|
}
|
|
499
594
|
this.resizeObserver = new ResizeObserver((entriesArg) => {
|
|
500
595
|
const matchingEntry = entriesArg.find((entryArg) => entryArg.target === this.resizeTarget);
|
|
501
|
-
if (!matchingEntry) {
|
|
596
|
+
if (!matchingEntry || runEpochArg !== this.runEpoch) {
|
|
502
597
|
return;
|
|
503
598
|
}
|
|
504
|
-
this.
|
|
505
|
-
matchingEntry.contentRect.width,
|
|
506
|
-
matchingEntry.contentRect.height,
|
|
507
|
-
|
|
508
|
-
)
|
|
599
|
+
this.pendingResizeSize = {
|
|
600
|
+
width: matchingEntry.contentRect.width,
|
|
601
|
+
height: matchingEntry.contentRect.height,
|
|
602
|
+
};
|
|
603
|
+
if (this.resizeDebounceTimer !== undefined) {
|
|
604
|
+
globalThis.clearTimeout(this.resizeDebounceTimer);
|
|
605
|
+
}
|
|
606
|
+
this.resizeDebounceTimer = globalThis.setTimeout(() => {
|
|
607
|
+
this.resizeDebounceTimer = undefined;
|
|
608
|
+
const pendingResizeSize = this.pendingResizeSize;
|
|
609
|
+
this.pendingResizeSize = undefined;
|
|
610
|
+
if (pendingResizeSize) {
|
|
611
|
+
this.queueViewport(pendingResizeSize.width, pendingResizeSize.height, runEpochArg);
|
|
612
|
+
}
|
|
613
|
+
}, resizeDebounceMs);
|
|
509
614
|
});
|
|
510
615
|
this.resizeObserver.observe(this.resizeTarget);
|
|
511
616
|
this.syncViewport();
|
|
512
617
|
}
|
|
513
618
|
|
|
619
|
+
private clearResizeDebounce(): void {
|
|
620
|
+
if (this.resizeDebounceTimer !== undefined) {
|
|
621
|
+
globalThis.clearTimeout(this.resizeDebounceTimer);
|
|
622
|
+
this.resizeDebounceTimer = undefined;
|
|
623
|
+
}
|
|
624
|
+
this.pendingResizeSize = undefined;
|
|
625
|
+
}
|
|
626
|
+
|
|
514
627
|
private handleEvent(eventArg: TLiveBrowserEvent, runEpochArg: number): void {
|
|
515
628
|
if (eventArg.type === 'frame') {
|
|
516
629
|
this.queueFrame(eventArg.frame, runEpochArg);
|
|
@@ -535,6 +648,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
535
648
|
return;
|
|
536
649
|
}
|
|
537
650
|
this.state = stateArg;
|
|
651
|
+
this.followNegotiatedViewport();
|
|
538
652
|
|
|
539
653
|
if (this.displayedFrame && this.isStateAheadOfDisplayedFrame(stateArg, this.displayedFrame)) {
|
|
540
654
|
this.displayedFrame = undefined;
|
|
@@ -564,6 +678,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
564
678
|
if (!this.running || !this.acceptingFrames || runEpochArg !== this.runEpoch) {
|
|
565
679
|
return;
|
|
566
680
|
}
|
|
681
|
+
this.statistics.framesReceived++;
|
|
567
682
|
|
|
568
683
|
try {
|
|
569
684
|
this.validateFrame(frameArg);
|
|
@@ -590,11 +705,12 @@ export class LiveBrowserCanvasRenderer {
|
|
|
590
705
|
acknowledgementAttempted: false,
|
|
591
706
|
};
|
|
592
707
|
if (this.queuedFrame) {
|
|
593
|
-
|
|
708
|
+
// The superseded frame was already acknowledged at receipt.
|
|
594
709
|
this.queuedFrame = undefined;
|
|
595
|
-
|
|
710
|
+
this.statistics.framesSkipped++;
|
|
596
711
|
}
|
|
597
712
|
this.queuedFrame = frameWork;
|
|
713
|
+
void this.acknowledgeFrame(frameWork);
|
|
598
714
|
|
|
599
715
|
this.ensureFrameProcessor();
|
|
600
716
|
}
|
|
@@ -643,32 +759,14 @@ export class LiveBrowserCanvasRenderer {
|
|
|
643
759
|
`decoded frame dimensions ${imageBitmap.width}x${imageBitmap.height} do not match ${frame.width}x${frame.height}`,
|
|
644
760
|
);
|
|
645
761
|
}
|
|
646
|
-
|
|
762
|
+
this.statistics.framesDecoded++;
|
|
763
|
+
if (!this.canRenderFrame(frame, runEpoch)) {
|
|
764
|
+
this.statistics.framesSkipped++;
|
|
647
765
|
return;
|
|
648
766
|
}
|
|
649
767
|
|
|
650
|
-
this.
|
|
651
|
-
|
|
652
|
-
this.canvasContext.clearRect(0, 0, frame.width, frame.height);
|
|
653
|
-
this.canvasContext.drawImage(imageBitmap, 0, 0, frame.width, frame.height);
|
|
654
|
-
this.displayedFrame = {
|
|
655
|
-
tabId: frame.tabId,
|
|
656
|
-
sequence: frame.sequence,
|
|
657
|
-
generation: frame.generation,
|
|
658
|
-
viewportRevision: frame.viewportRevision,
|
|
659
|
-
viewport: { ...frame.viewport },
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
if (
|
|
663
|
-
this.resizeFence
|
|
664
|
-
&& !this.viewportProcessing
|
|
665
|
-
&& frame.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
666
|
-
&& viewportEquals(frame.viewport, this.resizeFence.target)
|
|
667
|
-
) {
|
|
668
|
-
this.resizeFence = undefined;
|
|
669
|
-
}
|
|
670
|
-
this.updateInputAvailability();
|
|
671
|
-
this.callFrameRendered(frame, runEpoch);
|
|
768
|
+
this.setPendingDraw({ bitmap: imageBitmap, frame, runEpoch });
|
|
769
|
+
imageBitmap = undefined;
|
|
672
770
|
} catch (error) {
|
|
673
771
|
if (error instanceof LiveBrowserRunAbortedError) {
|
|
674
772
|
return;
|
|
@@ -677,7 +775,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
677
775
|
terminalError = error;
|
|
678
776
|
} finally {
|
|
679
777
|
imageBitmap?.close();
|
|
680
|
-
await this.acknowledgeFrame(frameWorkArg);
|
|
681
778
|
if (terminalFailure) {
|
|
682
779
|
this.requestSuspend(runEpoch);
|
|
683
780
|
this.reportError({
|
|
@@ -690,12 +787,109 @@ export class LiveBrowserCanvasRenderer {
|
|
|
690
787
|
}
|
|
691
788
|
}
|
|
692
789
|
|
|
790
|
+
/**
|
|
791
|
+
* Keeps only the newest decoded bitmap and presents it on the next animation frame.
|
|
792
|
+
*/
|
|
793
|
+
private setPendingDraw(pendingDrawArg: IPendingDraw): void {
|
|
794
|
+
if (this.pendingDraw) {
|
|
795
|
+
this.pendingDraw.bitmap.close();
|
|
796
|
+
this.statistics.framesSkipped++;
|
|
797
|
+
}
|
|
798
|
+
this.pendingDraw = pendingDrawArg;
|
|
799
|
+
if (!this.drawSchedule) {
|
|
800
|
+
this.drawSchedule = this.scheduleAnimationFrame(() => {
|
|
801
|
+
this.drawSchedule = undefined;
|
|
802
|
+
this.drawPendingFrame();
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private discardPendingDraw(): void {
|
|
808
|
+
if (this.drawSchedule) {
|
|
809
|
+
this.cancelScheduledFrame(this.drawSchedule);
|
|
810
|
+
this.drawSchedule = undefined;
|
|
811
|
+
}
|
|
812
|
+
const pendingDraw = this.pendingDraw;
|
|
813
|
+
this.pendingDraw = undefined;
|
|
814
|
+
if (pendingDraw) {
|
|
815
|
+
pendingDraw.bitmap.close();
|
|
816
|
+
this.statistics.framesSkipped++;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private drawPendingFrame(): void {
|
|
821
|
+
const pendingDraw = this.pendingDraw;
|
|
822
|
+
this.pendingDraw = undefined;
|
|
823
|
+
if (!pendingDraw) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const { bitmap, frame, runEpoch } = pendingDraw;
|
|
827
|
+
if (!this.canRenderFrame(frame, runEpoch)) {
|
|
828
|
+
bitmap.close();
|
|
829
|
+
this.statistics.framesSkipped++;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
this.paintBitmap(bitmap, frame.width, frame.height);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
bitmap.close();
|
|
836
|
+
this.requestSuspend(runEpoch);
|
|
837
|
+
this.reportError({
|
|
838
|
+
code: 'frame_render_failed',
|
|
839
|
+
message: `Could not render live browser frame: ${getErrorMessage(error)}`,
|
|
840
|
+
cause: error,
|
|
841
|
+
frame,
|
|
842
|
+
}, runEpoch);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
this.displayedFrame = {
|
|
846
|
+
tabId: frame.tabId,
|
|
847
|
+
sequence: frame.sequence,
|
|
848
|
+
generation: frame.generation,
|
|
849
|
+
viewportRevision: frame.viewportRevision,
|
|
850
|
+
viewport: { ...frame.viewport },
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
if (
|
|
854
|
+
this.resizeFence
|
|
855
|
+
&& !this.viewportProcessing
|
|
856
|
+
&& frame.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
857
|
+
&& viewportEquals(frame.viewport, this.resizeFence.target)
|
|
858
|
+
) {
|
|
859
|
+
this.resizeFence = undefined;
|
|
860
|
+
}
|
|
861
|
+
this.updateInputAvailability();
|
|
862
|
+
this.callFrameRendered(frame, runEpoch);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Presents one decoded bitmap. The canvas backing store is reallocated only
|
|
867
|
+
* when the frame dimensions differ from the current intrinsic dimensions.
|
|
868
|
+
*/
|
|
869
|
+
private paintBitmap(bitmapArg: ImageBitmap, widthArg: number, heightArg: number): void {
|
|
870
|
+
if (this.canvas.width !== widthArg) {
|
|
871
|
+
this.canvas.width = widthArg;
|
|
872
|
+
}
|
|
873
|
+
if (this.canvas.height !== heightArg) {
|
|
874
|
+
this.canvas.height = heightArg;
|
|
875
|
+
}
|
|
876
|
+
if (this.bitmapContext) {
|
|
877
|
+
this.bitmapContext.transferFromImageBitmap(bitmapArg);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (!this.canvasContext) {
|
|
881
|
+
throw new Error('LiveBrowserCanvasRenderer has no rendering context');
|
|
882
|
+
}
|
|
883
|
+
this.canvasContext.clearRect(0, 0, widthArg, heightArg);
|
|
884
|
+
this.canvasContext.drawImage(bitmapArg, 0, 0, widthArg, heightArg);
|
|
885
|
+
bitmapArg.close();
|
|
886
|
+
}
|
|
887
|
+
|
|
693
888
|
private canRenderFrame(frameArg: ILiveBrowserFrame, runEpochArg: number): boolean {
|
|
694
889
|
if (
|
|
695
890
|
!this.running
|
|
696
891
|
|| !this.acceptingFrames
|
|
697
892
|
|| runEpochArg !== this.runEpoch
|
|
698
|
-
|| this.viewportProcessing
|
|
699
893
|
) {
|
|
700
894
|
return false;
|
|
701
895
|
}
|
|
@@ -713,7 +907,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
713
907
|
) {
|
|
714
908
|
return false;
|
|
715
909
|
}
|
|
716
|
-
if (this.resizeFence) {
|
|
910
|
+
if (this.resizeFence && !this.viewportProcessing) {
|
|
717
911
|
return frameArg.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
718
912
|
&& viewportEquals(frameArg.viewport, this.resizeFence.target);
|
|
719
913
|
}
|
|
@@ -889,20 +1083,110 @@ export class LiveBrowserCanvasRenderer {
|
|
|
889
1083
|
: eventArg.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
|
890
1084
|
? displayedFrame.viewport.height
|
|
891
1085
|
: 1;
|
|
892
|
-
const
|
|
1086
|
+
const inputIdentity = createInputIdentity(displayedFrame);
|
|
1087
|
+
const modifiers = createModifiers(eventArg);
|
|
1088
|
+
this.statistics.inputCommandsEnqueued++;
|
|
1089
|
+
const pendingWheel = this.pendingWheel;
|
|
1090
|
+
if (
|
|
1091
|
+
pendingWheel
|
|
1092
|
+
&& pendingWheel.runEpoch === runEpochArg
|
|
1093
|
+
&& inputIdentityEquals(pendingWheel, inputIdentity)
|
|
1094
|
+
) {
|
|
1095
|
+
mergeWheelInput(pendingWheel, {
|
|
1096
|
+
...inputIdentity,
|
|
1097
|
+
...coordinates,
|
|
1098
|
+
deltaX: eventArg.deltaX * multiplier,
|
|
1099
|
+
deltaY: eventArg.deltaY * multiplier,
|
|
1100
|
+
modifiers,
|
|
1101
|
+
});
|
|
1102
|
+
this.statistics.inputCommandsCoalesced++;
|
|
1103
|
+
} else {
|
|
1104
|
+
if (pendingWheel) {
|
|
1105
|
+
this.flushPendingWheel();
|
|
1106
|
+
}
|
|
1107
|
+
this.pendingWheel = {
|
|
1108
|
+
...inputIdentity,
|
|
1109
|
+
...coordinates,
|
|
1110
|
+
deltaX: clampWheelDelta(eventArg.deltaX * multiplier),
|
|
1111
|
+
deltaY: clampWheelDelta(eventArg.deltaY * multiplier),
|
|
1112
|
+
modifiers,
|
|
1113
|
+
runEpoch: runEpochArg,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
this.tryFlushPendingWheel();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Flushes accumulated wheel input at most once per animation frame and only
|
|
1121
|
+
* while no wheel dispatch is in flight; the settling dispatch flushes the rest.
|
|
1122
|
+
*/
|
|
1123
|
+
private tryFlushPendingWheel(): void {
|
|
1124
|
+
if (!this.pendingWheel) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (!this.wheelFrameElapsed) {
|
|
1128
|
+
this.scheduleWheelFlush();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (this.inFlightWheelCommands > 0) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
this.flushPendingWheel();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private scheduleWheelFlush(): void {
|
|
1138
|
+
if (this.wheelFlushSchedule) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
this.wheelFlushSchedule = this.scheduleAnimationFrame(() => {
|
|
1142
|
+
this.wheelFlushSchedule = undefined;
|
|
1143
|
+
this.wheelFrameElapsed = true;
|
|
1144
|
+
this.tryFlushPendingWheel();
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Moves the accumulated wheel input into the command queue regardless of the
|
|
1150
|
+
* frame gate so that later discrete commands stay behind it.
|
|
1151
|
+
*/
|
|
1152
|
+
private flushPendingWheel(): void {
|
|
1153
|
+
const pendingWheel = this.pendingWheel;
|
|
1154
|
+
if (!pendingWheel) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
this.pendingWheel = undefined;
|
|
1158
|
+
this.wheelFrameElapsed = false;
|
|
1159
|
+
this.scheduleWheelFlush();
|
|
1160
|
+
const displayedFrame = this.getInputFrame();
|
|
1161
|
+
if (
|
|
1162
|
+
!displayedFrame
|
|
1163
|
+
|| pendingWheel.runEpoch !== this.runEpoch
|
|
1164
|
+
|| !inputIdentityEquals(pendingWheel, createInputIdentity(displayedFrame))
|
|
1165
|
+
) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const { runEpoch, ...wheelInput } = pendingWheel;
|
|
893
1169
|
void this.enqueueInputCommand({
|
|
894
1170
|
kind: 'wheel',
|
|
895
|
-
runEpoch
|
|
1171
|
+
runEpoch,
|
|
1172
|
+
wheelInput,
|
|
896
1173
|
execute: async (optionsArg) => this.client.dispatchWheel({
|
|
897
|
-
...
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
deltaY: clampDelta(eventArg.deltaY * multiplier),
|
|
901
|
-
modifiers: createModifiers(eventArg),
|
|
1174
|
+
...wheelInput,
|
|
1175
|
+
deltaX: clampWheelDelta(wheelInput.deltaX),
|
|
1176
|
+
deltaY: clampWheelDelta(wheelInput.deltaY),
|
|
902
1177
|
}, optionsArg),
|
|
903
1178
|
}).catch(() => undefined);
|
|
904
1179
|
}
|
|
905
1180
|
|
|
1181
|
+
private discardPendingWheel(): void {
|
|
1182
|
+
this.pendingWheel = undefined;
|
|
1183
|
+
if (this.wheelFlushSchedule) {
|
|
1184
|
+
this.cancelScheduledFrame(this.wheelFlushSchedule);
|
|
1185
|
+
this.wheelFlushSchedule = undefined;
|
|
1186
|
+
}
|
|
1187
|
+
this.wheelFrameElapsed = true;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
906
1190
|
private handleKey(
|
|
907
1191
|
eventArg: KeyboardEvent,
|
|
908
1192
|
typeArg: 'down' | 'up',
|
|
@@ -1028,97 +1312,175 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1028
1312
|
resolve,
|
|
1029
1313
|
reject,
|
|
1030
1314
|
};
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
this.inputCommands
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1315
|
+
if (isCoalescableInputCommand(command.kind)) {
|
|
1316
|
+
this.enqueueCoalescableCommand(command);
|
|
1317
|
+
} else {
|
|
1318
|
+
const queuedDiscreteCommands = this.inputCommands.filter((queuedCommand) => (
|
|
1319
|
+
!isCoalescableInputCommand(queuedCommand.kind)
|
|
1320
|
+
)).length;
|
|
1321
|
+
if (queuedDiscreteCommands >= maxInputQueueLength) {
|
|
1322
|
+
const error = new Error('Live browser input queue reached its capacity');
|
|
1323
|
+
this.reportError({
|
|
1324
|
+
code: 'input_queue_capacity_exceeded',
|
|
1325
|
+
message: error.message,
|
|
1326
|
+
cause: error,
|
|
1327
|
+
}, command.runEpoch);
|
|
1328
|
+
reject(error);
|
|
1038
1329
|
return;
|
|
1039
1330
|
}
|
|
1040
|
-
|
|
1041
|
-
this.
|
|
1042
|
-
|
|
1043
|
-
message: error.message,
|
|
1044
|
-
cause: error,
|
|
1045
|
-
}, command.runEpoch);
|
|
1046
|
-
reject(error);
|
|
1047
|
-
this.recoverFromInputOverflow(command.runEpoch);
|
|
1048
|
-
return;
|
|
1049
|
-
} else {
|
|
1331
|
+
// Accumulated wheel input precedes this command in event order.
|
|
1332
|
+
this.flushPendingWheel();
|
|
1333
|
+
this.statistics.inputCommandsEnqueued++;
|
|
1050
1334
|
this.inputCommands.push(command);
|
|
1051
1335
|
}
|
|
1052
1336
|
this.processInputCommands();
|
|
1053
1337
|
});
|
|
1054
1338
|
}
|
|
1055
1339
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1340
|
+
/**
|
|
1341
|
+
* Coalescable commands never exhaust capacity: they merge into a same-kind
|
|
1342
|
+
* queue tail or evict the oldest queued coalescable command under pressure.
|
|
1343
|
+
*/
|
|
1344
|
+
private enqueueCoalescableCommand(commandArg: IInputCommand): void {
|
|
1345
|
+
// Wheel submissions are counted per DOM event before accumulation.
|
|
1346
|
+
if (commandArg.kind === 'mouseMove') {
|
|
1347
|
+
this.statistics.inputCommandsEnqueued++;
|
|
1348
|
+
}
|
|
1349
|
+
const lastCommand = this.inputCommands.at(-1);
|
|
1350
|
+
if (lastCommand && lastCommand.kind === commandArg.kind) {
|
|
1351
|
+
this.statistics.inputCommandsCoalesced++;
|
|
1352
|
+
if (commandArg.kind === 'wheel' && lastCommand.wheelInput && commandArg.wheelInput) {
|
|
1353
|
+
mergeWheelInput(lastCommand.wheelInput, commandArg.wheelInput);
|
|
1354
|
+
commandArg.resolve();
|
|
1355
|
+
} else {
|
|
1356
|
+
lastCommand.resolve();
|
|
1357
|
+
this.inputCommands[this.inputCommands.length - 1] = commandArg;
|
|
1358
|
+
}
|
|
1058
1359
|
return;
|
|
1059
1360
|
}
|
|
1060
|
-
this.
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1361
|
+
const queuedCoalescableCommands = this.inputCommands.filter((queuedCommand) => (
|
|
1362
|
+
isCoalescableInputCommand(queuedCommand.kind)
|
|
1363
|
+
));
|
|
1364
|
+
if (queuedCoalescableCommands.length >= maxQueuedCoalescableCommands) {
|
|
1365
|
+
this.dropOldestCoalescableCommand(queuedCoalescableCommands[0]);
|
|
1366
|
+
}
|
|
1367
|
+
this.inputCommands.push(commandArg);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private dropOldestCoalescableCommand(oldestCommandArg: IInputCommand): void {
|
|
1371
|
+
const index = this.inputCommands.indexOf(oldestCommandArg);
|
|
1372
|
+
if (index < 0) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
this.inputCommands.splice(index, 1);
|
|
1376
|
+
this.statistics.inputCommandsCoalesced++;
|
|
1377
|
+
if (oldestCommandArg.kind === 'wheel' && oldestCommandArg.wheelInput) {
|
|
1378
|
+
const nextWheelCommand = this.inputCommands.find((queuedCommand) => (
|
|
1379
|
+
queuedCommand.kind === 'wheel' && queuedCommand.wheelInput
|
|
1380
|
+
));
|
|
1381
|
+
if (nextWheelCommand?.wheelInput) {
|
|
1382
|
+
const nextWheelInput = nextWheelCommand.wheelInput;
|
|
1383
|
+
nextWheelInput.deltaX = clampWheelDelta(
|
|
1384
|
+
nextWheelInput.deltaX + oldestCommandArg.wheelInput.deltaX,
|
|
1385
|
+
);
|
|
1386
|
+
nextWheelInput.deltaY = clampWheelDelta(
|
|
1387
|
+
nextWheelInput.deltaY + oldestCommandArg.wheelInput.deltaY,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
oldestCommandArg.resolve();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Dispatches queued commands in order with a bounded number in flight. The
|
|
1396
|
+
* remote session processes operations sequentially, so order is preserved.
|
|
1397
|
+
*/
|
|
1398
|
+
private processInputCommands(): void {
|
|
1399
|
+
while (
|
|
1400
|
+
this.inputCommands.length > 0
|
|
1401
|
+
&& this.inFlightInputCommands < maxInFlightInputCommands
|
|
1402
|
+
) {
|
|
1403
|
+
const command = this.inputCommands.shift()!;
|
|
1404
|
+
if (!this.isRunActive(command.runEpoch)) {
|
|
1405
|
+
command.reject(new LiveBrowserRunAbortedError(
|
|
1406
|
+
`Live browser input belongs to inactive run ${command.runEpoch}`,
|
|
1407
|
+
));
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
this.dispatchInputCommand(command);
|
|
1411
|
+
}
|
|
1412
|
+
if (this.inputCommands.length === 0 && this.inFlightInputCommands === 0) {
|
|
1413
|
+
const resolvers = this.inputIdleResolvers;
|
|
1414
|
+
this.inputIdleResolvers = [];
|
|
1415
|
+
for (const resolver of resolvers) {
|
|
1416
|
+
resolver();
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private dispatchInputCommand(commandArg: IInputCommand): void {
|
|
1422
|
+
this.inFlightInputCommands++;
|
|
1423
|
+
if (commandArg.kind === 'wheel') {
|
|
1424
|
+
this.inFlightWheelCommands++;
|
|
1425
|
+
}
|
|
1426
|
+
const startedAt = performance.now();
|
|
1427
|
+
let dispatch: Promise<void>;
|
|
1428
|
+
try {
|
|
1429
|
+
commandArg.onAttempt?.();
|
|
1430
|
+
dispatch = this.runClientOperation(commandArg.execute, 'input dispatch', commandArg.runEpoch);
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
dispatch = Promise.reject(error);
|
|
1433
|
+
}
|
|
1434
|
+
void dispatch.then(() => {
|
|
1435
|
+
if (!this.isRunActive(commandArg.runEpoch)) {
|
|
1436
|
+
throw new LiveBrowserRunAbortedError(
|
|
1437
|
+
`Live browser input belongs to inactive run ${commandArg.runEpoch}`,
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
this.statistics.lastInputRoundTripMs = performance.now() - startedAt;
|
|
1441
|
+
commandArg.onSuccess?.();
|
|
1442
|
+
commandArg.resolve();
|
|
1443
|
+
}).catch((error) => {
|
|
1444
|
+
commandArg.reject(error);
|
|
1445
|
+
this.handleInputDispatchFailure(error, commandArg.runEpoch);
|
|
1446
|
+
}).finally(() => {
|
|
1447
|
+
this.inFlightInputCommands--;
|
|
1448
|
+
if (commandArg.kind === 'wheel') {
|
|
1449
|
+
this.inFlightWheelCommands--;
|
|
1450
|
+
if (this.inFlightWheelCommands === 0) {
|
|
1451
|
+
this.tryFlushPendingWheel();
|
|
1115
1452
|
}
|
|
1116
1453
|
}
|
|
1117
|
-
|
|
1454
|
+
this.processInputCommands();
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
private handleInputDispatchFailure(errorArg: unknown, runEpochArg: number): void {
|
|
1459
|
+
if (errorArg instanceof LiveBrowserRunAbortedError) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (errorArg instanceof LiveBrowserOperationTimeoutError) {
|
|
1463
|
+
this.requestSuspend(runEpochArg);
|
|
1464
|
+
}
|
|
1465
|
+
this.reportError({
|
|
1466
|
+
code: 'input_dispatch_failed',
|
|
1467
|
+
message: `Could not dispatch live browser input: ${getErrorMessage(errorArg)}`,
|
|
1468
|
+
cause: errorArg,
|
|
1469
|
+
}, runEpochArg);
|
|
1470
|
+
this.discardPendingWheel();
|
|
1471
|
+
const abandonedCommands = this.inputCommands.splice(0);
|
|
1472
|
+
for (const abandonedCommand of abandonedCommands) {
|
|
1473
|
+
abandonedCommand.reject(
|
|
1474
|
+
new Error('Live browser input was abandoned after a dispatch failure'),
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
if (!(errorArg instanceof LiveBrowserOperationTimeoutError) && !this.stopping) {
|
|
1478
|
+
void this.scheduleInputReset(true, runEpochArg);
|
|
1479
|
+
}
|
|
1118
1480
|
}
|
|
1119
1481
|
|
|
1120
1482
|
private waitForInputIdle(): Promise<void> {
|
|
1121
|
-
if (
|
|
1483
|
+
if (this.inFlightInputCommands === 0 && this.inputCommands.length === 0) {
|
|
1122
1484
|
return Promise.resolve();
|
|
1123
1485
|
}
|
|
1124
1486
|
return new Promise<void>((resolve) => {
|
|
@@ -1159,17 +1521,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1159
1521
|
return resetOperation;
|
|
1160
1522
|
}
|
|
1161
1523
|
|
|
1162
|
-
private recoverFromInputOverflow(runEpochArg: number): void {
|
|
1163
|
-
if (this.inputOverflowRecoveryPending) {
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
this.inputOverflowRecoveryPending = true;
|
|
1167
|
-
this.inputBlocked = true;
|
|
1168
|
-
void this.scheduleInputReset(false, runEpochArg).finally(() => {
|
|
1169
|
-
this.inputOverflowRecoveryPending = false;
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
1524
|
private async releasePressedInput(runEpochArg: number): Promise<void> {
|
|
1174
1525
|
for (const [keyIdentity, pressedKey] of [...this.pressedKeys.entries()]) {
|
|
1175
1526
|
const released = await this.tryInputRelease(
|
|
@@ -1254,9 +1605,14 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1254
1605
|
this.pendingViewport = undefined;
|
|
1255
1606
|
return;
|
|
1256
1607
|
}
|
|
1608
|
+
if (!this.inFlightViewport && this.lastRequestedViewport && viewportEquals(viewport, this.lastRequestedViewport)) {
|
|
1609
|
+
this.pendingViewport = undefined;
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1257
1612
|
if (
|
|
1258
1613
|
!this.inFlightViewport
|
|
1259
1614
|
&& this.resizeFence
|
|
1615
|
+
&& !this.resizeFence.negotiated
|
|
1260
1616
|
&& viewportEquals(viewport, this.resizeFence.target)
|
|
1261
1617
|
) {
|
|
1262
1618
|
this.pendingViewport = undefined;
|
|
@@ -1265,10 +1621,12 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1265
1621
|
if (
|
|
1266
1622
|
!this.inFlightViewport
|
|
1267
1623
|
&& !this.resizeFence
|
|
1624
|
+
&& !this.lastRequestedViewport
|
|
1268
1625
|
&& this.state
|
|
1269
1626
|
&& viewportEquals(viewport, this.state.viewport)
|
|
1270
1627
|
) {
|
|
1271
1628
|
this.pendingViewport = undefined;
|
|
1629
|
+
this.lastRequestedViewport = { ...viewport };
|
|
1272
1630
|
this.updateInputAvailability();
|
|
1273
1631
|
return;
|
|
1274
1632
|
}
|
|
@@ -1364,19 +1722,20 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1364
1722
|
const viewport = this.pendingViewport;
|
|
1365
1723
|
this.pendingViewport = undefined;
|
|
1366
1724
|
this.inFlightViewport = viewport;
|
|
1367
|
-
if (!this.resizeFence && this.state && viewportEquals(viewport, this.state.viewport)) {
|
|
1725
|
+
if (!this.resizeFence && !this.lastRequestedViewport && this.state && viewportEquals(viewport, this.state.viewport)) {
|
|
1368
1726
|
this.inFlightViewport = undefined;
|
|
1727
|
+
this.lastRequestedViewport = { ...viewport };
|
|
1369
1728
|
continue;
|
|
1370
1729
|
}
|
|
1371
1730
|
|
|
1372
|
-
await this.scheduleInputReset(
|
|
1731
|
+
await this.scheduleInputReset(false, runEpochArg);
|
|
1373
1732
|
if (!this.running || this.stopping || runEpochArg !== this.runEpoch) {
|
|
1374
1733
|
return;
|
|
1375
1734
|
}
|
|
1376
1735
|
const currentRevision = this.state?.viewportRevision ?? 0;
|
|
1377
1736
|
const previousMinimumRevision = this.resizeFence?.minimumViewportRevision ?? currentRevision;
|
|
1378
1737
|
try {
|
|
1379
|
-
await this.runClientOperation(
|
|
1738
|
+
const result = await this.runClientOperation(
|
|
1380
1739
|
(optionsArg) => this.client.setViewport(viewport, optionsArg),
|
|
1381
1740
|
'viewport update',
|
|
1382
1741
|
runEpochArg,
|
|
@@ -1384,10 +1743,15 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1384
1743
|
if (!this.running || this.stopping || runEpochArg !== this.runEpoch) {
|
|
1385
1744
|
return;
|
|
1386
1745
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1746
|
+
if (result !== undefined) {
|
|
1747
|
+
this.validateViewport(result.viewport, 'viewport result');
|
|
1748
|
+
this.validateProtocolInteger(result.viewportRevision, 'viewport result revision', Math.max(1, currentRevision));
|
|
1749
|
+
this.resizeFence = { target: { ...result.viewport }, minimumViewportRevision: result.viewportRevision, negotiated: true };
|
|
1750
|
+
this.followNegotiatedViewport();
|
|
1751
|
+
} else {
|
|
1752
|
+
this.resizeFence = { target: viewport, minimumViewportRevision: Math.max(currentRevision, previousMinimumRevision) + 1 };
|
|
1753
|
+
}
|
|
1754
|
+
this.lastRequestedViewport = { ...viewport };
|
|
1391
1755
|
} catch (error) {
|
|
1392
1756
|
if (runEpochArg === this.runEpoch && !(error instanceof LiveBrowserRunAbortedError)) {
|
|
1393
1757
|
this.resizeFence = undefined;
|
|
@@ -1413,6 +1777,11 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1413
1777
|
}
|
|
1414
1778
|
|
|
1415
1779
|
private updateInputAvailability(): void {
|
|
1780
|
+
if (this.resizeFence && !this.viewportProcessing && this.displayedFrame && this.isDisplayedFrameCurrent()
|
|
1781
|
+
&& this.displayedFrame.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
1782
|
+
&& viewportEquals(this.displayedFrame.viewport, this.resizeFence.target)) {
|
|
1783
|
+
this.resizeFence = undefined;
|
|
1784
|
+
}
|
|
1416
1785
|
this.inputBlocked = !(
|
|
1417
1786
|
this.running
|
|
1418
1787
|
&& this.acceptingFrames
|
|
@@ -1423,6 +1792,12 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1423
1792
|
);
|
|
1424
1793
|
}
|
|
1425
1794
|
|
|
1795
|
+
private followNegotiatedViewport(): void {
|
|
1796
|
+
if (this.resizeFence?.negotiated && this.state && this.state.viewportRevision > this.resizeFence.minimumViewportRevision) {
|
|
1797
|
+
this.resizeFence = { target: { ...this.state.viewport }, minimumViewportRevision: this.state.viewportRevision, negotiated: true };
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1426
1801
|
private makeFocusTargetFocusable(): void {
|
|
1427
1802
|
this.focusWasAdjusted = false;
|
|
1428
1803
|
if (this.focusTarget.tabIndex >= 0) {
|
|
@@ -1447,7 +1822,46 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1447
1822
|
}
|
|
1448
1823
|
|
|
1449
1824
|
private clearCanvas(): void {
|
|
1450
|
-
|
|
1825
|
+
if (this.bitmapContext) {
|
|
1826
|
+
this.bitmapContext.transferFromImageBitmap(null);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
this.canvasContext?.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* Runs work on the next animation frame, or after one nominal frame period
|
|
1834
|
+
* when the document is not visible and animation frames would not fire.
|
|
1835
|
+
*/
|
|
1836
|
+
private scheduleAnimationFrame(callbackArg: () => void): IScheduledFrame {
|
|
1837
|
+
const schedule: IScheduledFrame = {};
|
|
1838
|
+
if (
|
|
1839
|
+
typeof document !== 'undefined'
|
|
1840
|
+
&& document.visibilityState === 'visible'
|
|
1841
|
+
&& typeof globalThis.requestAnimationFrame === 'function'
|
|
1842
|
+
) {
|
|
1843
|
+
schedule.animationFrameId = globalThis.requestAnimationFrame(() => {
|
|
1844
|
+
schedule.animationFrameId = undefined;
|
|
1845
|
+
callbackArg();
|
|
1846
|
+
});
|
|
1847
|
+
} else {
|
|
1848
|
+
schedule.timeoutId = globalThis.setTimeout(() => {
|
|
1849
|
+
schedule.timeoutId = undefined;
|
|
1850
|
+
callbackArg();
|
|
1851
|
+
}, animationFrameFallbackMs);
|
|
1852
|
+
}
|
|
1853
|
+
return schedule;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private cancelScheduledFrame(scheduleArg: IScheduledFrame): void {
|
|
1857
|
+
if (scheduleArg.animationFrameId !== undefined) {
|
|
1858
|
+
globalThis.cancelAnimationFrame(scheduleArg.animationFrameId);
|
|
1859
|
+
scheduleArg.animationFrameId = undefined;
|
|
1860
|
+
}
|
|
1861
|
+
if (scheduleArg.timeoutId !== undefined) {
|
|
1862
|
+
globalThis.clearTimeout(scheduleArg.timeoutId);
|
|
1863
|
+
scheduleArg.timeoutId = undefined;
|
|
1864
|
+
}
|
|
1451
1865
|
}
|
|
1452
1866
|
|
|
1453
1867
|
private callFrameRendered(frameArg: ILiveBrowserFrame, runEpochArg: number): void {
|