@push.rocks/smartpuppeteer 2.7.0 → 2.8.1
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/assets/livevideo/background.js +17 -0
- package/assets/livevideo/capture.html +3 -0
- package/assets/livevideo/capture.js +193 -0
- package/assets/livevideo/manifest.json +11 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.livevideo.d.ts +33 -0
- package/dist_ts/classes.livevideo.js +175 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.livevideo.d.ts +58 -0
- package/dist_ts/interfaces.livevideo.js +45 -0
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.d.ts +11 -0
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +137 -16
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +5 -0
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.js +1 -1
- package/dist_ts/smartpuppeteer.plugins.d.ts +3 -1
- package/dist_ts/smartpuppeteer.plugins.js +4 -2
- package/package.json +3 -3
- package/readme.md +36 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.livevideo.ts +210 -0
- package/ts/index.ts +1 -0
- package/ts/interfaces.livevideo.ts +103 -0
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +140 -17
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +5 -0
- package/ts/smartpuppeteer.plugins.ts +3 -1
- package/readme.hints.md +0 -20
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getEnvAwareBrowserInstance } from './smartpuppeteer.classes.smartpuppeteer.js';
|
|
2
|
+
import { LiveBrowserVideo } from './classes.livevideo.js';
|
|
3
|
+
import { normalizeLiveVideoOptions, type ILiveVideoDescription, type ILiveVideoOffer,
|
|
4
|
+
type ILiveVideoStatistics } from './interfaces.livevideo.js';
|
|
2
5
|
import {
|
|
3
6
|
liveBrowserDefaultMaxOutstandingFrames,
|
|
4
7
|
liveBrowserMaxOutstandingFrames,
|
|
@@ -504,6 +507,8 @@ export class LiveBrowserSession {
|
|
|
504
507
|
private readonly maxOutstandingFrames: number;
|
|
505
508
|
|
|
506
509
|
private browser?: plugins.puppeteer.Browser;
|
|
510
|
+
private video?: LiveBrowserVideo;
|
|
511
|
+
private frameCaptureEnabled: boolean;
|
|
507
512
|
private browserContext?: plugins.puppeteer.BrowserContext;
|
|
508
513
|
private browserLifetimeController?: AbortController;
|
|
509
514
|
private browserDisconnectedListener?: () => void;
|
|
@@ -549,6 +554,14 @@ export class LiveBrowserSession {
|
|
|
549
554
|
private lastError?: ILiveBrowserError;
|
|
550
555
|
|
|
551
556
|
constructor(optionsArg: ILiveBrowserSessionOptions = {}) {
|
|
557
|
+
validateOptionalBoolean(optionsArg.screencast?.enabled, 'screencast.enabled');
|
|
558
|
+
this.frameCaptureEnabled = optionsArg.screencast?.enabled ?? true;
|
|
559
|
+
if (optionsArg.video) {
|
|
560
|
+
normalizeLiveVideoOptions(optionsArg.video);
|
|
561
|
+
if (!(optionsArg.usePipe ?? optionsArg.launchOptions?.pipe ?? true)) {
|
|
562
|
+
throw new Error('Browser video requires the private CDP pipe transport.');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
552
565
|
if (
|
|
553
566
|
optionsArg.launchOptions?.protocol
|
|
554
567
|
&& optionsArg.launchOptions.protocol !== 'cdp'
|
|
@@ -655,6 +668,7 @@ export class LiveBrowserSession {
|
|
|
655
668
|
viewport: { ...this.viewport },
|
|
656
669
|
tabs: [...this.tabs.values()].map((tab) => this.createTabState(tab)),
|
|
657
670
|
...(this.lastError ? { lastError: { ...this.lastError } } : {}),
|
|
671
|
+
...(this.video ? { videoAcceleration: this.video.getAcceleration() } : {}),
|
|
658
672
|
};
|
|
659
673
|
}
|
|
660
674
|
|
|
@@ -766,6 +780,10 @@ export class LiveBrowserSession {
|
|
|
766
780
|
usePipe: this.options.usePipe,
|
|
767
781
|
launchOptions: {
|
|
768
782
|
...this.options.launchOptions,
|
|
783
|
+
...(this.options.video ? {
|
|
784
|
+
enableExtensions: true,
|
|
785
|
+
args: this.videoLaunchArguments(),
|
|
786
|
+
} : {}),
|
|
769
787
|
protocol: 'cdp',
|
|
770
788
|
signal: browserLifetimeController.signal,
|
|
771
789
|
},
|
|
@@ -808,8 +826,14 @@ export class LiveBrowserSession {
|
|
|
808
826
|
throw signal.reason;
|
|
809
827
|
}
|
|
810
828
|
this.browserContext = this.browser.defaultBrowserContext();
|
|
829
|
+
if (this.options.video) {
|
|
830
|
+
if (this.options.launchOptions?.args?.includes('--disable-extensions')) {
|
|
831
|
+
throw new Error('Browser video requires its private capture extension.');
|
|
832
|
+
}
|
|
833
|
+
this.video = new LiveBrowserVideo(this.browser, this.options.video);
|
|
834
|
+
}
|
|
811
835
|
this.browserTargetCreatedListener = (target) => {
|
|
812
|
-
if (target.type() !== 'page' || target.browserContext() !== this.browserContext) {
|
|
836
|
+
if (this.video?.ownsTarget(target) || target.type() !== 'page' || target.browserContext() !== this.browserContext) {
|
|
813
837
|
return;
|
|
814
838
|
}
|
|
815
839
|
void target.page().then((page) => {
|
|
@@ -824,6 +848,7 @@ export class LiveBrowserSession {
|
|
|
824
848
|
};
|
|
825
849
|
this.browser.on('targetcreated', this.browserTargetCreatedListener);
|
|
826
850
|
await this.configureBrowserSecurity(this.browser);
|
|
851
|
+
await this.video?.start();
|
|
827
852
|
this.browserDisconnectedListener = () => {
|
|
828
853
|
if (this.normalStopRequested || this.status === 'stopped' || this.status === 'stopping') {
|
|
829
854
|
return;
|
|
@@ -844,7 +869,7 @@ export class LiveBrowserSession {
|
|
|
844
869
|
};
|
|
845
870
|
this.browser.on('disconnected', this.browserDisconnectedListener);
|
|
846
871
|
|
|
847
|
-
const initialPages = await this.browserContext.pages();
|
|
872
|
+
const initialPages = (await this.browserContext.pages()).filter(page => !this.video?.ownsTarget(page.target()));
|
|
848
873
|
if (initialPages.length === 0) {
|
|
849
874
|
initialPages.push(await this.browserContext.newPage());
|
|
850
875
|
}
|
|
@@ -969,6 +994,71 @@ export class LiveBrowserSession {
|
|
|
969
994
|
);
|
|
970
995
|
}
|
|
971
996
|
|
|
997
|
+
private videoLaunchArguments(): string[] {
|
|
998
|
+
const args = [...(this.options.launchOptions?.args ?? [])];
|
|
999
|
+
if (this.options.video?.gpu === 'disabled') return [...args, '--disable-gpu'];
|
|
1000
|
+
if (args.includes('--disable-gpu')) throw new Error('Use video.gpu="disabled" to disable video GPU acceleration.');
|
|
1001
|
+
if (!args.includes('--enable-gpu')) args.push('--enable-gpu');
|
|
1002
|
+
if (process.platform === 'linux') {
|
|
1003
|
+
if (!args.some(arg => arg.startsWith('--use-angle='))) args.push('--use-angle=vulkan');
|
|
1004
|
+
if (args.includes('--use-angle=vulkan')) {
|
|
1005
|
+
const features = new Set(args.filter(arg => arg.startsWith('--enable-features='))
|
|
1006
|
+
.flatMap(arg => arg.slice('--enable-features='.length).split(',')));
|
|
1007
|
+
features.add('Vulkan');
|
|
1008
|
+
for (let index = args.length - 1; index >= 0; index--) {
|
|
1009
|
+
if (args[index]!.startsWith('--enable-features=')) args.splice(index, 1);
|
|
1010
|
+
}
|
|
1011
|
+
args.push(`--enable-features=${[...features].join(',')}`);
|
|
1012
|
+
if (!args.includes('--disable-vulkan-surface')) args.push('--disable-vulkan-surface');
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return args;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
public setFrameCaptureEnabled(enabled: boolean, options: ILiveBrowserOperationOptions = {}): Promise<void> {
|
|
1019
|
+
if (typeof enabled !== 'boolean') throw new Error('Frame capture enabled must be a boolean.');
|
|
1020
|
+
return this.enqueuePublicOperation(async signal => {
|
|
1021
|
+
signal.throwIfAborted();
|
|
1022
|
+
if (enabled === this.frameCaptureEnabled) return;
|
|
1023
|
+
const tab = this.requireActiveTab();
|
|
1024
|
+
if (!tab.cdpSession || !tab.streaming) throw new Error('Browser input transport is unavailable.');
|
|
1025
|
+
if (enabled) {
|
|
1026
|
+
await tab.cdpSession.send('Page.startScreencast', this.screencastParameters());
|
|
1027
|
+
} else {
|
|
1028
|
+
await tab.cdpSession.send('Page.stopScreencast');
|
|
1029
|
+
await this.retireOutstandingFrames(frame => frame.tabId === tab.id);
|
|
1030
|
+
}
|
|
1031
|
+
this.frameCaptureEnabled = enabled;
|
|
1032
|
+
}, options);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
public openVideoPeer(peerId: string, options: ILiveBrowserOperationOptions = {}): Promise<ILiveVideoOffer> {
|
|
1036
|
+
return this.enqueuePublicOperation(async signal => {
|
|
1037
|
+
const tab = this.requireActiveTab();
|
|
1038
|
+
if (!this.video) throw new Error('Browser video is not enabled.');
|
|
1039
|
+
return this.video.open(peerId, tab.page, { tabId: tab.id, generation: tab.generation,
|
|
1040
|
+
viewportRevision: this.viewportRevision, viewport: { ...this.viewport } }, signal);
|
|
1041
|
+
}, options);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
public answerVideoPeer(peerId: string, negotiationId: string, description: ILiveVideoDescription,
|
|
1045
|
+
options: ILiveBrowserOperationOptions = {}): Promise<void> {
|
|
1046
|
+
return this.enqueuePublicOperation(async signal => {
|
|
1047
|
+
signal.throwIfAborted();
|
|
1048
|
+
if (!this.video) throw new Error('Browser video is not enabled.');
|
|
1049
|
+
await this.video.answer(peerId, negotiationId, description);
|
|
1050
|
+
}, options);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
public closeVideoPeer(peerId: string): Promise<void> {
|
|
1054
|
+
return this.video?.close(peerId) ?? Promise.resolve();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
public getVideoStatistics(peerId: string): Promise<ILiveVideoStatistics> {
|
|
1058
|
+
if (!this.video) return Promise.reject(new Error('Browser video is not enabled.'));
|
|
1059
|
+
return this.video.getStatistics(peerId);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
972
1062
|
public async updateScreencastOptions(
|
|
973
1063
|
optionsArg: ILiveBrowserScreencastUpdateOptions,
|
|
974
1064
|
operationOptions: ILiveBrowserOperationOptions = {},
|
|
@@ -985,7 +1075,7 @@ export class LiveBrowserSession {
|
|
|
985
1075
|
const activeTab = this.status === 'running' && this.activeTabId
|
|
986
1076
|
? this.tabs.get(this.activeTabId)
|
|
987
1077
|
: undefined;
|
|
988
|
-
if (!activeTab?.streaming) {
|
|
1078
|
+
if (!activeTab?.streaming || !this.frameCaptureEnabled) {
|
|
989
1079
|
return null;
|
|
990
1080
|
}
|
|
991
1081
|
return this.restartActiveScreencast(signal);
|
|
@@ -995,6 +1085,7 @@ export class LiveBrowserSession {
|
|
|
995
1085
|
private async restartActiveScreencast(
|
|
996
1086
|
signal: AbortSignal,
|
|
997
1087
|
): Promise<ILiveBrowserFrameIdentity> {
|
|
1088
|
+
if (!this.frameCaptureEnabled) throw new Error('Frame capture is not enabled.');
|
|
998
1089
|
signal.throwIfAborted();
|
|
999
1090
|
const tab = this.requireActiveTab();
|
|
1000
1091
|
let firstFrame: ReturnType<LiveBrowserSession['waitForScreencastFrame']> | undefined;
|
|
@@ -2274,7 +2365,7 @@ export class LiveBrowserSession {
|
|
|
2274
2365
|
status: tab.status,
|
|
2275
2366
|
generation: tab.generation,
|
|
2276
2367
|
appliedViewportRevision: tab.appliedViewportRevision,
|
|
2277
|
-
streaming: tab.streaming,
|
|
2368
|
+
streaming: tab.streaming && !tab.streamInvalidated,
|
|
2278
2369
|
};
|
|
2279
2370
|
}
|
|
2280
2371
|
|
|
@@ -2540,6 +2631,10 @@ export class LiveBrowserSession {
|
|
|
2540
2631
|
this.browserDisconnectedListener = undefined;
|
|
2541
2632
|
|
|
2542
2633
|
const shutdownErrors: unknown[] = [];
|
|
2634
|
+
if (this.video) {
|
|
2635
|
+
await this.video.stop().catch(error => shutdownErrors.push(error));
|
|
2636
|
+
this.video = undefined;
|
|
2637
|
+
}
|
|
2543
2638
|
const shutdownFrameDrainPromise = this.shutdownFrameDrainPromise;
|
|
2544
2639
|
this.shutdownFrameDrainPromise = undefined;
|
|
2545
2640
|
if (shutdownFrameDrainPromise) {
|
|
@@ -2971,7 +3066,9 @@ export class LiveBrowserSession {
|
|
|
2971
3066
|
session.send('Fetch.continueWithAuth', {
|
|
2972
3067
|
requestId: event.requestId,
|
|
2973
3068
|
authChallengeResponse,
|
|
2974
|
-
}, proxySecurityCommandOptions).then(() => undefined)
|
|
3069
|
+
}, proxySecurityCommandOptions).then(() => undefined).catch(error => (
|
|
3070
|
+
this.reconcileClosedProxyRequest(proxySecuritySession, error)
|
|
3071
|
+
)),
|
|
2975
3072
|
generation,
|
|
2976
3073
|
'proxy_security_protocol_failed',
|
|
2977
3074
|
);
|
|
@@ -3008,7 +3105,9 @@ export class LiveBrowserSession {
|
|
|
3008
3105
|
'Fetch.continueRequest',
|
|
3009
3106
|
{ requestId: event.requestId },
|
|
3010
3107
|
proxySecurityCommandOptions,
|
|
3011
|
-
).then(() => undefined)
|
|
3108
|
+
).then(() => undefined).catch(error => (
|
|
3109
|
+
this.reconcileClosedProxyRequest(proxySecuritySession, error)
|
|
3110
|
+
)),
|
|
3012
3111
|
generation,
|
|
3013
3112
|
'proxy_security_protocol_failed',
|
|
3014
3113
|
);
|
|
@@ -3156,6 +3255,7 @@ export class LiveBrowserSession {
|
|
|
3156
3255
|
private async waitForProxySecurityTargetCoverage(
|
|
3157
3256
|
targetId: string,
|
|
3158
3257
|
generation: number,
|
|
3258
|
+
excludedSession?: plugins.puppeteer.CDPSession,
|
|
3159
3259
|
): Promise<boolean> {
|
|
3160
3260
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3161
3261
|
const generationSessions = [...this.proxySecuritySessions.values()].filter((candidate) => (
|
|
@@ -3165,6 +3265,7 @@ export class LiveBrowserSession {
|
|
|
3165
3265
|
const hasReplacement = [...this.proxySecuritySessions.values()].some((candidate) => (
|
|
3166
3266
|
candidate.generation === generation
|
|
3167
3267
|
&& candidate.targetId === targetId
|
|
3268
|
+
&& candidate.session !== excludedSession
|
|
3168
3269
|
&& candidate.fetchEnabled
|
|
3169
3270
|
&& !candidate.session.detached
|
|
3170
3271
|
));
|
|
@@ -3187,6 +3288,22 @@ export class LiveBrowserSession {
|
|
|
3187
3288
|
return !targetInfos.some((targetInfo) => targetInfo.targetId === targetId);
|
|
3188
3289
|
}
|
|
3189
3290
|
|
|
3291
|
+
private async reconcileClosedProxyRequest(
|
|
3292
|
+
record: IProxySecuritySession,
|
|
3293
|
+
error: unknown,
|
|
3294
|
+
): Promise<void> {
|
|
3295
|
+
if (this.proxySecurityStopping || this.normalStopRequested
|
|
3296
|
+
|| record.generation !== this.proxySecurityGeneration) return;
|
|
3297
|
+
// An in-flight Fetch command can reject after its worker/session has retired.
|
|
3298
|
+
// Retain the original record: detach handling may already have removed it.
|
|
3299
|
+
await record.setupPromise;
|
|
3300
|
+
if (!record.session.detached || !record.targetId) throw error;
|
|
3301
|
+
if (await this.waitForProxySecurityTargetCoverage(
|
|
3302
|
+
record.targetId, record.generation, record.session,
|
|
3303
|
+
)) return;
|
|
3304
|
+
throw error;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3190
3307
|
private removeProxySecuritySession(proxySecuritySession: IProxySecuritySession): void {
|
|
3191
3308
|
if (this.proxySecuritySessions.get(proxySecuritySession.session.id()) !== proxySecuritySession) {
|
|
3192
3309
|
return;
|
|
@@ -4195,17 +4312,11 @@ export class LiveBrowserSession {
|
|
|
4195
4312
|
cdpSessionDetachedListener,
|
|
4196
4313
|
);
|
|
4197
4314
|
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
maxWidth: this.options.screencast?.maxWidth,
|
|
4204
|
-
maxHeight: this.options.screencast?.maxHeight,
|
|
4205
|
-
everyNthFrame: this.options.screencast?.everyNthFrame ?? 1,
|
|
4206
|
-
}),
|
|
4207
|
-
authority,
|
|
4208
|
-
);
|
|
4315
|
+
if (this.frameCaptureEnabled) {
|
|
4316
|
+
await this.waitForScreencastAuthority(
|
|
4317
|
+
cdpSession.send('Page.startScreencast', this.screencastParameters()), authority,
|
|
4318
|
+
);
|
|
4319
|
+
}
|
|
4209
4320
|
this.assertScreencastAuthority(tab, authority);
|
|
4210
4321
|
this.emitState();
|
|
4211
4322
|
} catch (error) {
|
|
@@ -4318,6 +4429,7 @@ export class LiveBrowserSession {
|
|
|
4318
4429
|
}
|
|
4319
4430
|
|
|
4320
4431
|
private invalidateScreencast(tab: IPrivateLiveBrowserTab, reason: unknown): void {
|
|
4432
|
+
const wasStreaming = tab.streaming && !tab.streamInvalidated;
|
|
4321
4433
|
tab.streamInvalidated = true;
|
|
4322
4434
|
tab.streamLifecycleRevision += 1;
|
|
4323
4435
|
const authority = tab.screencastAuthority;
|
|
@@ -4325,6 +4437,9 @@ export class LiveBrowserSession {
|
|
|
4325
4437
|
if (authority && !authority.controller.signal.aborted) {
|
|
4326
4438
|
authority.controller.abort(reason);
|
|
4327
4439
|
}
|
|
4440
|
+
// Input authority is revoked synchronously on renderer navigation. Publish
|
|
4441
|
+
// that same boundary before the queued page/title refresh can yield.
|
|
4442
|
+
if (wasStreaming) this.emitState();
|
|
4328
4443
|
}
|
|
4329
4444
|
|
|
4330
4445
|
private waitForScreencastFrame(
|
|
@@ -4377,7 +4492,15 @@ export class LiveBrowserSession {
|
|
|
4377
4492
|
};
|
|
4378
4493
|
}
|
|
4379
4494
|
|
|
4495
|
+
private screencastParameters(): plugins.puppeteer.Protocol.Page.StartScreencastRequest {
|
|
4496
|
+
return { format: this.options.screencast?.format ?? 'jpeg',
|
|
4497
|
+
quality: this.options.screencast?.quality ?? 80,
|
|
4498
|
+
maxWidth: this.options.screencast?.maxWidth, maxHeight: this.options.screencast?.maxHeight,
|
|
4499
|
+
everyNthFrame: this.options.screencast?.everyNthFrame ?? 1 };
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4380
4502
|
private async stopScreencast(tab: IPrivateLiveBrowserTab): Promise<void> {
|
|
4503
|
+
if (this.video && this.activeTabId === tab.id) await this.video.invalidate();
|
|
4381
4504
|
const cdpSession = tab.cdpSession;
|
|
4382
4505
|
const cdpConnection = tab.cdpConnection;
|
|
4383
4506
|
const frameListener = tab.screencastFrameListener;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IEnvAwareOptions } from './smartpuppeteer.classes.smartpuppeteer.js';
|
|
2
|
+
import type { ILiveVideoOptions, ILiveVideoAcceleration } from './interfaces.livevideo.js';
|
|
2
3
|
|
|
3
4
|
export type TLiveBrowserStatus = 'stopped' | 'starting' | 'running' | 'stopping';
|
|
4
5
|
export type TLiveBrowserImageFormat = 'jpeg' | 'png';
|
|
@@ -19,6 +20,8 @@ export interface ILiveBrowserViewport {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ILiveBrowserScreencastOptions {
|
|
23
|
+
/** JPEG/PNG production can be enabled on demand independently of input. */
|
|
24
|
+
enabled?: boolean;
|
|
22
25
|
format?: TLiveBrowserImageFormat;
|
|
23
26
|
quality?: number;
|
|
24
27
|
maxWidth?: number;
|
|
@@ -79,6 +82,7 @@ export interface ILiveBrowserSessionOptions extends Omit<IEnvAwareOptions, 'laun
|
|
|
79
82
|
launchOptions?: TLiveBrowserLaunchOptions;
|
|
80
83
|
viewport?: ILiveBrowserViewport;
|
|
81
84
|
screencast?: ILiveBrowserScreencastOptions;
|
|
85
|
+
video?: ILiveVideoOptions;
|
|
82
86
|
security?: ILiveBrowserSecurityOptions;
|
|
83
87
|
allowEvaluation?: boolean;
|
|
84
88
|
}
|
|
@@ -129,6 +133,7 @@ export interface ILiveBrowserState {
|
|
|
129
133
|
viewport: ILiveBrowserViewport;
|
|
130
134
|
tabs: ILiveBrowserTabState[];
|
|
131
135
|
lastError?: ILiveBrowserError;
|
|
136
|
+
videoAcceleration?: ILiveVideoAcceleration;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
export interface ILiveBrowserScreencastMetadata {
|
|
@@ -3,8 +3,10 @@ import { Buffer } from 'node:buffer';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as http from 'node:http';
|
|
5
5
|
import * as os from 'node:os';
|
|
6
|
+
import * as crypto from 'node:crypto';
|
|
7
|
+
import * as url from 'node:url';
|
|
6
8
|
|
|
7
|
-
export { Buffer, fs, http, os };
|
|
9
|
+
export { Buffer, fs, http, os, crypto, url };
|
|
8
10
|
|
|
9
11
|
// @pushrocks scope
|
|
10
12
|
import * as smartdelay from '@push.rocks/smartdelay';
|
package/readme.hints.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Implementation Hints
|
|
2
|
-
|
|
3
|
-
- `getEnvAwareBrowserInstance()` is the only Chromium launch path. Merge caller arguments before adding environment-required sandbox arguments, retain the pipe default, and do not run executable discovery when the caller selected a browser, channel, or executable.
|
|
4
|
-
- `LiveBrowserSession` uses the browser's default context so all tabs and popups share one profile. Omitting both `launchOptions.userDataDir` and a `--user-data-dir` argument intentionally relies on Puppeteer's ephemeral profile lifecycle.
|
|
5
|
-
- CDP is private to the live runtime. Public contracts contain transport-neutral values and `Uint8Array` image data, never `CDPSession`, raw CDP frame IDs, or base64 image strings.
|
|
6
|
-
- Every published screencast frame has one private sequence-to-CDP acknowledgement entry. Public acknowledgement requires matching tab ID, sequence, generation, and viewport revision. `screencast.maxOutstandingFrames` bounds those entries independently of any application transport window; overflow retires oldest-first. Drops and all stream invalidation paths must issue each CDP acknowledgement at most once and await in-flight acknowledgements before detaching the CDP session.
|
|
7
|
-
- `refreshScreencast()` installs its exact-generation waiter before starting CDP, retires the previous stream, and resolves only after the first validated frame is published. Pre-aborted and queued calls reject without changing the stream. Active caller cancellation settles only after restorative restart; shutdown, tab replacement, crash, or closure revokes restart authority and must never resurrect the stream.
|
|
8
|
-
- A refresh timeout or restart failure on a tab that is still restorable is not runtime-fatal. Revoke the restart authority, wait for the in-flight `startScreencast()` to unwind, stop whatever it established, and emit non-fatal `screencast_refresh_failed` with the tab left open, non-streaming, and invalidated. `refreshScreencast()` therefore also accepts a non-streaming active tab; `updateScreencastOptions()` shares that restart core after storing validated options. Browser loss stays fatal and is normally reported by the disconnect listener first.
|
|
9
|
-
- Raw input is dispatched through a per-tab queue. Chromium does not keep a pipelined mouse or key command behind an earlier `mouseWheel` command, so anything queued behind wheel input waits until that wheel CDP call settles. Wheel input arriving while a wheel dispatch is in flight merges into the trailing wheel entry for the same CDP session, generation, and viewport revision; a non-wheel entry ends that batch.
|
|
10
|
-
- Activation, viewport changes, navigation, tab closure, snapshots, observations, semantic actions, and shutdown share one bounded operation scheduler. Raw input and frame acknowledgement remain direct, but must validate active tab, generation, and viewport revision. Repeated internal navigation/load state updates are coalesced per tab, and shutdown cancels queued work.
|
|
11
|
-
- Retain the scheduler-owned launch `AbortController` for the full browser lifetime. Shutdown aborts both the active operation and Chromium itself so a non-signal-aware Puppeteer command or disabled protocol timeout cannot retain the browser ahead of queued cleanup.
|
|
12
|
-
- Viewport revision starts at 1 and advances only after `Page.setViewport()` succeeds. Stop and flush the active screencast before applying a viewport or navigation mutation, then restart it with a new generation.
|
|
13
|
-
- Track the applied viewport revision on every tab. Before an inactive tab is captured, observed, activated, or acted upon, apply the current global viewport and report metadata from that exact viewport. Full-page capture is outside the initial live-runtime scope.
|
|
14
|
-
- Canonicalize the configured viewport once. `viewport` wins over `launchOptions.defaultViewport`, `null` uses 800x600, and every Puppeteer viewport explicitly disables mobile, landscape, and touch emulation. The operation scheduler owns `LaunchOptions.signal`; never accept a caller signal for `LiveBrowserSession`.
|
|
15
|
-
- New-page registration and navigation are transactional: validate before creating a page where possible and restore page, listener, tab-map, active-tab, viewport, and screencast ownership on failure. Keep listeners attached during `page.close()` so a failed close remains observed and recoverable.
|
|
16
|
-
- Browser disconnect is runtime-fatal. Page crash or current CDP-session loss is tab-scoped: activate another usable tab, trying all candidates, or stop cleanly when none remains.
|
|
17
|
-
- Every popup is either registered, closed, or escalated to browser-wide shutdown. Queue saturation and startup-time popup events must never leave an untracked live page.
|
|
18
|
-
- Authenticated proxy coverage owns a filtered browser-target auto-attach scope for service workers. Configure Fetch and Network before resuming each owned worker, use the hidden page-target lifecycle observer to release stopped or request-drained redundant workers, bound every security command and detach wait, and keep Puppeteer's target sessions independent.
|
|
19
|
-
- A navigation metadata refresh failure must not consume screencast restoration ownership. Restore the active stream first, preserve newer navigation revisions, and report the metadata failure separately.
|
|
20
|
-
- Verify Puppeteer behavior against the installed Puppeteer 25 declarations and implementation. Do not add a direct `devtools-protocol` dependency; Puppeteer's public protocol typing is sufficient for private CDP calls.
|