@push.rocks/smartpuppeteer 2.6.0 → 2.7.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/smartpuppeteer.classes.livebrowsersession.d.ts +7 -2
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +276 -132
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +2 -0
- package/package.json +2 -2
- package/readme.hints.md +2 -0
- package/readme.md +21 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +356 -133
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +6 -0
|
@@ -33,6 +33,8 @@ import type {
|
|
|
33
33
|
ILiveBrowserOperationOptions,
|
|
34
34
|
ILiveBrowserPressOptions,
|
|
35
35
|
ILiveBrowserProcessState,
|
|
36
|
+
ILiveBrowserScreencastOptions,
|
|
37
|
+
ILiveBrowserScreencastUpdateOptions,
|
|
36
38
|
ILiveBrowserSessionOptions,
|
|
37
39
|
ILiveBrowserSnapshot,
|
|
38
40
|
ILiveBrowserSnapshotOptions,
|
|
@@ -64,6 +66,7 @@ const maxSelectorLength = 4096;
|
|
|
64
66
|
const maxTextLength = 32768;
|
|
65
67
|
const maxUrlLength = 16384;
|
|
66
68
|
const maxTimeoutMs = 60000;
|
|
69
|
+
const maxWheelDelta = 1000000;
|
|
67
70
|
const frameAcknowledgementTimeoutMs = 5000;
|
|
68
71
|
const defaultFirstFrameTimeoutMs = 5000;
|
|
69
72
|
const maxQueuedPublicOperations = 64;
|
|
@@ -136,6 +139,34 @@ interface IScreencastAuthority {
|
|
|
136
139
|
controller: AbortController;
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
interface IPendingWheelDispatch {
|
|
143
|
+
cdpSession: plugins.puppeteer.CDPSession;
|
|
144
|
+
generation: number;
|
|
145
|
+
viewportRevision: number;
|
|
146
|
+
x: number;
|
|
147
|
+
y: number;
|
|
148
|
+
deltaX: number;
|
|
149
|
+
deltaY: number;
|
|
150
|
+
modifiers: number;
|
|
151
|
+
promise: Promise<void>;
|
|
152
|
+
resolve: () => void;
|
|
153
|
+
reject: (error: unknown) => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface IQueuedWheelInput {
|
|
157
|
+
kind: 'wheel';
|
|
158
|
+
dispatch: IPendingWheelDispatch;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface IQueuedEventInput {
|
|
162
|
+
kind: 'event';
|
|
163
|
+
send: () => Promise<void>;
|
|
164
|
+
resolve: () => void;
|
|
165
|
+
reject: (error: unknown) => void;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type TQueuedTabInput = IQueuedWheelInput | IQueuedEventInput;
|
|
169
|
+
|
|
139
170
|
interface IPrivateLiveBrowserTab {
|
|
140
171
|
id: string;
|
|
141
172
|
page: plugins.puppeteer.Page;
|
|
@@ -160,6 +191,9 @@ interface IPrivateLiveBrowserTab {
|
|
|
160
191
|
cdpConnection?: plugins.puppeteer.Connection;
|
|
161
192
|
screencastFrameListener?: TScreencastFrameListener;
|
|
162
193
|
cdpSessionDetachedListener?: TCdpSessionDetachedListener;
|
|
194
|
+
inputQueue: TQueuedTabInput[];
|
|
195
|
+
inputPumpActive: boolean;
|
|
196
|
+
wheelDispatchInFlight?: Promise<void>;
|
|
163
197
|
removeListeners: Array<() => void>;
|
|
164
198
|
}
|
|
165
199
|
|
|
@@ -386,6 +420,80 @@ const createPuppeteerViewport = (
|
|
|
386
420
|
hasTouch: false,
|
|
387
421
|
});
|
|
388
422
|
|
|
423
|
+
const validateScreencastOptions = (
|
|
424
|
+
options: ILiveBrowserScreencastOptions | undefined,
|
|
425
|
+
): void => {
|
|
426
|
+
if (!options) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (options.format !== undefined && options.format !== 'jpeg' && options.format !== 'png') {
|
|
430
|
+
throw new Error('screencast.format must be jpeg or png');
|
|
431
|
+
}
|
|
432
|
+
if (options.quality !== undefined) {
|
|
433
|
+
validateInteger(options.quality, 'screencast.quality', 0, 100);
|
|
434
|
+
}
|
|
435
|
+
if (options.maxWidth !== undefined) {
|
|
436
|
+
validateInteger(options.maxWidth, 'screencast.maxWidth', 1, maxViewportWidth);
|
|
437
|
+
}
|
|
438
|
+
if (options.maxHeight !== undefined) {
|
|
439
|
+
validateInteger(options.maxHeight, 'screencast.maxHeight', 1, maxViewportHeight);
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
options.maxWidth !== undefined
|
|
443
|
+
&& options.maxHeight !== undefined
|
|
444
|
+
&& options.maxWidth * options.maxHeight > maxViewportPixelArea
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`screencast pixel area must not exceed ${maxViewportPixelArea} pixels`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (options.everyNthFrame !== undefined) {
|
|
451
|
+
validateInteger(options.everyNthFrame, 'screencast.everyNthFrame', 1, 100);
|
|
452
|
+
}
|
|
453
|
+
if (options.maxOutstandingFrames !== undefined) {
|
|
454
|
+
validateInteger(
|
|
455
|
+
options.maxOutstandingFrames,
|
|
456
|
+
'screencast.maxOutstandingFrames',
|
|
457
|
+
1,
|
|
458
|
+
liveBrowserMaxOutstandingFrames,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (options.firstFrameTimeoutMs !== undefined) {
|
|
462
|
+
validateInteger(
|
|
463
|
+
options.firstFrameTimeoutMs,
|
|
464
|
+
'screencast.firstFrameTimeoutMs',
|
|
465
|
+
100,
|
|
466
|
+
maxTimeoutMs,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const screencastUpdateKeys = ['quality', 'maxWidth', 'maxHeight', 'everyNthFrame'] as const;
|
|
472
|
+
|
|
473
|
+
const normalizeScreencastUpdate = (
|
|
474
|
+
optionsArg: ILiveBrowserScreencastUpdateOptions,
|
|
475
|
+
): ILiveBrowserScreencastUpdateOptions => {
|
|
476
|
+
if (!optionsArg || typeof optionsArg !== 'object' || Array.isArray(optionsArg)) {
|
|
477
|
+
throw new Error('screencast update options must be an object');
|
|
478
|
+
}
|
|
479
|
+
const update: ILiveBrowserScreencastUpdateOptions = {};
|
|
480
|
+
for (const key of Object.keys(optionsArg)) {
|
|
481
|
+
if (!(screencastUpdateKeys as readonly string[]).includes(key)) {
|
|
482
|
+
throw new Error(`Unknown screencast update option: ${key}`);
|
|
483
|
+
}
|
|
484
|
+
const value = optionsArg[key as keyof ILiveBrowserScreencastUpdateOptions];
|
|
485
|
+
if (value !== undefined) {
|
|
486
|
+
update[key as keyof ILiveBrowserScreencastUpdateOptions] = value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
validateScreencastOptions(update);
|
|
490
|
+
return update;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const clampWheelDelta = (value: number): number => {
|
|
494
|
+
return Math.max(-maxWheelDelta, Math.min(maxWheelDelta, value));
|
|
495
|
+
};
|
|
496
|
+
|
|
389
497
|
export class LiveBrowserSession {
|
|
390
498
|
private readonly options: ILiveBrowserSessionOptions;
|
|
391
499
|
private readonly eventListeners = new Set<TLiveBrowserEventListener>();
|
|
@@ -524,7 +632,7 @@ export class LiveBrowserSession {
|
|
|
524
632
|
: undefined,
|
|
525
633
|
};
|
|
526
634
|
this.viewport = { ...viewport };
|
|
527
|
-
this.
|
|
635
|
+
validateScreencastOptions(this.options.screencast);
|
|
528
636
|
this.maxOutstandingFrames = this.options.screencast?.maxOutstandingFrames
|
|
529
637
|
?? liveBrowserDefaultMaxOutstandingFrames;
|
|
530
638
|
}
|
|
@@ -855,58 +963,95 @@ export class LiveBrowserSession {
|
|
|
855
963
|
public refreshScreencast(
|
|
856
964
|
operationOptions: ILiveBrowserOperationOptions = {},
|
|
857
965
|
): Promise<ILiveBrowserFrameIdentity> {
|
|
966
|
+
return this.enqueuePublicOperation(
|
|
967
|
+
(signal) => this.restartActiveScreencast(signal),
|
|
968
|
+
operationOptions,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
public async updateScreencastOptions(
|
|
973
|
+
optionsArg: ILiveBrowserScreencastUpdateOptions,
|
|
974
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
975
|
+
): Promise<ILiveBrowserFrameIdentity | null> {
|
|
976
|
+
const update = normalizeScreencastUpdate(optionsArg);
|
|
858
977
|
return this.enqueuePublicOperation(async (signal) => {
|
|
859
978
|
signal.throwIfAborted();
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
979
|
+
const screencast: ILiveBrowserScreencastOptions = {
|
|
980
|
+
...this.options.screencast,
|
|
981
|
+
...update,
|
|
982
|
+
};
|
|
983
|
+
validateScreencastOptions(screencast);
|
|
984
|
+
this.options.screencast = screencast;
|
|
985
|
+
const activeTab = this.status === 'running' && this.activeTabId
|
|
986
|
+
? this.tabs.get(this.activeTabId)
|
|
987
|
+
: undefined;
|
|
988
|
+
if (!activeTab?.streaming) {
|
|
989
|
+
return null;
|
|
863
990
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
991
|
+
return this.restartActiveScreencast(signal);
|
|
992
|
+
}, operationOptions);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private async restartActiveScreencast(
|
|
996
|
+
signal: AbortSignal,
|
|
997
|
+
): Promise<ILiveBrowserFrameIdentity> {
|
|
998
|
+
signal.throwIfAborted();
|
|
999
|
+
const tab = this.requireActiveTab();
|
|
1000
|
+
let firstFrame: ReturnType<LiveBrowserSession['waitForScreencastFrame']> | undefined;
|
|
1001
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1002
|
+
let restartPromise: Promise<void> | undefined;
|
|
1003
|
+
try {
|
|
1004
|
+
let refreshLifecycleRevision = tab.streamLifecycleRevision;
|
|
1005
|
+
if (tab.streaming) {
|
|
868
1006
|
await this.stopScreencast(tab);
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
])
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1007
|
+
refreshLifecycleRevision += 1;
|
|
1008
|
+
}
|
|
1009
|
+
if (
|
|
1010
|
+
!this.canRestoreScreencast(tab)
|
|
1011
|
+
|| tab.streaming
|
|
1012
|
+
|| tab.streamLifecycleRevision !== refreshLifecycleRevision
|
|
1013
|
+
) {
|
|
1014
|
+
throw new Error(`Screencast lifecycle changed while refreshing tab: ${tab.id}`);
|
|
1015
|
+
}
|
|
1016
|
+
const authority = this.createScreencastAuthority(tab, refreshLifecycleRevision);
|
|
1017
|
+
const generation = tab.generation + 1;
|
|
1018
|
+
firstFrame = this.waitForScreencastFrame(
|
|
1019
|
+
tab,
|
|
1020
|
+
generation,
|
|
1021
|
+
this.viewportRevision,
|
|
1022
|
+
authority.controller.signal,
|
|
1023
|
+
);
|
|
1024
|
+
const refreshTimeoutMs = this.options.screencast?.firstFrameTimeoutMs
|
|
1025
|
+
?? defaultFirstFrameTimeoutMs;
|
|
1026
|
+
const refreshTimeout = new Promise<never>((_resolve, reject) => {
|
|
1027
|
+
timeout = setTimeout(() => {
|
|
1028
|
+
reject(new Error(
|
|
1029
|
+
`Screencast generation ${generation} did not restart and produce a frame within ${
|
|
1030
|
+
refreshTimeoutMs
|
|
1031
|
+
}ms`,
|
|
1032
|
+
));
|
|
1033
|
+
}, refreshTimeoutMs);
|
|
1034
|
+
});
|
|
1035
|
+
restartPromise = this.startScreencast(tab, authority);
|
|
1036
|
+
const [, identity] = await Promise.race([
|
|
1037
|
+
Promise.all([restartPromise, firstFrame.promise]),
|
|
1038
|
+
refreshTimeout,
|
|
1039
|
+
]);
|
|
1040
|
+
return identity;
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
if (!this.canRestoreScreencast(tab)) throw error;
|
|
1043
|
+
await this.abandonScreencastRestart(tab, restartPromise, error);
|
|
1044
|
+
// Browser loss is the only refresh failure that is not tab-recoverable; the
|
|
1045
|
+
// disconnect listener normally revokes restoration first, so this stays a guard.
|
|
1046
|
+
const fatal = this.browser !== undefined && !this.browser.connected;
|
|
1047
|
+
const refreshError: ILiveBrowserError = {
|
|
1048
|
+
code: 'screencast_refresh_failed',
|
|
1049
|
+
message: normalizeErrorMessage(error),
|
|
1050
|
+
fatal,
|
|
1051
|
+
tabId: tab.id,
|
|
1052
|
+
};
|
|
1053
|
+
this.emitError(refreshError);
|
|
1054
|
+
if (fatal) {
|
|
910
1055
|
void this.requestShutdown(refreshError).catch((shutdownError) => {
|
|
911
1056
|
this.emitError({
|
|
912
1057
|
code: 'screencast_refresh_shutdown_failed',
|
|
@@ -915,12 +1060,27 @@ export class LiveBrowserSession {
|
|
|
915
1060
|
tabId: tab.id,
|
|
916
1061
|
});
|
|
917
1062
|
});
|
|
918
|
-
throw error;
|
|
919
|
-
} finally {
|
|
920
|
-
if (timeout) clearTimeout(timeout);
|
|
921
|
-
firstFrame?.cancel();
|
|
922
1063
|
}
|
|
923
|
-
|
|
1064
|
+
throw error;
|
|
1065
|
+
} finally {
|
|
1066
|
+
if (timeout) clearTimeout(timeout);
|
|
1067
|
+
firstFrame?.cancel();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private async abandonScreencastRestart(
|
|
1072
|
+
tab: IPrivateLiveBrowserTab,
|
|
1073
|
+
restartPromise: Promise<void> | undefined,
|
|
1074
|
+
reason: unknown,
|
|
1075
|
+
): Promise<void> {
|
|
1076
|
+
// Revoke the restart authority so an in-flight startScreencast() unwinds, then
|
|
1077
|
+
// retire anything it already established. The tab stays open and invalidated so
|
|
1078
|
+
// a later refresh, viewport change, or activation can start a new generation.
|
|
1079
|
+
this.invalidateScreencast(tab, reason);
|
|
1080
|
+
if (restartPromise) {
|
|
1081
|
+
await restartPromise.catch(() => {});
|
|
1082
|
+
}
|
|
1083
|
+
await this.stopScreencast(tab);
|
|
924
1084
|
}
|
|
925
1085
|
|
|
926
1086
|
public async createTab(
|
|
@@ -1150,43 +1310,73 @@ export class LiveBrowserSession {
|
|
|
1150
1310
|
const clickCount = input.clickCount === undefined
|
|
1151
1311
|
? undefined
|
|
1152
1312
|
: validateInteger(input.clickCount, 'clickCount', 0, 3);
|
|
1313
|
+
const modifiers = this.createModifierMask(input.modifiers);
|
|
1153
1314
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1315
|
+
await this.dispatchTabInput(tab, cdpSession, () => cdpSession.send(
|
|
1316
|
+
'Input.dispatchMouseEvent',
|
|
1317
|
+
{
|
|
1156
1318
|
type,
|
|
1157
1319
|
x: input.x,
|
|
1158
1320
|
y: input.y,
|
|
1159
1321
|
button,
|
|
1160
1322
|
buttons,
|
|
1161
1323
|
clickCount,
|
|
1162
|
-
modifiers
|
|
1324
|
+
modifiers,
|
|
1163
1325
|
pointerType: 'mouse',
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1167
|
-
throw error;
|
|
1168
|
-
}
|
|
1326
|
+
},
|
|
1327
|
+
));
|
|
1169
1328
|
}
|
|
1170
1329
|
|
|
1171
1330
|
public async dispatchWheel(input: ILiveBrowserWheelInput): Promise<void> {
|
|
1172
1331
|
const { tab, cdpSession } = this.requireRawInputTarget(input);
|
|
1173
1332
|
this.validateCoordinates(input.x, input.y);
|
|
1174
|
-
validateFiniteNumber(input.deltaX, 'deltaX', -
|
|
1175
|
-
validateFiniteNumber(input.deltaY, 'deltaY', -
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1333
|
+
const deltaX = validateFiniteNumber(input.deltaX, 'deltaX', -maxWheelDelta, maxWheelDelta);
|
|
1334
|
+
const deltaY = validateFiniteNumber(input.deltaY, 'deltaY', -maxWheelDelta, maxWheelDelta);
|
|
1335
|
+
const modifiers = this.createModifierMask(input.modifiers);
|
|
1336
|
+
|
|
1337
|
+
// Queued input only waits while an earlier wheel dispatch is in flight; wheel
|
|
1338
|
+
// input for the same stream merges into a wheel entry still waiting at the tail.
|
|
1339
|
+
const lastQueuedInput = tab.inputQueue[tab.inputQueue.length - 1];
|
|
1340
|
+
if (
|
|
1341
|
+
lastQueuedInput?.kind === 'wheel'
|
|
1342
|
+
&& lastQueuedInput.dispatch.cdpSession === cdpSession
|
|
1343
|
+
&& lastQueuedInput.dispatch.generation === input.generation
|
|
1344
|
+
&& lastQueuedInput.dispatch.viewportRevision === input.viewportRevision
|
|
1345
|
+
) {
|
|
1346
|
+
const pendingDispatch = lastQueuedInput.dispatch;
|
|
1347
|
+
pendingDispatch.x = input.x;
|
|
1348
|
+
pendingDispatch.y = input.y;
|
|
1349
|
+
pendingDispatch.deltaX = clampWheelDelta(pendingDispatch.deltaX + deltaX);
|
|
1350
|
+
pendingDispatch.deltaY = clampWheelDelta(pendingDispatch.deltaY + deltaY);
|
|
1351
|
+
pendingDispatch.modifiers = modifiers;
|
|
1352
|
+
return pendingDispatch.promise;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
let resolveDispatch!: () => void;
|
|
1356
|
+
let rejectDispatch!: (error: unknown) => void;
|
|
1357
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
1358
|
+
resolveDispatch = resolve;
|
|
1359
|
+
rejectDispatch = reject;
|
|
1360
|
+
});
|
|
1361
|
+
void promise.catch(() => {});
|
|
1362
|
+
tab.inputQueue.push({
|
|
1363
|
+
kind: 'wheel',
|
|
1364
|
+
dispatch: {
|
|
1365
|
+
cdpSession,
|
|
1366
|
+
generation: input.generation,
|
|
1367
|
+
viewportRevision: input.viewportRevision,
|
|
1179
1368
|
x: input.x,
|
|
1180
1369
|
y: input.y,
|
|
1181
|
-
deltaX
|
|
1182
|
-
deltaY
|
|
1183
|
-
modifiers
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1370
|
+
deltaX,
|
|
1371
|
+
deltaY,
|
|
1372
|
+
modifiers,
|
|
1373
|
+
promise,
|
|
1374
|
+
resolve: resolveDispatch,
|
|
1375
|
+
reject: rejectDispatch,
|
|
1376
|
+
},
|
|
1377
|
+
});
|
|
1378
|
+
this.pumpTabInput(tab);
|
|
1379
|
+
return promise;
|
|
1190
1380
|
}
|
|
1191
1381
|
|
|
1192
1382
|
public async dispatchKey(input: ILiveBrowserKeyInput): Promise<void> {
|
|
@@ -1220,9 +1410,11 @@ export class LiveBrowserSession {
|
|
|
1220
1410
|
: validateInteger(input.location, 'location', 0, 3);
|
|
1221
1411
|
validateOptionalBoolean(input.autoRepeat, 'autoRepeat');
|
|
1222
1412
|
validateOptionalBoolean(input.isKeypad, 'isKeypad');
|
|
1413
|
+
const modifiers = this.createModifierMask(input.modifiers);
|
|
1223
1414
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1415
|
+
await this.dispatchTabInput(tab, cdpSession, () => cdpSession.send(
|
|
1416
|
+
'Input.dispatchKeyEvent',
|
|
1417
|
+
{
|
|
1226
1418
|
type,
|
|
1227
1419
|
key,
|
|
1228
1420
|
code,
|
|
@@ -1233,22 +1425,97 @@ export class LiveBrowserSession {
|
|
|
1233
1425
|
autoRepeat: input.autoRepeat,
|
|
1234
1426
|
isKeypad: input.isKeypad,
|
|
1235
1427
|
location,
|
|
1236
|
-
modifiers
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1240
|
-
throw error;
|
|
1241
|
-
}
|
|
1428
|
+
modifiers,
|
|
1429
|
+
},
|
|
1430
|
+
));
|
|
1242
1431
|
}
|
|
1243
1432
|
|
|
1244
1433
|
public async insertText(input: ILiveBrowserInsertTextInput): Promise<void> {
|
|
1245
1434
|
const { tab, cdpSession } = this.requireRawInputTarget(input);
|
|
1246
1435
|
const text = validateBoundedString(input.text, 'text', 0, maxTextLength);
|
|
1436
|
+
await this.dispatchTabInput(tab, cdpSession, () => cdpSession.send(
|
|
1437
|
+
'Input.insertText',
|
|
1438
|
+
{ text },
|
|
1439
|
+
));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private dispatchTabInput(
|
|
1443
|
+
tab: IPrivateLiveBrowserTab,
|
|
1444
|
+
cdpSession: plugins.puppeteer.CDPSession,
|
|
1445
|
+
send: () => Promise<unknown>,
|
|
1446
|
+
): Promise<void> {
|
|
1447
|
+
const guardedSend = async (): Promise<void> => {
|
|
1448
|
+
try {
|
|
1449
|
+
await send();
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
// Chromium does not keep a pipelined mouse or key event behind an earlier wheel
|
|
1456
|
+
// command, so any input behind wheel input waits until that wheel dispatch settles.
|
|
1457
|
+
if (tab.inputQueue.length === 0 && !tab.wheelDispatchInFlight) {
|
|
1458
|
+
return guardedSend();
|
|
1459
|
+
}
|
|
1460
|
+
return new Promise<void>((resolve, reject) => {
|
|
1461
|
+
tab.inputQueue.push({ kind: 'event', send: guardedSend, resolve, reject });
|
|
1462
|
+
this.pumpTabInput(tab);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private pumpTabInput(tab: IPrivateLiveBrowserTab): void {
|
|
1467
|
+
if (tab.inputPumpActive) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
tab.inputPumpActive = true;
|
|
1471
|
+
void (async () => {
|
|
1472
|
+
try {
|
|
1473
|
+
while (tab.inputQueue.length > 0) {
|
|
1474
|
+
const wheelDispatchInFlight = tab.wheelDispatchInFlight;
|
|
1475
|
+
if (wheelDispatchInFlight) {
|
|
1476
|
+
await wheelDispatchInFlight;
|
|
1477
|
+
if (tab.wheelDispatchInFlight === wheelDispatchInFlight) {
|
|
1478
|
+
tab.wheelDispatchInFlight = undefined;
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
const nextInput = tab.inputQueue.shift()!;
|
|
1483
|
+
if (nextInput.kind === 'wheel') {
|
|
1484
|
+
const inFlight = this.performWheelDispatch(tab, nextInput.dispatch);
|
|
1485
|
+
tab.wheelDispatchInFlight = inFlight;
|
|
1486
|
+
void inFlight.then(() => {
|
|
1487
|
+
if (tab.wheelDispatchInFlight === inFlight) {
|
|
1488
|
+
tab.wheelDispatchInFlight = undefined;
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
} else {
|
|
1492
|
+
nextInput.send().then(nextInput.resolve, nextInput.reject);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
} finally {
|
|
1496
|
+
tab.inputPumpActive = false;
|
|
1497
|
+
}
|
|
1498
|
+
})();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private async performWheelDispatch(
|
|
1502
|
+
tab: IPrivateLiveBrowserTab,
|
|
1503
|
+
dispatch: IPendingWheelDispatch,
|
|
1504
|
+
): Promise<void> {
|
|
1247
1505
|
try {
|
|
1248
|
-
await cdpSession.send('Input.
|
|
1506
|
+
await dispatch.cdpSession.send('Input.dispatchMouseEvent', {
|
|
1507
|
+
type: 'mouseWheel',
|
|
1508
|
+
x: dispatch.x,
|
|
1509
|
+
y: dispatch.y,
|
|
1510
|
+
deltaX: dispatch.deltaX,
|
|
1511
|
+
deltaY: dispatch.deltaY,
|
|
1512
|
+
modifiers: dispatch.modifiers,
|
|
1513
|
+
pointerType: 'mouse',
|
|
1514
|
+
});
|
|
1515
|
+
dispatch.resolve();
|
|
1249
1516
|
} catch (error) {
|
|
1250
|
-
this.handlePossibleCdpDisconnection(tab, cdpSession, error);
|
|
1251
|
-
|
|
1517
|
+
this.handlePossibleCdpDisconnection(tab, dispatch.cdpSession, error);
|
|
1518
|
+
dispatch.reject(error);
|
|
1252
1519
|
}
|
|
1253
1520
|
}
|
|
1254
1521
|
|
|
@@ -3349,6 +3616,8 @@ export class LiveBrowserSession {
|
|
|
3349
3616
|
stateUpdatePending: false,
|
|
3350
3617
|
navigationResetRevision: 0,
|
|
3351
3618
|
restoredNavigationRevision: 0,
|
|
3619
|
+
inputQueue: [],
|
|
3620
|
+
inputPumpActive: false,
|
|
3352
3621
|
removeListeners: [],
|
|
3353
3622
|
};
|
|
3354
3623
|
this.tabs.set(tab.id, tab);
|
|
@@ -4739,50 +5008,4 @@ export class LiveBrowserSession {
|
|
|
4739
5008
|
return validatedWaitUntil;
|
|
4740
5009
|
}
|
|
4741
5010
|
|
|
4742
|
-
private validateScreencastOptions(): void {
|
|
4743
|
-
const options = this.options.screencast;
|
|
4744
|
-
if (!options) {
|
|
4745
|
-
return;
|
|
4746
|
-
}
|
|
4747
|
-
if (options.format !== undefined && options.format !== 'jpeg' && options.format !== 'png') {
|
|
4748
|
-
throw new Error('screencast.format must be jpeg or png');
|
|
4749
|
-
}
|
|
4750
|
-
if (options.quality !== undefined) {
|
|
4751
|
-
validateInteger(options.quality, 'screencast.quality', 0, 100);
|
|
4752
|
-
}
|
|
4753
|
-
if (options.maxWidth !== undefined) {
|
|
4754
|
-
validateInteger(options.maxWidth, 'screencast.maxWidth', 1, maxViewportWidth);
|
|
4755
|
-
}
|
|
4756
|
-
if (options.maxHeight !== undefined) {
|
|
4757
|
-
validateInteger(options.maxHeight, 'screencast.maxHeight', 1, maxViewportHeight);
|
|
4758
|
-
}
|
|
4759
|
-
if (
|
|
4760
|
-
options.maxWidth !== undefined
|
|
4761
|
-
&& options.maxHeight !== undefined
|
|
4762
|
-
&& options.maxWidth * options.maxHeight > maxViewportPixelArea
|
|
4763
|
-
) {
|
|
4764
|
-
throw new Error(
|
|
4765
|
-
`screencast pixel area must not exceed ${maxViewportPixelArea} pixels`,
|
|
4766
|
-
);
|
|
4767
|
-
}
|
|
4768
|
-
if (options.everyNthFrame !== undefined) {
|
|
4769
|
-
validateInteger(options.everyNthFrame, 'screencast.everyNthFrame', 1, 100);
|
|
4770
|
-
}
|
|
4771
|
-
if (options.maxOutstandingFrames !== undefined) {
|
|
4772
|
-
validateInteger(
|
|
4773
|
-
options.maxOutstandingFrames,
|
|
4774
|
-
'screencast.maxOutstandingFrames',
|
|
4775
|
-
1,
|
|
4776
|
-
liveBrowserMaxOutstandingFrames,
|
|
4777
|
-
);
|
|
4778
|
-
}
|
|
4779
|
-
if (options.firstFrameTimeoutMs !== undefined) {
|
|
4780
|
-
validateInteger(
|
|
4781
|
-
options.firstFrameTimeoutMs,
|
|
4782
|
-
'screencast.firstFrameTimeoutMs',
|
|
4783
|
-
100,
|
|
4784
|
-
maxTimeoutMs,
|
|
4785
|
-
);
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
5011
|
}
|
|
@@ -28,6 +28,12 @@ export interface ILiveBrowserScreencastOptions {
|
|
|
28
28
|
firstFrameTimeoutMs?: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export interface ILiveBrowserScreencastUpdateOptions
|
|
32
|
+
extends Pick<
|
|
33
|
+
ILiveBrowserScreencastOptions,
|
|
34
|
+
'quality' | 'maxWidth' | 'maxHeight' | 'everyNthFrame'
|
|
35
|
+
> {}
|
|
36
|
+
|
|
31
37
|
export interface ILiveBrowserSecurityOptions {
|
|
32
38
|
denyDownloads?: boolean;
|
|
33
39
|
denyFileChoosers?: boolean;
|