@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.
@@ -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.validateScreencastOptions();
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 tab = this.requireActiveTab();
861
- if (!tab.streaming || tab.streamInvalidated) {
862
- throw new Error(`Tab input transport is not available: ${tab.id}`);
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
- let firstFrame: ReturnType<LiveBrowserSession['waitForScreencastFrame']> | undefined;
865
- let timeout: ReturnType<typeof setTimeout> | undefined;
866
- try {
867
- const previousLifecycleRevision = tab.streamLifecycleRevision;
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
- const refreshLifecycleRevision = previousLifecycleRevision + 1;
870
- if (
871
- !this.canRestoreScreencast(tab)
872
- || tab.streamLifecycleRevision !== refreshLifecycleRevision
873
- ) {
874
- throw new Error(`Screencast lifecycle changed while refreshing tab: ${tab.id}`);
875
- }
876
- const authority = this.createScreencastAuthority(tab, refreshLifecycleRevision);
877
- const generation = tab.generation + 1;
878
- firstFrame = this.waitForScreencastFrame(
879
- tab,
880
- generation,
881
- this.viewportRevision,
882
- authority.controller.signal,
883
- );
884
- const refreshTimeoutMs = this.options.screencast?.firstFrameTimeoutMs
885
- ?? defaultFirstFrameTimeoutMs;
886
- const refreshTimeout = new Promise<never>((_resolve, reject) => {
887
- timeout = setTimeout(() => {
888
- reject(new Error(
889
- `Screencast generation ${generation} did not restart and produce a frame within ${
890
- refreshTimeoutMs
891
- }ms`,
892
- ));
893
- }, refreshTimeoutMs);
894
- });
895
- const restartPromise = this.startScreencast(tab, authority);
896
- const [, identity] = await Promise.race([
897
- Promise.all([restartPromise, firstFrame.promise]),
898
- refreshTimeout,
899
- ]);
900
- return identity;
901
- } catch (error) {
902
- if (!this.canRestoreScreencast(tab)) throw error;
903
- const refreshError: ILiveBrowserError = {
904
- code: 'screencast_refresh_failed',
905
- message: normalizeErrorMessage(error),
906
- fatal: true,
907
- tabId: tab.id,
908
- };
909
- this.emitError(refreshError);
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
- }, operationOptions);
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
- try {
1155
- await cdpSession.send('Input.dispatchMouseEvent', {
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: this.createModifierMask(input.modifiers),
1324
+ modifiers,
1163
1325
  pointerType: 'mouse',
1164
- });
1165
- } catch (error) {
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', -1000000, 1000000);
1175
- validateFiniteNumber(input.deltaY, 'deltaY', -1000000, 1000000);
1176
- try {
1177
- await cdpSession.send('Input.dispatchMouseEvent', {
1178
- type: 'mouseWheel',
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: input.deltaX,
1182
- deltaY: input.deltaY,
1183
- modifiers: this.createModifierMask(input.modifiers),
1184
- pointerType: 'mouse',
1185
- });
1186
- } catch (error) {
1187
- this.handlePossibleCdpDisconnection(tab, cdpSession, error);
1188
- throw error;
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
- try {
1225
- await cdpSession.send('Input.dispatchKeyEvent', {
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: this.createModifierMask(input.modifiers),
1237
- });
1238
- } catch (error) {
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.insertText', { text });
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
- throw error;
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;