@push.rocks/smartbrowser 4.0.1 → 4.1.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 +59 -5
- package/dist_ts_web/classes.livebrowsercanvasrenderer.js +443 -126
- package/dist_ts_web/interfaces.livebrowsercanvas.d.ts +20 -0
- package/package.json +3 -3
- package/readme.hints.md +13 -9
- package/readme.md +9 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.livebrowsercanvasrenderer.ts +525 -137
- package/ts_web/interfaces.livebrowsercanvas.ts +21 -0
|
@@ -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,17 +52,63 @@ 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;
|
|
@@ -102,7 +155,8 @@ const getErrorMessage = (errorArg: unknown): string => (
|
|
|
102
155
|
*/
|
|
103
156
|
export class LiveBrowserCanvasRenderer {
|
|
104
157
|
private readonly canvas: HTMLCanvasElement;
|
|
105
|
-
private readonly canvasContext
|
|
158
|
+
private readonly canvasContext?: CanvasRenderingContext2D;
|
|
159
|
+
private readonly bitmapContext?: ImageBitmapRenderingContext;
|
|
106
160
|
private readonly client: ILiveBrowserCanvasRendererOptions['client'];
|
|
107
161
|
private readonly focusTarget: HTMLElement;
|
|
108
162
|
private readonly resizeTarget?: Element;
|
|
@@ -135,17 +189,22 @@ export class LiveBrowserCanvasRenderer {
|
|
|
135
189
|
private highestFrameSequence = -1;
|
|
136
190
|
private queuedFrame?: IFrameWork;
|
|
137
191
|
private frameProcessingPromise?: Promise<void>;
|
|
192
|
+
private pendingDraw?: IPendingDraw;
|
|
193
|
+
private drawSchedule?: IScheduledFrame;
|
|
138
194
|
private acknowledgementPromises = new Set<Promise<void>>();
|
|
139
195
|
private rawDecodeSettlementPromises = new Set<Promise<void>>();
|
|
140
196
|
|
|
141
197
|
private inputBlocked = true;
|
|
142
198
|
private inputCommands: IInputCommand[] = [];
|
|
143
|
-
private
|
|
199
|
+
private inFlightInputCommands = 0;
|
|
200
|
+
private inFlightWheelCommands = 0;
|
|
144
201
|
private inputIdleResolvers: Array<() => void> = [];
|
|
145
202
|
private inputResetTail: Promise<void> = Promise.resolve();
|
|
146
203
|
private inputResetPromise?: Promise<void>;
|
|
147
204
|
private inputResetCount = 0;
|
|
148
|
-
private
|
|
205
|
+
private pendingWheel?: IPendingWheel;
|
|
206
|
+
private wheelFlushSchedule?: IScheduledFrame;
|
|
207
|
+
private wheelFrameElapsed = true;
|
|
149
208
|
private pressedKeys = new Map<string, ILiveBrowserKeyInput>();
|
|
150
209
|
private pressedMouseButtons = new Map<string, ILiveBrowserMouseInput>();
|
|
151
210
|
private capturedPointerIds = new Set<number>();
|
|
@@ -155,6 +214,17 @@ export class LiveBrowserCanvasRenderer {
|
|
|
155
214
|
private viewportProcessing = false;
|
|
156
215
|
private viewportProcessingPromise?: Promise<void>;
|
|
157
216
|
private resizeFence?: IResizeFence;
|
|
217
|
+
private resizeDebounceTimer?: ReturnType<typeof setTimeout>;
|
|
218
|
+
private pendingResizeSize?: { width: number; height: number };
|
|
219
|
+
|
|
220
|
+
private readonly statistics: Omit<ILiveBrowserCanvasRendererStatistics, 'inputCommandsInFlight'> = {
|
|
221
|
+
framesReceived: 0,
|
|
222
|
+
framesDecoded: 0,
|
|
223
|
+
framesSkipped: 0,
|
|
224
|
+
inputCommandsEnqueued: 0,
|
|
225
|
+
inputCommandsCoalesced: 0,
|
|
226
|
+
lastInputRoundTripMs: 0,
|
|
227
|
+
};
|
|
158
228
|
|
|
159
229
|
constructor(optionsArg: ILiveBrowserCanvasRendererOptions) {
|
|
160
230
|
this.canvas = optionsArg.canvas;
|
|
@@ -176,11 +246,17 @@ export class LiveBrowserCanvasRenderer {
|
|
|
176
246
|
this.initialCanvasWidth = this.canvas.width;
|
|
177
247
|
this.initialCanvasHeight = this.canvas.height;
|
|
178
248
|
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
249
|
+
const bitmapContext = this.canvas.getContext('bitmaprenderer');
|
|
250
|
+
if (bitmapContext) {
|
|
251
|
+
this.bitmapContext = bitmapContext;
|
|
252
|
+
} else {
|
|
253
|
+
// A canvas that already owns a 2D context keeps the 2D drawing path.
|
|
254
|
+
const canvasContext = this.canvas.getContext('2d');
|
|
255
|
+
if (!canvasContext) {
|
|
256
|
+
throw new Error('LiveBrowserCanvasRenderer requires a bitmaprenderer or 2D canvas context');
|
|
257
|
+
}
|
|
258
|
+
this.canvasContext = canvasContext;
|
|
182
259
|
}
|
|
183
|
-
this.canvasContext = canvasContext;
|
|
184
260
|
}
|
|
185
261
|
|
|
186
262
|
public get isRunning(): boolean {
|
|
@@ -191,6 +267,16 @@ export class LiveBrowserCanvasRenderer {
|
|
|
191
267
|
return this.suspended;
|
|
192
268
|
}
|
|
193
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Cumulative frame and input counters for this renderer instance across runs.
|
|
272
|
+
*/
|
|
273
|
+
public getStatistics(): ILiveBrowserCanvasRendererStatistics {
|
|
274
|
+
return {
|
|
275
|
+
...this.statistics,
|
|
276
|
+
inputCommandsInFlight: this.inFlightInputCommands,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
194
280
|
public start(): Promise<void> {
|
|
195
281
|
return this.enqueueLifecycle(async () => {
|
|
196
282
|
if (this.running) {
|
|
@@ -299,6 +385,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
299
385
|
this.interruptionRequested = false;
|
|
300
386
|
this.acceptingFrames = true;
|
|
301
387
|
this.inputBlocked = true;
|
|
388
|
+
this.wheelFrameElapsed = true;
|
|
302
389
|
this.state = undefined;
|
|
303
390
|
this.displayedFrame = undefined;
|
|
304
391
|
this.highestFrameSequence = -1;
|
|
@@ -335,6 +422,8 @@ export class LiveBrowserCanvasRenderer {
|
|
|
335
422
|
this.queuedFrame = undefined;
|
|
336
423
|
this.displayedFrame = undefined;
|
|
337
424
|
this.resizeFence = undefined;
|
|
425
|
+
this.discardPendingDraw();
|
|
426
|
+
this.discardPendingWheel();
|
|
338
427
|
this.releasePointerCaptures();
|
|
339
428
|
this.clearCanvas();
|
|
340
429
|
|
|
@@ -352,7 +441,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
352
441
|
}
|
|
353
442
|
this.pressedKeys.clear();
|
|
354
443
|
this.pressedMouseButtons.clear();
|
|
355
|
-
this.inputOverflowRecoveryPending = false;
|
|
356
444
|
}
|
|
357
445
|
|
|
358
446
|
private async finishRun(runEpochArg: number, suspendArg: boolean): Promise<void> {
|
|
@@ -363,6 +451,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
363
451
|
try {
|
|
364
452
|
this.resizeObserver?.disconnect();
|
|
365
453
|
this.resizeObserver = undefined;
|
|
454
|
+
this.clearResizeDebounce();
|
|
366
455
|
this.listenerController?.abort();
|
|
367
456
|
this.listenerController = undefined;
|
|
368
457
|
const unsubscribe = this.unsubscribe;
|
|
@@ -402,6 +491,9 @@ export class LiveBrowserCanvasRenderer {
|
|
|
402
491
|
this.highestFrameSequence = -1;
|
|
403
492
|
this.queuedFrame = undefined;
|
|
404
493
|
this.frameProcessingPromise = undefined;
|
|
494
|
+
this.discardPendingDraw();
|
|
495
|
+
this.discardPendingWheel();
|
|
496
|
+
this.clearResizeDebounce();
|
|
405
497
|
this.acknowledgementPromises.clear();
|
|
406
498
|
this.resizeFence = undefined;
|
|
407
499
|
this.pendingViewport = undefined;
|
|
@@ -498,19 +590,37 @@ export class LiveBrowserCanvasRenderer {
|
|
|
498
590
|
}
|
|
499
591
|
this.resizeObserver = new ResizeObserver((entriesArg) => {
|
|
500
592
|
const matchingEntry = entriesArg.find((entryArg) => entryArg.target === this.resizeTarget);
|
|
501
|
-
if (!matchingEntry) {
|
|
593
|
+
if (!matchingEntry || runEpochArg !== this.runEpoch) {
|
|
502
594
|
return;
|
|
503
595
|
}
|
|
504
|
-
this.
|
|
505
|
-
matchingEntry.contentRect.width,
|
|
506
|
-
matchingEntry.contentRect.height,
|
|
507
|
-
|
|
508
|
-
)
|
|
596
|
+
this.pendingResizeSize = {
|
|
597
|
+
width: matchingEntry.contentRect.width,
|
|
598
|
+
height: matchingEntry.contentRect.height,
|
|
599
|
+
};
|
|
600
|
+
if (this.resizeDebounceTimer !== undefined) {
|
|
601
|
+
globalThis.clearTimeout(this.resizeDebounceTimer);
|
|
602
|
+
}
|
|
603
|
+
this.resizeDebounceTimer = globalThis.setTimeout(() => {
|
|
604
|
+
this.resizeDebounceTimer = undefined;
|
|
605
|
+
const pendingResizeSize = this.pendingResizeSize;
|
|
606
|
+
this.pendingResizeSize = undefined;
|
|
607
|
+
if (pendingResizeSize) {
|
|
608
|
+
this.queueViewport(pendingResizeSize.width, pendingResizeSize.height, runEpochArg);
|
|
609
|
+
}
|
|
610
|
+
}, resizeDebounceMs);
|
|
509
611
|
});
|
|
510
612
|
this.resizeObserver.observe(this.resizeTarget);
|
|
511
613
|
this.syncViewport();
|
|
512
614
|
}
|
|
513
615
|
|
|
616
|
+
private clearResizeDebounce(): void {
|
|
617
|
+
if (this.resizeDebounceTimer !== undefined) {
|
|
618
|
+
globalThis.clearTimeout(this.resizeDebounceTimer);
|
|
619
|
+
this.resizeDebounceTimer = undefined;
|
|
620
|
+
}
|
|
621
|
+
this.pendingResizeSize = undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
514
624
|
private handleEvent(eventArg: TLiveBrowserEvent, runEpochArg: number): void {
|
|
515
625
|
if (eventArg.type === 'frame') {
|
|
516
626
|
this.queueFrame(eventArg.frame, runEpochArg);
|
|
@@ -553,7 +663,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
553
663
|
return true;
|
|
554
664
|
}
|
|
555
665
|
const activeTab = stateArg.tabs.find((tabArg) => tabArg.id === displayedFrameArg.tabId);
|
|
556
|
-
if (!activeTab || activeTab.status !== 'open') {
|
|
666
|
+
if (!activeTab || activeTab.status !== 'open' || !activeTab.streaming) {
|
|
557
667
|
return true;
|
|
558
668
|
}
|
|
559
669
|
return stateArg.viewportRevision > displayedFrameArg.viewportRevision
|
|
@@ -564,6 +674,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
564
674
|
if (!this.running || !this.acceptingFrames || runEpochArg !== this.runEpoch) {
|
|
565
675
|
return;
|
|
566
676
|
}
|
|
677
|
+
this.statistics.framesReceived++;
|
|
567
678
|
|
|
568
679
|
try {
|
|
569
680
|
this.validateFrame(frameArg);
|
|
@@ -590,11 +701,12 @@ export class LiveBrowserCanvasRenderer {
|
|
|
590
701
|
acknowledgementAttempted: false,
|
|
591
702
|
};
|
|
592
703
|
if (this.queuedFrame) {
|
|
593
|
-
|
|
704
|
+
// The superseded frame was already acknowledged at receipt.
|
|
594
705
|
this.queuedFrame = undefined;
|
|
595
|
-
|
|
706
|
+
this.statistics.framesSkipped++;
|
|
596
707
|
}
|
|
597
708
|
this.queuedFrame = frameWork;
|
|
709
|
+
void this.acknowledgeFrame(frameWork);
|
|
598
710
|
|
|
599
711
|
this.ensureFrameProcessor();
|
|
600
712
|
}
|
|
@@ -643,32 +755,14 @@ export class LiveBrowserCanvasRenderer {
|
|
|
643
755
|
`decoded frame dimensions ${imageBitmap.width}x${imageBitmap.height} do not match ${frame.width}x${frame.height}`,
|
|
644
756
|
);
|
|
645
757
|
}
|
|
646
|
-
|
|
758
|
+
this.statistics.framesDecoded++;
|
|
759
|
+
if (!this.canRenderFrame(frame, runEpoch)) {
|
|
760
|
+
this.statistics.framesSkipped++;
|
|
647
761
|
return;
|
|
648
762
|
}
|
|
649
763
|
|
|
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);
|
|
764
|
+
this.setPendingDraw({ bitmap: imageBitmap, frame, runEpoch });
|
|
765
|
+
imageBitmap = undefined;
|
|
672
766
|
} catch (error) {
|
|
673
767
|
if (error instanceof LiveBrowserRunAbortedError) {
|
|
674
768
|
return;
|
|
@@ -677,7 +771,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
677
771
|
terminalError = error;
|
|
678
772
|
} finally {
|
|
679
773
|
imageBitmap?.close();
|
|
680
|
-
await this.acknowledgeFrame(frameWorkArg);
|
|
681
774
|
if (terminalFailure) {
|
|
682
775
|
this.requestSuspend(runEpoch);
|
|
683
776
|
this.reportError({
|
|
@@ -690,6 +783,104 @@ export class LiveBrowserCanvasRenderer {
|
|
|
690
783
|
}
|
|
691
784
|
}
|
|
692
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Keeps only the newest decoded bitmap and presents it on the next animation frame.
|
|
788
|
+
*/
|
|
789
|
+
private setPendingDraw(pendingDrawArg: IPendingDraw): void {
|
|
790
|
+
if (this.pendingDraw) {
|
|
791
|
+
this.pendingDraw.bitmap.close();
|
|
792
|
+
this.statistics.framesSkipped++;
|
|
793
|
+
}
|
|
794
|
+
this.pendingDraw = pendingDrawArg;
|
|
795
|
+
if (!this.drawSchedule) {
|
|
796
|
+
this.drawSchedule = this.scheduleAnimationFrame(() => {
|
|
797
|
+
this.drawSchedule = undefined;
|
|
798
|
+
this.drawPendingFrame();
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private discardPendingDraw(): void {
|
|
804
|
+
if (this.drawSchedule) {
|
|
805
|
+
this.cancelScheduledFrame(this.drawSchedule);
|
|
806
|
+
this.drawSchedule = undefined;
|
|
807
|
+
}
|
|
808
|
+
const pendingDraw = this.pendingDraw;
|
|
809
|
+
this.pendingDraw = undefined;
|
|
810
|
+
if (pendingDraw) {
|
|
811
|
+
pendingDraw.bitmap.close();
|
|
812
|
+
this.statistics.framesSkipped++;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private drawPendingFrame(): void {
|
|
817
|
+
const pendingDraw = this.pendingDraw;
|
|
818
|
+
this.pendingDraw = undefined;
|
|
819
|
+
if (!pendingDraw) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const { bitmap, frame, runEpoch } = pendingDraw;
|
|
823
|
+
if (!this.canRenderFrame(frame, runEpoch)) {
|
|
824
|
+
bitmap.close();
|
|
825
|
+
this.statistics.framesSkipped++;
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
this.paintBitmap(bitmap, frame.width, frame.height);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
bitmap.close();
|
|
832
|
+
this.requestSuspend(runEpoch);
|
|
833
|
+
this.reportError({
|
|
834
|
+
code: 'frame_render_failed',
|
|
835
|
+
message: `Could not render live browser frame: ${getErrorMessage(error)}`,
|
|
836
|
+
cause: error,
|
|
837
|
+
frame,
|
|
838
|
+
}, runEpoch);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
this.displayedFrame = {
|
|
842
|
+
tabId: frame.tabId,
|
|
843
|
+
sequence: frame.sequence,
|
|
844
|
+
generation: frame.generation,
|
|
845
|
+
viewportRevision: frame.viewportRevision,
|
|
846
|
+
viewport: { ...frame.viewport },
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
if (
|
|
850
|
+
this.resizeFence
|
|
851
|
+
&& !this.viewportProcessing
|
|
852
|
+
&& frame.viewportRevision >= this.resizeFence.minimumViewportRevision
|
|
853
|
+
&& viewportEquals(frame.viewport, this.resizeFence.target)
|
|
854
|
+
) {
|
|
855
|
+
this.resizeFence = undefined;
|
|
856
|
+
}
|
|
857
|
+
this.updateInputAvailability();
|
|
858
|
+
this.callFrameRendered(frame, runEpoch);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Presents one decoded bitmap. The canvas backing store is reallocated only
|
|
863
|
+
* when the frame dimensions differ from the current intrinsic dimensions.
|
|
864
|
+
*/
|
|
865
|
+
private paintBitmap(bitmapArg: ImageBitmap, widthArg: number, heightArg: number): void {
|
|
866
|
+
if (this.canvas.width !== widthArg) {
|
|
867
|
+
this.canvas.width = widthArg;
|
|
868
|
+
}
|
|
869
|
+
if (this.canvas.height !== heightArg) {
|
|
870
|
+
this.canvas.height = heightArg;
|
|
871
|
+
}
|
|
872
|
+
if (this.bitmapContext) {
|
|
873
|
+
this.bitmapContext.transferFromImageBitmap(bitmapArg);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (!this.canvasContext) {
|
|
877
|
+
throw new Error('LiveBrowserCanvasRenderer has no rendering context');
|
|
878
|
+
}
|
|
879
|
+
this.canvasContext.clearRect(0, 0, widthArg, heightArg);
|
|
880
|
+
this.canvasContext.drawImage(bitmapArg, 0, 0, widthArg, heightArg);
|
|
881
|
+
bitmapArg.close();
|
|
882
|
+
}
|
|
883
|
+
|
|
693
884
|
private canRenderFrame(frameArg: ILiveBrowserFrame, runEpochArg: number): boolean {
|
|
694
885
|
if (
|
|
695
886
|
!this.running
|
|
@@ -704,7 +895,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
704
895
|
return false;
|
|
705
896
|
}
|
|
706
897
|
const activeTab = state.tabs.find((tabArg) => tabArg.id === frameArg.tabId);
|
|
707
|
-
if (!activeTab || activeTab.status !== 'open') {
|
|
898
|
+
if (!activeTab || activeTab.status !== 'open' || !activeTab.streaming) {
|
|
708
899
|
return false;
|
|
709
900
|
}
|
|
710
901
|
if (
|
|
@@ -889,20 +1080,110 @@ export class LiveBrowserCanvasRenderer {
|
|
|
889
1080
|
: eventArg.deltaMode === WheelEvent.DOM_DELTA_PAGE
|
|
890
1081
|
? displayedFrame.viewport.height
|
|
891
1082
|
: 1;
|
|
892
|
-
const
|
|
1083
|
+
const inputIdentity = createInputIdentity(displayedFrame);
|
|
1084
|
+
const modifiers = createModifiers(eventArg);
|
|
1085
|
+
this.statistics.inputCommandsEnqueued++;
|
|
1086
|
+
const pendingWheel = this.pendingWheel;
|
|
1087
|
+
if (
|
|
1088
|
+
pendingWheel
|
|
1089
|
+
&& pendingWheel.runEpoch === runEpochArg
|
|
1090
|
+
&& inputIdentityEquals(pendingWheel, inputIdentity)
|
|
1091
|
+
) {
|
|
1092
|
+
mergeWheelInput(pendingWheel, {
|
|
1093
|
+
...inputIdentity,
|
|
1094
|
+
...coordinates,
|
|
1095
|
+
deltaX: eventArg.deltaX * multiplier,
|
|
1096
|
+
deltaY: eventArg.deltaY * multiplier,
|
|
1097
|
+
modifiers,
|
|
1098
|
+
});
|
|
1099
|
+
this.statistics.inputCommandsCoalesced++;
|
|
1100
|
+
} else {
|
|
1101
|
+
if (pendingWheel) {
|
|
1102
|
+
this.flushPendingWheel();
|
|
1103
|
+
}
|
|
1104
|
+
this.pendingWheel = {
|
|
1105
|
+
...inputIdentity,
|
|
1106
|
+
...coordinates,
|
|
1107
|
+
deltaX: clampWheelDelta(eventArg.deltaX * multiplier),
|
|
1108
|
+
deltaY: clampWheelDelta(eventArg.deltaY * multiplier),
|
|
1109
|
+
modifiers,
|
|
1110
|
+
runEpoch: runEpochArg,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
this.tryFlushPendingWheel();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Flushes accumulated wheel input at most once per animation frame and only
|
|
1118
|
+
* while no wheel dispatch is in flight; the settling dispatch flushes the rest.
|
|
1119
|
+
*/
|
|
1120
|
+
private tryFlushPendingWheel(): void {
|
|
1121
|
+
if (!this.pendingWheel) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!this.wheelFrameElapsed) {
|
|
1125
|
+
this.scheduleWheelFlush();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (this.inFlightWheelCommands > 0) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
this.flushPendingWheel();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private scheduleWheelFlush(): void {
|
|
1135
|
+
if (this.wheelFlushSchedule) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
this.wheelFlushSchedule = this.scheduleAnimationFrame(() => {
|
|
1139
|
+
this.wheelFlushSchedule = undefined;
|
|
1140
|
+
this.wheelFrameElapsed = true;
|
|
1141
|
+
this.tryFlushPendingWheel();
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Moves the accumulated wheel input into the command queue regardless of the
|
|
1147
|
+
* frame gate so that later discrete commands stay behind it.
|
|
1148
|
+
*/
|
|
1149
|
+
private flushPendingWheel(): void {
|
|
1150
|
+
const pendingWheel = this.pendingWheel;
|
|
1151
|
+
if (!pendingWheel) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
this.pendingWheel = undefined;
|
|
1155
|
+
this.wheelFrameElapsed = false;
|
|
1156
|
+
this.scheduleWheelFlush();
|
|
1157
|
+
const displayedFrame = this.getInputFrame();
|
|
1158
|
+
if (
|
|
1159
|
+
!displayedFrame
|
|
1160
|
+
|| pendingWheel.runEpoch !== this.runEpoch
|
|
1161
|
+
|| !inputIdentityEquals(pendingWheel, createInputIdentity(displayedFrame))
|
|
1162
|
+
) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const { runEpoch, ...wheelInput } = pendingWheel;
|
|
893
1166
|
void this.enqueueInputCommand({
|
|
894
1167
|
kind: 'wheel',
|
|
895
|
-
runEpoch
|
|
1168
|
+
runEpoch,
|
|
1169
|
+
wheelInput,
|
|
896
1170
|
execute: async (optionsArg) => this.client.dispatchWheel({
|
|
897
|
-
...
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
deltaY: clampDelta(eventArg.deltaY * multiplier),
|
|
901
|
-
modifiers: createModifiers(eventArg),
|
|
1171
|
+
...wheelInput,
|
|
1172
|
+
deltaX: clampWheelDelta(wheelInput.deltaX),
|
|
1173
|
+
deltaY: clampWheelDelta(wheelInput.deltaY),
|
|
902
1174
|
}, optionsArg),
|
|
903
1175
|
}).catch(() => undefined);
|
|
904
1176
|
}
|
|
905
1177
|
|
|
1178
|
+
private discardPendingWheel(): void {
|
|
1179
|
+
this.pendingWheel = undefined;
|
|
1180
|
+
if (this.wheelFlushSchedule) {
|
|
1181
|
+
this.cancelScheduledFrame(this.wheelFlushSchedule);
|
|
1182
|
+
this.wheelFlushSchedule = undefined;
|
|
1183
|
+
}
|
|
1184
|
+
this.wheelFrameElapsed = true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
906
1187
|
private handleKey(
|
|
907
1188
|
eventArg: KeyboardEvent,
|
|
908
1189
|
typeArg: 'down' | 'up',
|
|
@@ -1012,6 +1293,7 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1012
1293
|
return Boolean(
|
|
1013
1294
|
activeTab
|
|
1014
1295
|
&& activeTab.status === 'open'
|
|
1296
|
+
&& activeTab.streaming
|
|
1015
1297
|
&& activeTab.generation <= displayedFrame.generation,
|
|
1016
1298
|
);
|
|
1017
1299
|
}
|
|
@@ -1027,97 +1309,175 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1027
1309
|
resolve,
|
|
1028
1310
|
reject,
|
|
1029
1311
|
};
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
this.inputCommands
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1312
|
+
if (isCoalescableInputCommand(command.kind)) {
|
|
1313
|
+
this.enqueueCoalescableCommand(command);
|
|
1314
|
+
} else {
|
|
1315
|
+
const queuedDiscreteCommands = this.inputCommands.filter((queuedCommand) => (
|
|
1316
|
+
!isCoalescableInputCommand(queuedCommand.kind)
|
|
1317
|
+
)).length;
|
|
1318
|
+
if (queuedDiscreteCommands >= maxInputQueueLength) {
|
|
1319
|
+
const error = new Error('Live browser input queue reached its capacity');
|
|
1320
|
+
this.reportError({
|
|
1321
|
+
code: 'input_queue_capacity_exceeded',
|
|
1322
|
+
message: error.message,
|
|
1323
|
+
cause: error,
|
|
1324
|
+
}, command.runEpoch);
|
|
1325
|
+
reject(error);
|
|
1037
1326
|
return;
|
|
1038
1327
|
}
|
|
1039
|
-
|
|
1040
|
-
this.
|
|
1041
|
-
|
|
1042
|
-
message: error.message,
|
|
1043
|
-
cause: error,
|
|
1044
|
-
}, command.runEpoch);
|
|
1045
|
-
reject(error);
|
|
1046
|
-
this.recoverFromInputOverflow(command.runEpoch);
|
|
1047
|
-
return;
|
|
1048
|
-
} else {
|
|
1328
|
+
// Accumulated wheel input precedes this command in event order.
|
|
1329
|
+
this.flushPendingWheel();
|
|
1330
|
+
this.statistics.inputCommandsEnqueued++;
|
|
1049
1331
|
this.inputCommands.push(command);
|
|
1050
1332
|
}
|
|
1051
1333
|
this.processInputCommands();
|
|
1052
1334
|
});
|
|
1053
1335
|
}
|
|
1054
1336
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1337
|
+
/**
|
|
1338
|
+
* Coalescable commands never exhaust capacity: they merge into a same-kind
|
|
1339
|
+
* queue tail or evict the oldest queued coalescable command under pressure.
|
|
1340
|
+
*/
|
|
1341
|
+
private enqueueCoalescableCommand(commandArg: IInputCommand): void {
|
|
1342
|
+
// Wheel submissions are counted per DOM event before accumulation.
|
|
1343
|
+
if (commandArg.kind === 'mouseMove') {
|
|
1344
|
+
this.statistics.inputCommandsEnqueued++;
|
|
1345
|
+
}
|
|
1346
|
+
const lastCommand = this.inputCommands.at(-1);
|
|
1347
|
+
if (lastCommand && lastCommand.kind === commandArg.kind) {
|
|
1348
|
+
this.statistics.inputCommandsCoalesced++;
|
|
1349
|
+
if (commandArg.kind === 'wheel' && lastCommand.wheelInput && commandArg.wheelInput) {
|
|
1350
|
+
mergeWheelInput(lastCommand.wheelInput, commandArg.wheelInput);
|
|
1351
|
+
commandArg.resolve();
|
|
1352
|
+
} else {
|
|
1353
|
+
lastCommand.resolve();
|
|
1354
|
+
this.inputCommands[this.inputCommands.length - 1] = commandArg;
|
|
1355
|
+
}
|
|
1057
1356
|
return;
|
|
1058
1357
|
}
|
|
1059
|
-
this.
|
|
1060
|
-
|
|
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
|
-
|
|
1358
|
+
const queuedCoalescableCommands = this.inputCommands.filter((queuedCommand) => (
|
|
1359
|
+
isCoalescableInputCommand(queuedCommand.kind)
|
|
1360
|
+
));
|
|
1361
|
+
if (queuedCoalescableCommands.length >= maxQueuedCoalescableCommands) {
|
|
1362
|
+
this.dropOldestCoalescableCommand(queuedCoalescableCommands[0]);
|
|
1363
|
+
}
|
|
1364
|
+
this.inputCommands.push(commandArg);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
private dropOldestCoalescableCommand(oldestCommandArg: IInputCommand): void {
|
|
1368
|
+
const index = this.inputCommands.indexOf(oldestCommandArg);
|
|
1369
|
+
if (index < 0) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
this.inputCommands.splice(index, 1);
|
|
1373
|
+
this.statistics.inputCommandsCoalesced++;
|
|
1374
|
+
if (oldestCommandArg.kind === 'wheel' && oldestCommandArg.wheelInput) {
|
|
1375
|
+
const nextWheelCommand = this.inputCommands.find((queuedCommand) => (
|
|
1376
|
+
queuedCommand.kind === 'wheel' && queuedCommand.wheelInput
|
|
1377
|
+
));
|
|
1378
|
+
if (nextWheelCommand?.wheelInput) {
|
|
1379
|
+
const nextWheelInput = nextWheelCommand.wheelInput;
|
|
1380
|
+
nextWheelInput.deltaX = clampWheelDelta(
|
|
1381
|
+
nextWheelInput.deltaX + oldestCommandArg.wheelInput.deltaX,
|
|
1382
|
+
);
|
|
1383
|
+
nextWheelInput.deltaY = clampWheelDelta(
|
|
1384
|
+
nextWheelInput.deltaY + oldestCommandArg.wheelInput.deltaY,
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
oldestCommandArg.resolve();
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Dispatches queued commands in order with a bounded number in flight. The
|
|
1393
|
+
* remote session processes operations sequentially, so order is preserved.
|
|
1394
|
+
*/
|
|
1395
|
+
private processInputCommands(): void {
|
|
1396
|
+
while (
|
|
1397
|
+
this.inputCommands.length > 0
|
|
1398
|
+
&& this.inFlightInputCommands < maxInFlightInputCommands
|
|
1399
|
+
) {
|
|
1400
|
+
const command = this.inputCommands.shift()!;
|
|
1401
|
+
if (!this.isRunActive(command.runEpoch)) {
|
|
1402
|
+
command.reject(new LiveBrowserRunAbortedError(
|
|
1403
|
+
`Live browser input belongs to inactive run ${command.runEpoch}`,
|
|
1404
|
+
));
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
this.dispatchInputCommand(command);
|
|
1408
|
+
}
|
|
1409
|
+
if (this.inputCommands.length === 0 && this.inFlightInputCommands === 0) {
|
|
1410
|
+
const resolvers = this.inputIdleResolvers;
|
|
1411
|
+
this.inputIdleResolvers = [];
|
|
1412
|
+
for (const resolver of resolvers) {
|
|
1413
|
+
resolver();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private dispatchInputCommand(commandArg: IInputCommand): void {
|
|
1419
|
+
this.inFlightInputCommands++;
|
|
1420
|
+
if (commandArg.kind === 'wheel') {
|
|
1421
|
+
this.inFlightWheelCommands++;
|
|
1422
|
+
}
|
|
1423
|
+
const startedAt = performance.now();
|
|
1424
|
+
let dispatch: Promise<void>;
|
|
1425
|
+
try {
|
|
1426
|
+
commandArg.onAttempt?.();
|
|
1427
|
+
dispatch = this.runClientOperation(commandArg.execute, 'input dispatch', commandArg.runEpoch);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
dispatch = Promise.reject(error);
|
|
1430
|
+
}
|
|
1431
|
+
void dispatch.then(() => {
|
|
1432
|
+
if (!this.isRunActive(commandArg.runEpoch)) {
|
|
1433
|
+
throw new LiveBrowserRunAbortedError(
|
|
1434
|
+
`Live browser input belongs to inactive run ${commandArg.runEpoch}`,
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
this.statistics.lastInputRoundTripMs = performance.now() - startedAt;
|
|
1438
|
+
commandArg.onSuccess?.();
|
|
1439
|
+
commandArg.resolve();
|
|
1440
|
+
}).catch((error) => {
|
|
1441
|
+
commandArg.reject(error);
|
|
1442
|
+
this.handleInputDispatchFailure(error, commandArg.runEpoch);
|
|
1443
|
+
}).finally(() => {
|
|
1444
|
+
this.inFlightInputCommands--;
|
|
1445
|
+
if (commandArg.kind === 'wheel') {
|
|
1446
|
+
this.inFlightWheelCommands--;
|
|
1447
|
+
if (this.inFlightWheelCommands === 0) {
|
|
1448
|
+
this.tryFlushPendingWheel();
|
|
1114
1449
|
}
|
|
1115
1450
|
}
|
|
1116
|
-
|
|
1451
|
+
this.processInputCommands();
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private handleInputDispatchFailure(errorArg: unknown, runEpochArg: number): void {
|
|
1456
|
+
if (errorArg instanceof LiveBrowserRunAbortedError) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (errorArg instanceof LiveBrowserOperationTimeoutError) {
|
|
1460
|
+
this.requestSuspend(runEpochArg);
|
|
1461
|
+
}
|
|
1462
|
+
this.reportError({
|
|
1463
|
+
code: 'input_dispatch_failed',
|
|
1464
|
+
message: `Could not dispatch live browser input: ${getErrorMessage(errorArg)}`,
|
|
1465
|
+
cause: errorArg,
|
|
1466
|
+
}, runEpochArg);
|
|
1467
|
+
this.discardPendingWheel();
|
|
1468
|
+
const abandonedCommands = this.inputCommands.splice(0);
|
|
1469
|
+
for (const abandonedCommand of abandonedCommands) {
|
|
1470
|
+
abandonedCommand.reject(
|
|
1471
|
+
new Error('Live browser input was abandoned after a dispatch failure'),
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
if (!(errorArg instanceof LiveBrowserOperationTimeoutError) && !this.stopping) {
|
|
1475
|
+
void this.scheduleInputReset(true, runEpochArg);
|
|
1476
|
+
}
|
|
1117
1477
|
}
|
|
1118
1478
|
|
|
1119
1479
|
private waitForInputIdle(): Promise<void> {
|
|
1120
|
-
if (
|
|
1480
|
+
if (this.inFlightInputCommands === 0 && this.inputCommands.length === 0) {
|
|
1121
1481
|
return Promise.resolve();
|
|
1122
1482
|
}
|
|
1123
1483
|
return new Promise<void>((resolve) => {
|
|
@@ -1158,17 +1518,6 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1158
1518
|
return resetOperation;
|
|
1159
1519
|
}
|
|
1160
1520
|
|
|
1161
|
-
private recoverFromInputOverflow(runEpochArg: number): void {
|
|
1162
|
-
if (this.inputOverflowRecoveryPending) {
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
this.inputOverflowRecoveryPending = true;
|
|
1166
|
-
this.inputBlocked = true;
|
|
1167
|
-
void this.scheduleInputReset(false, runEpochArg).finally(() => {
|
|
1168
|
-
this.inputOverflowRecoveryPending = false;
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
1521
|
private async releasePressedInput(runEpochArg: number): Promise<void> {
|
|
1173
1522
|
for (const [keyIdentity, pressedKey] of [...this.pressedKeys.entries()]) {
|
|
1174
1523
|
const released = await this.tryInputRelease(
|
|
@@ -1446,7 +1795,46 @@ export class LiveBrowserCanvasRenderer {
|
|
|
1446
1795
|
}
|
|
1447
1796
|
|
|
1448
1797
|
private clearCanvas(): void {
|
|
1449
|
-
|
|
1798
|
+
if (this.bitmapContext) {
|
|
1799
|
+
this.bitmapContext.transferFromImageBitmap(null);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
this.canvasContext?.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Runs work on the next animation frame, or after one nominal frame period
|
|
1807
|
+
* when the document is not visible and animation frames would not fire.
|
|
1808
|
+
*/
|
|
1809
|
+
private scheduleAnimationFrame(callbackArg: () => void): IScheduledFrame {
|
|
1810
|
+
const schedule: IScheduledFrame = {};
|
|
1811
|
+
if (
|
|
1812
|
+
typeof document !== 'undefined'
|
|
1813
|
+
&& document.visibilityState === 'visible'
|
|
1814
|
+
&& typeof globalThis.requestAnimationFrame === 'function'
|
|
1815
|
+
) {
|
|
1816
|
+
schedule.animationFrameId = globalThis.requestAnimationFrame(() => {
|
|
1817
|
+
schedule.animationFrameId = undefined;
|
|
1818
|
+
callbackArg();
|
|
1819
|
+
});
|
|
1820
|
+
} else {
|
|
1821
|
+
schedule.timeoutId = globalThis.setTimeout(() => {
|
|
1822
|
+
schedule.timeoutId = undefined;
|
|
1823
|
+
callbackArg();
|
|
1824
|
+
}, animationFrameFallbackMs);
|
|
1825
|
+
}
|
|
1826
|
+
return schedule;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
private cancelScheduledFrame(scheduleArg: IScheduledFrame): void {
|
|
1830
|
+
if (scheduleArg.animationFrameId !== undefined) {
|
|
1831
|
+
globalThis.cancelAnimationFrame(scheduleArg.animationFrameId);
|
|
1832
|
+
scheduleArg.animationFrameId = undefined;
|
|
1833
|
+
}
|
|
1834
|
+
if (scheduleArg.timeoutId !== undefined) {
|
|
1835
|
+
globalThis.clearTimeout(scheduleArg.timeoutId);
|
|
1836
|
+
scheduleArg.timeoutId = undefined;
|
|
1837
|
+
}
|
|
1450
1838
|
}
|
|
1451
1839
|
|
|
1452
1840
|
private callFrameRendered(frameArg: ILiveBrowserFrame, runEpochArg: number): void {
|