@push.rocks/smartpuppeteer 2.3.0 → 2.5.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 +12 -1
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +338 -68
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +11 -5
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.js +3 -2
- package/package.json +1 -1
- package/readme.hints.md +2 -1
- package/readme.md +8 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +433 -89
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +13 -6
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { getEnvAwareBrowserInstance } from './smartpuppeteer.classes.smartpuppeteer.js';
|
|
2
|
+
import {
|
|
3
|
+
liveBrowserDefaultMaxOutstandingFrames,
|
|
4
|
+
liveBrowserMaxOutstandingFrames,
|
|
5
|
+
} from './smartpuppeteer.interfaces.livebrowser.js';
|
|
2
6
|
import {
|
|
3
7
|
delay,
|
|
4
8
|
type IOwnedProcessIdentity,
|
|
@@ -17,6 +21,7 @@ import type {
|
|
|
17
21
|
ILiveBrowserFrame,
|
|
18
22
|
ILiveBrowserFrameAcknowledgement,
|
|
19
23
|
ILiveBrowserFrameAcknowledgementRequest,
|
|
24
|
+
ILiveBrowserFrameIdentity,
|
|
20
25
|
ILiveBrowserInsertTextInput,
|
|
21
26
|
ILiveBrowserKeyInput,
|
|
22
27
|
ILiveBrowserModifierState,
|
|
@@ -59,7 +64,8 @@ const maxSelectorLength = 4096;
|
|
|
59
64
|
const maxTextLength = 32768;
|
|
60
65
|
const maxUrlLength = 16384;
|
|
61
66
|
const maxTimeoutMs = 60000;
|
|
62
|
-
const
|
|
67
|
+
const frameAcknowledgementTimeoutMs = 5000;
|
|
68
|
+
const defaultFirstFrameTimeoutMs = 5000;
|
|
63
69
|
const maxQueuedPublicOperations = 64;
|
|
64
70
|
const maxQueuedInternalOperations = 128;
|
|
65
71
|
const maxEvaluationScriptBytes = 262144;
|
|
@@ -119,6 +125,11 @@ interface IOwnedBrowserProcess {
|
|
|
119
125
|
forceSignalled: boolean;
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
interface IScreencastAuthority {
|
|
129
|
+
revision: number;
|
|
130
|
+
controller: AbortController;
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
interface IPrivateLiveBrowserTab {
|
|
123
134
|
id: string;
|
|
124
135
|
page: plugins.puppeteer.Page;
|
|
@@ -129,6 +140,8 @@ interface IPrivateLiveBrowserTab {
|
|
|
129
140
|
appliedViewportRevision: number;
|
|
130
141
|
streaming: boolean;
|
|
131
142
|
streamInvalidated: boolean;
|
|
143
|
+
streamLifecycleRevision: number;
|
|
144
|
+
screencastAuthority?: IScreencastAuthority;
|
|
132
145
|
navigationInProgress: boolean;
|
|
133
146
|
closing: boolean;
|
|
134
147
|
stateUpdateQueued: boolean;
|
|
@@ -143,12 +156,17 @@ interface IPrivateLiveBrowserTab {
|
|
|
143
156
|
removeListeners: Array<() => void>;
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
interface
|
|
159
|
+
interface ICdpScreencastFrame {
|
|
147
160
|
tabId: string;
|
|
148
161
|
generation: number;
|
|
149
162
|
viewportRevision: number;
|
|
150
163
|
cdpSessionId: number;
|
|
151
164
|
cdpSession: plugins.puppeteer.CDPSession;
|
|
165
|
+
acknowledgementPromise?: Promise<boolean>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface IOutstandingFrame extends ICdpScreencastFrame {
|
|
169
|
+
sequence: number;
|
|
152
170
|
}
|
|
153
171
|
|
|
154
172
|
interface IImageDimensions {
|
|
@@ -367,6 +385,8 @@ export class LiveBrowserSession {
|
|
|
367
385
|
private readonly tabs = new Map<string, IPrivateLiveBrowserTab>();
|
|
368
386
|
private readonly tabIdsByPage = new WeakMap<plugins.puppeteer.Page, string>();
|
|
369
387
|
private readonly outstandingFrames = new Map<number, IOutstandingFrame>();
|
|
388
|
+
private readonly cdpFramesBeingAcknowledged = new Set<ICdpScreencastFrame>();
|
|
389
|
+
private readonly maxOutstandingFrames: number;
|
|
370
390
|
|
|
371
391
|
private browser?: plugins.puppeteer.Browser;
|
|
372
392
|
private browserContext?: plugins.puppeteer.BrowserContext;
|
|
@@ -392,6 +412,7 @@ export class LiveBrowserSession {
|
|
|
392
412
|
private admittedPublicOperations = 0;
|
|
393
413
|
private admittedInternalOperations = 0;
|
|
394
414
|
private shutdownPromise?: Promise<void>;
|
|
415
|
+
private shutdownFrameDrainPromise?: Promise<void>;
|
|
395
416
|
private status: 'stopped' | 'starting' | 'running' | 'stopping' = 'stopped';
|
|
396
417
|
private activeTabId: string | null = null;
|
|
397
418
|
private viewport: ILiveBrowserViewport = { ...defaultViewport };
|
|
@@ -418,6 +439,16 @@ export class LiveBrowserSession {
|
|
|
418
439
|
if (optionsArg.launchOptions && 'signal' in optionsArg.launchOptions) {
|
|
419
440
|
throw new Error('LiveBrowserSession owns launch cancellation; launchOptions.signal is unsupported');
|
|
420
441
|
}
|
|
442
|
+
if (
|
|
443
|
+
optionsArg.screencast !== undefined
|
|
444
|
+
&& (
|
|
445
|
+
!optionsArg.screencast
|
|
446
|
+
|| typeof optionsArg.screencast !== 'object'
|
|
447
|
+
|| Array.isArray(optionsArg.screencast)
|
|
448
|
+
)
|
|
449
|
+
) {
|
|
450
|
+
throw new Error('screencast must be an object');
|
|
451
|
+
}
|
|
421
452
|
validateOptionalBoolean(optionsArg.allowEvaluation, 'allowEvaluation');
|
|
422
453
|
for (const name of [
|
|
423
454
|
'denyDownloads',
|
|
@@ -477,6 +508,8 @@ export class LiveBrowserSession {
|
|
|
477
508
|
};
|
|
478
509
|
this.viewport = { ...viewport };
|
|
479
510
|
this.validateScreencastOptions();
|
|
511
|
+
this.maxOutstandingFrames = this.options.screencast?.maxOutstandingFrames
|
|
512
|
+
?? liveBrowserDefaultMaxOutstandingFrames;
|
|
480
513
|
}
|
|
481
514
|
|
|
482
515
|
public onEvent(listener: TLiveBrowserEventListener): () => void {
|
|
@@ -802,6 +835,77 @@ export class LiveBrowserSession {
|
|
|
802
835
|
};
|
|
803
836
|
}
|
|
804
837
|
|
|
838
|
+
public refreshScreencast(
|
|
839
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
840
|
+
): Promise<ILiveBrowserFrameIdentity> {
|
|
841
|
+
return this.enqueuePublicOperation(async (signal) => {
|
|
842
|
+
signal.throwIfAborted();
|
|
843
|
+
const tab = this.requireActiveTab();
|
|
844
|
+
if (!tab.streaming || tab.streamInvalidated) {
|
|
845
|
+
throw new Error(`Tab input transport is not available: ${tab.id}`);
|
|
846
|
+
}
|
|
847
|
+
let firstFrame: ReturnType<LiveBrowserSession['waitForScreencastFrame']> | undefined;
|
|
848
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
849
|
+
try {
|
|
850
|
+
const previousLifecycleRevision = tab.streamLifecycleRevision;
|
|
851
|
+
await this.stopScreencast(tab);
|
|
852
|
+
const refreshLifecycleRevision = previousLifecycleRevision + 1;
|
|
853
|
+
if (
|
|
854
|
+
!this.canRestoreScreencast(tab)
|
|
855
|
+
|| tab.streamLifecycleRevision !== refreshLifecycleRevision
|
|
856
|
+
) {
|
|
857
|
+
throw new Error(`Screencast lifecycle changed while refreshing tab: ${tab.id}`);
|
|
858
|
+
}
|
|
859
|
+
const authority = this.createScreencastAuthority(tab, refreshLifecycleRevision);
|
|
860
|
+
const generation = tab.generation + 1;
|
|
861
|
+
firstFrame = this.waitForScreencastFrame(
|
|
862
|
+
tab,
|
|
863
|
+
generation,
|
|
864
|
+
this.viewportRevision,
|
|
865
|
+
authority.controller.signal,
|
|
866
|
+
);
|
|
867
|
+
const refreshTimeoutMs = this.options.screencast?.firstFrameTimeoutMs
|
|
868
|
+
?? defaultFirstFrameTimeoutMs;
|
|
869
|
+
const refreshTimeout = new Promise<never>((_resolve, reject) => {
|
|
870
|
+
timeout = setTimeout(() => {
|
|
871
|
+
reject(new Error(
|
|
872
|
+
`Screencast generation ${generation} did not restart and produce a frame within ${
|
|
873
|
+
refreshTimeoutMs
|
|
874
|
+
}ms`,
|
|
875
|
+
));
|
|
876
|
+
}, refreshTimeoutMs);
|
|
877
|
+
});
|
|
878
|
+
const restartPromise = this.startScreencast(tab, authority);
|
|
879
|
+
const [, identity] = await Promise.race([
|
|
880
|
+
Promise.all([restartPromise, firstFrame.promise]),
|
|
881
|
+
refreshTimeout,
|
|
882
|
+
]);
|
|
883
|
+
return identity;
|
|
884
|
+
} catch (error) {
|
|
885
|
+
if (!this.canRestoreScreencast(tab)) throw error;
|
|
886
|
+
const refreshError: ILiveBrowserError = {
|
|
887
|
+
code: 'screencast_refresh_failed',
|
|
888
|
+
message: normalizeErrorMessage(error),
|
|
889
|
+
fatal: true,
|
|
890
|
+
tabId: tab.id,
|
|
891
|
+
};
|
|
892
|
+
this.emitError(refreshError);
|
|
893
|
+
void this.requestShutdown(refreshError).catch((shutdownError) => {
|
|
894
|
+
this.emitError({
|
|
895
|
+
code: 'screencast_refresh_shutdown_failed',
|
|
896
|
+
message: normalizeErrorMessage(shutdownError),
|
|
897
|
+
fatal: true,
|
|
898
|
+
tabId: tab.id,
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
throw error;
|
|
902
|
+
} finally {
|
|
903
|
+
if (timeout) clearTimeout(timeout);
|
|
904
|
+
firstFrame?.cancel();
|
|
905
|
+
}
|
|
906
|
+
}, operationOptions);
|
|
907
|
+
}
|
|
908
|
+
|
|
805
909
|
public async createTab(
|
|
806
910
|
optionsArg: ILiveBrowserCreateTabOptions = {},
|
|
807
911
|
operationOptions: ILiveBrowserOperationOptions = {},
|
|
@@ -1747,15 +1851,30 @@ export class LiveBrowserSession {
|
|
|
1747
1851
|
this.emitState();
|
|
1748
1852
|
}
|
|
1749
1853
|
const shutdownError = new Error('LiveBrowserSession is stopping');
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
&& this.activeOperation
|
|
1753
|
-
&& this.activeOperation.kind !== 'shutdown'
|
|
1754
|
-
) {
|
|
1755
|
-
this.activeOperation.controller.abort(shutdownError);
|
|
1854
|
+
for (const tab of this.tabs.values()) {
|
|
1855
|
+
this.invalidateScreencast(tab, shutdownError);
|
|
1756
1856
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1857
|
+
const activeOperation = this.activeOperation;
|
|
1858
|
+
const shouldAbortActiveOperation = Boolean(
|
|
1859
|
+
abortActiveOperation
|
|
1860
|
+
&& activeOperation
|
|
1861
|
+
&& activeOperation.kind !== 'shutdown'
|
|
1862
|
+
);
|
|
1863
|
+
if (shouldAbortActiveOperation && activeOperation) {
|
|
1864
|
+
const browserLifetimeController = this.browserLifetimeController;
|
|
1865
|
+
const frameDrainPromise = (async () => {
|
|
1866
|
+
try {
|
|
1867
|
+
await this.retireOutstandingFrames(() => true);
|
|
1868
|
+
await this.waitForCdpFrameAcknowledgements();
|
|
1869
|
+
} finally {
|
|
1870
|
+
if (browserLifetimeController && !browserLifetimeController.signal.aborted) {
|
|
1871
|
+
browserLifetimeController.abort(shutdownError);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
})();
|
|
1875
|
+
this.shutdownFrameDrainPromise = frameDrainPromise;
|
|
1876
|
+
void frameDrainPromise.catch(() => {});
|
|
1877
|
+
activeOperation.controller.abort(shutdownError);
|
|
1759
1878
|
}
|
|
1760
1879
|
this.cancelQueuedOperations(shutdownError);
|
|
1761
1880
|
}
|
|
@@ -2126,10 +2245,6 @@ export class LiveBrowserSession {
|
|
|
2126
2245
|
this.lastError = { ...error };
|
|
2127
2246
|
}
|
|
2128
2247
|
this.emitState();
|
|
2129
|
-
if (this.browserLifetimeController && !this.browserLifetimeController.signal.aborted) {
|
|
2130
|
-
this.browserLifetimeController.abort(new Error('LiveBrowserSession is stopping'));
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
2248
|
const browser = this.browser;
|
|
2134
2249
|
if (browser && this.browserTargetCreatedListener) {
|
|
2135
2250
|
browser.off('targetcreated', this.browserTargetCreatedListener);
|
|
@@ -2141,6 +2256,15 @@ export class LiveBrowserSession {
|
|
|
2141
2256
|
this.browserDisconnectedListener = undefined;
|
|
2142
2257
|
|
|
2143
2258
|
const shutdownErrors: unknown[] = [];
|
|
2259
|
+
const shutdownFrameDrainPromise = this.shutdownFrameDrainPromise;
|
|
2260
|
+
this.shutdownFrameDrainPromise = undefined;
|
|
2261
|
+
if (shutdownFrameDrainPromise) {
|
|
2262
|
+
try {
|
|
2263
|
+
await shutdownFrameDrainPromise;
|
|
2264
|
+
} catch (cleanupError) {
|
|
2265
|
+
shutdownErrors.push(cleanupError);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2144
2268
|
try {
|
|
2145
2269
|
await this.teardownProxySecurity();
|
|
2146
2270
|
} catch (cleanupError) {
|
|
@@ -2173,6 +2297,10 @@ export class LiveBrowserSession {
|
|
|
2173
2297
|
} catch (cleanupError) {
|
|
2174
2298
|
shutdownErrors.push(cleanupError);
|
|
2175
2299
|
}
|
|
2300
|
+
await this.waitForCdpFrameAcknowledgements();
|
|
2301
|
+
if (this.browserLifetimeController && !this.browserLifetimeController.signal.aborted) {
|
|
2302
|
+
this.browserLifetimeController.abort(new Error('LiveBrowserSession is stopping'));
|
|
2303
|
+
}
|
|
2176
2304
|
|
|
2177
2305
|
if (browser) {
|
|
2178
2306
|
try {
|
|
@@ -2794,6 +2922,7 @@ export class LiveBrowserSession {
|
|
|
2794
2922
|
appliedViewportRevision: 0,
|
|
2795
2923
|
streaming: false,
|
|
2796
2924
|
streamInvalidated: true,
|
|
2925
|
+
streamLifecycleRevision: 0,
|
|
2797
2926
|
navigationInProgress: false,
|
|
2798
2927
|
closing: false,
|
|
2799
2928
|
stateUpdateQueued: false,
|
|
@@ -2874,7 +3003,7 @@ export class LiveBrowserSession {
|
|
|
2874
3003
|
tab.evaluationExecutionContextId = undefined;
|
|
2875
3004
|
const navigationReset = !tab.navigationInProgress;
|
|
2876
3005
|
if (navigationReset) {
|
|
2877
|
-
tab
|
|
3006
|
+
this.invalidateScreencast(tab, new Error('Page navigation invalidated the screencast'));
|
|
2878
3007
|
this.retireFramesForTabInBackground(tab.id);
|
|
2879
3008
|
}
|
|
2880
3009
|
this.requestTabStateUpdate(tab, navigationReset);
|
|
@@ -2883,7 +3012,7 @@ export class LiveBrowserSession {
|
|
|
2883
3012
|
this.requestTabStateUpdate(tab, false);
|
|
2884
3013
|
};
|
|
2885
3014
|
const onClose = (): void => {
|
|
2886
|
-
tab
|
|
3015
|
+
this.invalidateScreencast(tab, new Error('Page closure invalidated the screencast'));
|
|
2887
3016
|
this.retireFramesForTabInBackground(tab.id);
|
|
2888
3017
|
if (tab.closing || this.normalStopRequested) {
|
|
2889
3018
|
return;
|
|
@@ -2893,7 +3022,7 @@ export class LiveBrowserSession {
|
|
|
2893
3022
|
}, 'page_close_cleanup_failed', tab.id, true);
|
|
2894
3023
|
};
|
|
2895
3024
|
const onCrash = (error: Error): void => {
|
|
2896
|
-
tab
|
|
3025
|
+
this.invalidateScreencast(tab, error);
|
|
2897
3026
|
this.retireFramesForTabInBackground(tab.id);
|
|
2898
3027
|
if (tab.closing || this.normalStopRequested) {
|
|
2899
3028
|
return;
|
|
@@ -3256,7 +3385,10 @@ export class LiveBrowserSession {
|
|
|
3256
3385
|
}
|
|
3257
3386
|
}
|
|
3258
3387
|
|
|
3259
|
-
private async startScreencast(
|
|
3388
|
+
private async startScreencast(
|
|
3389
|
+
tab: IPrivateLiveBrowserTab,
|
|
3390
|
+
authorityArg?: IScreencastAuthority,
|
|
3391
|
+
): Promise<void> {
|
|
3260
3392
|
if (
|
|
3261
3393
|
this.status !== 'running'
|
|
3262
3394
|
|| this.activeTabId !== tab.id
|
|
@@ -3267,78 +3399,254 @@ export class LiveBrowserSession {
|
|
|
3267
3399
|
return;
|
|
3268
3400
|
}
|
|
3269
3401
|
|
|
3270
|
-
|
|
3402
|
+
const authority = authorityArg
|
|
3403
|
+
?? this.createScreencastAuthority(tab, tab.streamLifecycleRevision);
|
|
3404
|
+
let cdpSession: plugins.puppeteer.CDPSession | undefined;
|
|
3405
|
+
let cdpConnection: plugins.puppeteer.Connection | undefined;
|
|
3406
|
+
let frameListener: TScreencastFrameListener | undefined;
|
|
3407
|
+
let cdpSessionDetachedListener: TCdpSessionDetachedListener | undefined;
|
|
3408
|
+
try {
|
|
3409
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3410
|
+
await this.waitForScreencastAuthority(this.ensureTabViewport(tab), authority);
|
|
3411
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3271
3412
|
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3413
|
+
const cdpSessionPromise = tab.page.createCDPSession();
|
|
3414
|
+
void cdpSessionPromise.then(async (createdSession) => {
|
|
3415
|
+
if (
|
|
3416
|
+
tab.screencastAuthority === authority
|
|
3417
|
+
&& !authority.controller.signal.aborted
|
|
3418
|
+
) {
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
this.allowOperationalCdpSessionDetach(createdSession);
|
|
3422
|
+
if (!createdSession.detached) {
|
|
3423
|
+
try {
|
|
3424
|
+
await createdSession.detach();
|
|
3425
|
+
} catch {
|
|
3426
|
+
// Browser shutdown may detach a late-created session first.
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}).catch(() => {});
|
|
3430
|
+
cdpSession = await this.waitForScreencastAuthority(cdpSessionPromise, authority);
|
|
3431
|
+
this.allowOperationalCdpSessionDetach(cdpSession);
|
|
3432
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3433
|
+
const generation = tab.generation + 1;
|
|
3434
|
+
frameListener = (event) => {
|
|
3435
|
+
this.handleScreencastFrame(tab, cdpSession!, generation, event);
|
|
3436
|
+
};
|
|
3437
|
+
cdpConnection = cdpSession.connection();
|
|
3438
|
+
cdpSessionDetachedListener = (detachedSession) => {
|
|
3439
|
+
if (detachedSession !== cdpSession) {
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
this.handlePossibleCdpDisconnection(
|
|
3443
|
+
tab,
|
|
3444
|
+
cdpSession!,
|
|
3445
|
+
new Error('The tab CDP session disconnected'),
|
|
3446
|
+
);
|
|
3447
|
+
};
|
|
3448
|
+
tab.cdpSession = cdpSession;
|
|
3449
|
+
tab.cdpConnection = cdpConnection;
|
|
3450
|
+
tab.screencastFrameListener = frameListener;
|
|
3451
|
+
tab.cdpSessionDetachedListener = cdpSessionDetachedListener;
|
|
3452
|
+
tab.generation = generation;
|
|
3453
|
+
tab.streaming = true;
|
|
3454
|
+
tab.streamInvalidated = false;
|
|
3455
|
+
cdpSession.on('Page.screencastFrame', frameListener);
|
|
3456
|
+
cdpConnection?.on(
|
|
3457
|
+
plugins.puppeteer.CDPSessionEvent.SessionDetached,
|
|
3458
|
+
cdpSessionDetachedListener,
|
|
3287
3459
|
);
|
|
3288
|
-
};
|
|
3289
|
-
tab.cdpSession = cdpSession;
|
|
3290
|
-
tab.cdpConnection = cdpConnection;
|
|
3291
|
-
tab.screencastFrameListener = frameListener;
|
|
3292
|
-
tab.cdpSessionDetachedListener = cdpSessionDetachedListener;
|
|
3293
|
-
tab.generation = generation;
|
|
3294
|
-
tab.streaming = true;
|
|
3295
|
-
tab.streamInvalidated = false;
|
|
3296
|
-
cdpSession.on('Page.screencastFrame', frameListener);
|
|
3297
|
-
cdpConnection?.on(
|
|
3298
|
-
plugins.puppeteer.CDPSessionEvent.SessionDetached,
|
|
3299
|
-
cdpSessionDetachedListener,
|
|
3300
|
-
);
|
|
3301
3460
|
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3461
|
+
const format = this.options.screencast?.format ?? 'jpeg';
|
|
3462
|
+
await this.waitForScreencastAuthority(
|
|
3463
|
+
cdpSession.send('Page.startScreencast', {
|
|
3464
|
+
format,
|
|
3465
|
+
quality: this.options.screencast?.quality ?? 80,
|
|
3466
|
+
maxWidth: this.options.screencast?.maxWidth,
|
|
3467
|
+
maxHeight: this.options.screencast?.maxHeight,
|
|
3468
|
+
everyNthFrame: this.options.screencast?.everyNthFrame ?? 1,
|
|
3469
|
+
}),
|
|
3470
|
+
authority,
|
|
3471
|
+
);
|
|
3472
|
+
this.assertScreencastAuthority(tab, authority);
|
|
3311
3473
|
this.emitState();
|
|
3312
3474
|
} catch (error) {
|
|
3313
|
-
tab.
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
)
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3475
|
+
if (tab.cdpSession === cdpSession) {
|
|
3476
|
+
tab.streaming = false;
|
|
3477
|
+
tab.cdpSession = undefined;
|
|
3478
|
+
tab.cdpConnection = undefined;
|
|
3479
|
+
tab.screencastFrameListener = undefined;
|
|
3480
|
+
tab.cdpSessionDetachedListener = undefined;
|
|
3481
|
+
}
|
|
3482
|
+
if (tab.screencastAuthority === authority) {
|
|
3483
|
+
this.invalidateScreencast(tab, error);
|
|
3484
|
+
}
|
|
3485
|
+
if (cdpSession) {
|
|
3486
|
+
if (frameListener) {
|
|
3487
|
+
cdpSession.off('Page.screencastFrame', frameListener);
|
|
3488
|
+
}
|
|
3489
|
+
if (cdpConnection && cdpSessionDetachedListener) {
|
|
3490
|
+
cdpConnection.off(
|
|
3491
|
+
plugins.puppeteer.CDPSessionEvent.SessionDetached,
|
|
3492
|
+
cdpSessionDetachedListener,
|
|
3493
|
+
);
|
|
3494
|
+
}
|
|
3495
|
+
await this.retireOutstandingFrames((frame) => frame.cdpSession === cdpSession);
|
|
3496
|
+
await this.waitForCdpFrameAcknowledgements(cdpSession);
|
|
3497
|
+
if (!cdpSession.detached) {
|
|
3498
|
+
try {
|
|
3499
|
+
await cdpSession.detach();
|
|
3500
|
+
} catch {
|
|
3501
|
+
// The target may have closed while screencast startup was failing.
|
|
3502
|
+
}
|
|
3329
3503
|
}
|
|
3330
3504
|
}
|
|
3331
3505
|
throw error;
|
|
3332
3506
|
}
|
|
3333
3507
|
}
|
|
3334
3508
|
|
|
3509
|
+
private canRestoreScreencast(tab: IPrivateLiveBrowserTab): boolean {
|
|
3510
|
+
return this.status === 'running'
|
|
3511
|
+
&& !this.normalStopRequested
|
|
3512
|
+
&& this.activeTabId === tab.id
|
|
3513
|
+
&& this.tabs.get(tab.id) === tab
|
|
3514
|
+
&& tab.status === 'open'
|
|
3515
|
+
&& !tab.closing
|
|
3516
|
+
&& !tab.page.isClosed();
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
private createScreencastAuthority(
|
|
3520
|
+
tab: IPrivateLiveBrowserTab,
|
|
3521
|
+
expectedLifecycleRevision: number,
|
|
3522
|
+
): IScreencastAuthority {
|
|
3523
|
+
if (
|
|
3524
|
+
!this.canRestoreScreencast(tab)
|
|
3525
|
+
|| tab.streaming
|
|
3526
|
+
|| tab.streamLifecycleRevision !== expectedLifecycleRevision
|
|
3527
|
+
) {
|
|
3528
|
+
throw new Error(`Screencast lifecycle authority is unavailable for tab: ${tab.id}`);
|
|
3529
|
+
}
|
|
3530
|
+
const authority: IScreencastAuthority = {
|
|
3531
|
+
revision: expectedLifecycleRevision,
|
|
3532
|
+
controller: new AbortController(),
|
|
3533
|
+
};
|
|
3534
|
+
tab.screencastAuthority = authority;
|
|
3535
|
+
return authority;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
private assertScreencastAuthority(
|
|
3539
|
+
tab: IPrivateLiveBrowserTab,
|
|
3540
|
+
authority: IScreencastAuthority,
|
|
3541
|
+
): void {
|
|
3542
|
+
if (
|
|
3543
|
+
this.canRestoreScreencast(tab)
|
|
3544
|
+
&& tab.streamLifecycleRevision === authority.revision
|
|
3545
|
+
&& tab.screencastAuthority === authority
|
|
3546
|
+
&& !authority.controller.signal.aborted
|
|
3547
|
+
) {
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
throw authority.controller.signal.aborted
|
|
3551
|
+
? normalizeAbortReason(authority.controller.signal)
|
|
3552
|
+
: new Error(`Screencast lifecycle authority was revoked for tab: ${tab.id}`);
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
private waitForScreencastAuthority<T>(
|
|
3556
|
+
operation: Promise<T>,
|
|
3557
|
+
authority: IScreencastAuthority,
|
|
3558
|
+
): Promise<T> {
|
|
3559
|
+
const signal = authority.controller.signal;
|
|
3560
|
+
if (signal.aborted) {
|
|
3561
|
+
return Promise.reject(normalizeAbortReason(signal));
|
|
3562
|
+
}
|
|
3563
|
+
return new Promise<T>((resolve, reject) => {
|
|
3564
|
+
let settled = false;
|
|
3565
|
+
const finish = (actionArg: () => void): void => {
|
|
3566
|
+
if (settled) return;
|
|
3567
|
+
settled = true;
|
|
3568
|
+
signal.removeEventListener('abort', handleAbort);
|
|
3569
|
+
actionArg();
|
|
3570
|
+
};
|
|
3571
|
+
const handleAbort = (): void => {
|
|
3572
|
+
finish(() => reject(normalizeAbortReason(signal)));
|
|
3573
|
+
};
|
|
3574
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
3575
|
+
operation.then(
|
|
3576
|
+
(value) => finish(() => resolve(value)),
|
|
3577
|
+
(error) => finish(() => reject(error)),
|
|
3578
|
+
);
|
|
3579
|
+
if (signal.aborted) handleAbort();
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
private invalidateScreencast(tab: IPrivateLiveBrowserTab, reason: unknown): void {
|
|
3584
|
+
tab.streamInvalidated = true;
|
|
3585
|
+
tab.streamLifecycleRevision += 1;
|
|
3586
|
+
const authority = tab.screencastAuthority;
|
|
3587
|
+
tab.screencastAuthority = undefined;
|
|
3588
|
+
if (authority && !authority.controller.signal.aborted) {
|
|
3589
|
+
authority.controller.abort(reason);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
private waitForScreencastFrame(
|
|
3594
|
+
tab: IPrivateLiveBrowserTab,
|
|
3595
|
+
generation: number,
|
|
3596
|
+
viewportRevision: number,
|
|
3597
|
+
invalidationSignal: AbortSignal,
|
|
3598
|
+
): {
|
|
3599
|
+
promise: Promise<ILiveBrowserFrameIdentity>;
|
|
3600
|
+
cancel: () => void;
|
|
3601
|
+
} {
|
|
3602
|
+
let settled = false;
|
|
3603
|
+
let resolvePromise!: (identityArg: ILiveBrowserFrameIdentity) => void;
|
|
3604
|
+
let rejectPromise!: (errorArg: unknown) => void;
|
|
3605
|
+
const promise = new Promise<ILiveBrowserFrameIdentity>((resolve, reject) => {
|
|
3606
|
+
resolvePromise = resolve;
|
|
3607
|
+
rejectPromise = reject;
|
|
3608
|
+
});
|
|
3609
|
+
void promise.catch(() => {});
|
|
3610
|
+
let unsubscribe: () => void = () => {};
|
|
3611
|
+
const finish = (actionArg: () => void) => {
|
|
3612
|
+
if (settled) return;
|
|
3613
|
+
settled = true;
|
|
3614
|
+
invalidationSignal.removeEventListener('abort', handleAbort);
|
|
3615
|
+
unsubscribe();
|
|
3616
|
+
actionArg();
|
|
3617
|
+
};
|
|
3618
|
+
const handleAbort = () => {
|
|
3619
|
+
finish(() => rejectPromise(normalizeAbortReason(invalidationSignal)));
|
|
3620
|
+
};
|
|
3621
|
+
unsubscribe = this.onEvent((eventArg) => {
|
|
3622
|
+
if (
|
|
3623
|
+
eventArg.type !== 'frame'
|
|
3624
|
+
|| eventArg.frame.tabId !== tab.id
|
|
3625
|
+
|| eventArg.frame.generation !== generation
|
|
3626
|
+
|| eventArg.frame.viewportRevision !== viewportRevision
|
|
3627
|
+
) return;
|
|
3628
|
+
finish(() => resolvePromise({
|
|
3629
|
+
tabId: eventArg.frame.tabId,
|
|
3630
|
+
sequence: eventArg.frame.sequence,
|
|
3631
|
+
generation: eventArg.frame.generation,
|
|
3632
|
+
viewportRevision: eventArg.frame.viewportRevision,
|
|
3633
|
+
}));
|
|
3634
|
+
});
|
|
3635
|
+
invalidationSignal.addEventListener('abort', handleAbort, { once: true });
|
|
3636
|
+
if (invalidationSignal.aborted) handleAbort();
|
|
3637
|
+
return {
|
|
3638
|
+
promise,
|
|
3639
|
+
cancel: () => finish(() => rejectPromise(new Error('Screencast frame wait was cancelled'))),
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3335
3643
|
private async stopScreencast(tab: IPrivateLiveBrowserTab): Promise<void> {
|
|
3336
3644
|
const cdpSession = tab.cdpSession;
|
|
3337
3645
|
const cdpConnection = tab.cdpConnection;
|
|
3338
3646
|
const frameListener = tab.screencastFrameListener;
|
|
3339
3647
|
const cdpSessionDetachedListener = tab.cdpSessionDetachedListener;
|
|
3340
3648
|
tab.streaming = false;
|
|
3341
|
-
tab
|
|
3649
|
+
this.invalidateScreencast(tab, new Error('Screencast stopped'));
|
|
3342
3650
|
tab.cdpSession = undefined;
|
|
3343
3651
|
tab.cdpConnection = undefined;
|
|
3344
3652
|
tab.screencastFrameListener = undefined;
|
|
@@ -3365,6 +3673,7 @@ export class LiveBrowserSession {
|
|
|
3365
3673
|
cdpSessionDetachedListener,
|
|
3366
3674
|
);
|
|
3367
3675
|
}
|
|
3676
|
+
await this.waitForCdpFrameAcknowledgements(cdpSession);
|
|
3368
3677
|
if (!cdpSession.detached) {
|
|
3369
3678
|
try {
|
|
3370
3679
|
await cdpSession.detach();
|
|
@@ -3409,12 +3718,13 @@ export class LiveBrowserSession {
|
|
|
3409
3718
|
const sequence = ++this.frameSequence;
|
|
3410
3719
|
const outstandingFrame: IOutstandingFrame = {
|
|
3411
3720
|
tabId: tab.id,
|
|
3721
|
+
sequence,
|
|
3412
3722
|
generation,
|
|
3413
3723
|
viewportRevision: this.viewportRevision,
|
|
3414
3724
|
cdpSessionId: event.sessionId,
|
|
3415
3725
|
cdpSession,
|
|
3416
3726
|
};
|
|
3417
|
-
while (this.outstandingFrames.size >= maxOutstandingFrames) {
|
|
3727
|
+
while (this.outstandingFrames.size >= this.maxOutstandingFrames) {
|
|
3418
3728
|
const oldestFrameEntry = this.outstandingFrames.entries().next().value as
|
|
3419
3729
|
| [number, IOutstandingFrame]
|
|
3420
3730
|
| undefined;
|
|
@@ -3450,31 +3760,40 @@ export class LiveBrowserSession {
|
|
|
3450
3760
|
this.emitEvent({ type: 'frame', frame });
|
|
3451
3761
|
}
|
|
3452
3762
|
|
|
3453
|
-
private
|
|
3454
|
-
if (frame.
|
|
3455
|
-
|
|
3456
|
-
frame,
|
|
3457
|
-
new Error('CDP session detached before frame acknowledgement'),
|
|
3458
|
-
);
|
|
3459
|
-
return false;
|
|
3763
|
+
private acknowledgeCdpFrame(frame: ICdpScreencastFrame): Promise<boolean> {
|
|
3764
|
+
if (frame.acknowledgementPromise) {
|
|
3765
|
+
return frame.acknowledgementPromise;
|
|
3460
3766
|
}
|
|
3461
|
-
|
|
3767
|
+
let resolveAcknowledgement!: (accepted: boolean) => void;
|
|
3768
|
+
const acknowledgementPromise = new Promise<boolean>((resolve) => {
|
|
3769
|
+
resolveAcknowledgement = resolve;
|
|
3770
|
+
});
|
|
3771
|
+
frame.acknowledgementPromise = acknowledgementPromise;
|
|
3772
|
+
this.cdpFramesBeingAcknowledged.add(frame);
|
|
3773
|
+
void (async (): Promise<boolean> => {
|
|
3774
|
+
if (frame.cdpSession.detached) {
|
|
3775
|
+
throw new Error('CDP session detached before frame acknowledgement');
|
|
3776
|
+
}
|
|
3462
3777
|
await frame.cdpSession.send('Page.screencastFrameAck', {
|
|
3463
3778
|
sessionId: frame.cdpSessionId,
|
|
3464
|
-
});
|
|
3779
|
+
}, { timeout: frameAcknowledgementTimeoutMs });
|
|
3465
3780
|
return true;
|
|
3466
|
-
}
|
|
3781
|
+
})().then(resolveAcknowledgement, (error) => {
|
|
3467
3782
|
this.handleFrameAcknowledgementFailure(frame, error);
|
|
3468
|
-
|
|
3469
|
-
}
|
|
3783
|
+
resolveAcknowledgement(false);
|
|
3784
|
+
});
|
|
3785
|
+
void acknowledgementPromise.then(() => {
|
|
3786
|
+
this.cdpFramesBeingAcknowledged.delete(frame);
|
|
3787
|
+
});
|
|
3788
|
+
return acknowledgementPromise;
|
|
3470
3789
|
}
|
|
3471
3790
|
|
|
3472
|
-
private acknowledgeCdpFrameInBackground(frame:
|
|
3791
|
+
private acknowledgeCdpFrameInBackground(frame: ICdpScreencastFrame): void {
|
|
3473
3792
|
void this.acknowledgeCdpFrame(frame);
|
|
3474
3793
|
}
|
|
3475
3794
|
|
|
3476
3795
|
private handleFrameAcknowledgementFailure(
|
|
3477
|
-
frame:
|
|
3796
|
+
frame: ICdpScreencastFrame,
|
|
3478
3797
|
error: unknown,
|
|
3479
3798
|
): void {
|
|
3480
3799
|
const tab = this.tabs.get(frame.tabId);
|
|
@@ -3514,6 +3833,15 @@ export class LiveBrowserSession {
|
|
|
3514
3833
|
await Promise.all(acknowledgements);
|
|
3515
3834
|
}
|
|
3516
3835
|
|
|
3836
|
+
private async waitForCdpFrameAcknowledgements(
|
|
3837
|
+
cdpSession?: plugins.puppeteer.CDPSession,
|
|
3838
|
+
): Promise<void> {
|
|
3839
|
+
const acknowledgements = [...this.cdpFramesBeingAcknowledged]
|
|
3840
|
+
.filter((frame) => !cdpSession || frame.cdpSession === cdpSession)
|
|
3841
|
+
.map((frame) => frame.acknowledgementPromise!);
|
|
3842
|
+
await Promise.all(acknowledgements);
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3517
3845
|
private handlePossibleCdpDisconnection(
|
|
3518
3846
|
tab: IPrivateLiveBrowserTab,
|
|
3519
3847
|
cdpSession: plugins.puppeteer.CDPSession,
|
|
@@ -3528,7 +3856,7 @@ export class LiveBrowserSession {
|
|
|
3528
3856
|
) {
|
|
3529
3857
|
return;
|
|
3530
3858
|
}
|
|
3531
|
-
tab
|
|
3859
|
+
this.invalidateScreencast(tab, error);
|
|
3532
3860
|
this.retireFramesForTabInBackground(tab.id);
|
|
3533
3861
|
this.scheduleOperation(async () => {
|
|
3534
3862
|
if (!this.tabs.has(tab.id) || tab.cdpSession !== cdpSession) {
|
|
@@ -3972,5 +4300,21 @@ export class LiveBrowserSession {
|
|
|
3972
4300
|
if (options.everyNthFrame !== undefined) {
|
|
3973
4301
|
validateInteger(options.everyNthFrame, 'screencast.everyNthFrame', 1, 100);
|
|
3974
4302
|
}
|
|
4303
|
+
if (options.maxOutstandingFrames !== undefined) {
|
|
4304
|
+
validateInteger(
|
|
4305
|
+
options.maxOutstandingFrames,
|
|
4306
|
+
'screencast.maxOutstandingFrames',
|
|
4307
|
+
1,
|
|
4308
|
+
liveBrowserMaxOutstandingFrames,
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
if (options.firstFrameTimeoutMs !== undefined) {
|
|
4312
|
+
validateInteger(
|
|
4313
|
+
options.firstFrameTimeoutMs,
|
|
4314
|
+
'screencast.firstFrameTimeoutMs',
|
|
4315
|
+
100,
|
|
4316
|
+
maxTimeoutMs,
|
|
4317
|
+
);
|
|
4318
|
+
}
|
|
3975
4319
|
}
|
|
3976
4320
|
}
|