Update dashboard, kb, memory +4 more (+28 ~18 -1)

This commit is contained in:
Echo
2026-02-06 14:25:10 +00:00
parent 7f64d5054a
commit 19d178268a
6767 changed files with 1346472 additions and 1282 deletions

551
node_modules/puppeteer-core/src/node/BrowserLauncher.ts generated vendored Normal file
View File

@@ -0,0 +1,551 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {existsSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {
Browser as InstalledBrowser,
CDP_WEBSOCKET_ENDPOINT_REGEX,
launch,
TimeoutError as BrowsersTimeoutError,
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
computeExecutablePath,
} from '@puppeteer/browsers';
import {
firstValueFrom,
from,
map,
race,
timer,
} from '../../third_party/rxjs/rxjs.js';
import type {Browser, BrowserCloseCallback} from '../api/Browser.js';
import {CdpBrowser} from '../cdp/Browser.js';
import {Connection} from '../cdp/Connection.js';
import {TimeoutError} from '../common/Errors.js';
import type {SupportedBrowser} from '../common/SupportedBrowser.js';
import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {
createIncrementalIdGenerator,
type GetIdFn,
} from '../util/incremental-id-generator.js';
import type {ChromeReleaseChannel, LaunchOptions} from './LaunchOptions.js';
import {NodeWebSocketTransport as WebSocketTransport} from './NodeWebSocketTransport.js';
import {PipeTransport} from './PipeTransport.js';
import type {PuppeteerNode} from './PuppeteerNode.js';
/**
* @internal
*/
export interface ResolvedLaunchArgs {
isTempUserDataDir: boolean;
userDataDir: string;
executablePath: string;
args: string[];
}
/**
* Describes a launcher - a class that is able to create and launch a browser instance.
*
* @public
*/
export abstract class BrowserLauncher {
#browser: SupportedBrowser;
/**
* @internal
*/
puppeteer: PuppeteerNode;
/**
* @internal
*/
constructor(puppeteer: PuppeteerNode, browser: SupportedBrowser) {
this.puppeteer = puppeteer;
this.#browser = browser;
}
get browser(): SupportedBrowser {
return this.#browser;
}
async launch(options: LaunchOptions = {}): Promise<Browser> {
const {
dumpio = false,
enableExtensions = false,
env = process.env,
handleSIGINT = true,
handleSIGTERM = true,
handleSIGHUP = true,
acceptInsecureCerts = false,
networkEnabled = true,
defaultViewport = DEFAULT_VIEWPORT,
downloadBehavior,
slowMo = 0,
timeout = 30000,
waitForInitialPage = true,
protocolTimeout,
handleDevToolsAsPage,
idGenerator = createIncrementalIdGenerator(),
} = options;
let {protocol} = options;
// Default to 'webDriverBiDi' for Firefox.
if (this.#browser === 'firefox' && protocol === undefined) {
protocol = 'webDriverBiDi';
}
if (this.#browser === 'firefox' && protocol === 'cdp') {
throw new Error('Connecting to Firefox using CDP is no longer supported');
}
const launchArgs = await this.computeLaunchArguments({
...options,
protocol,
});
if (!existsSync(launchArgs.executablePath)) {
throw new Error(
`Browser was not found at the configured executablePath (${launchArgs.executablePath})`,
);
}
const usePipe = launchArgs.args.includes('--remote-debugging-pipe');
const onProcessExit = async () => {
await this.cleanUserDataDir(launchArgs.userDataDir, {
isTemp: launchArgs.isTempUserDataDir,
});
};
if (
this.#browser === 'firefox' &&
protocol === 'webDriverBiDi' &&
usePipe
) {
throw new Error(
'Pipe connections are not supported with Firefox and WebDriver BiDi',
);
}
const browserProcess = launch({
executablePath: launchArgs.executablePath,
args: launchArgs.args,
handleSIGHUP,
handleSIGTERM,
handleSIGINT,
dumpio,
env,
pipe: usePipe,
onExit: onProcessExit,
signal: options.signal,
});
let browser: Browser;
let cdpConnection: Connection;
let closing = false;
const browserCloseCallback: BrowserCloseCallback = async () => {
if (closing) {
return;
}
closing = true;
await this.closeBrowser(browserProcess, cdpConnection);
};
try {
if (this.#browser === 'firefox') {
browser = await this.createBiDiBrowser(
browserProcess,
browserCloseCallback,
{
timeout,
protocolTimeout,
slowMo,
defaultViewport,
acceptInsecureCerts,
networkEnabled,
idGenerator,
},
);
} else {
if (usePipe) {
cdpConnection = await this.createCdpPipeConnection(browserProcess, {
timeout,
protocolTimeout,
slowMo,
idGenerator,
});
} else {
cdpConnection = await this.createCdpSocketConnection(browserProcess, {
timeout,
protocolTimeout,
slowMo,
idGenerator,
});
}
if (protocol === 'webDriverBiDi') {
browser = await this.createBiDiOverCdpBrowser(
browserProcess,
cdpConnection,
browserCloseCallback,
{
defaultViewport,
acceptInsecureCerts,
networkEnabled,
},
);
} else {
browser = await CdpBrowser._create(
cdpConnection,
[],
acceptInsecureCerts,
defaultViewport,
downloadBehavior,
browserProcess.nodeProcess,
browserCloseCallback,
options.targetFilter,
undefined,
undefined,
networkEnabled,
handleDevToolsAsPage,
);
}
}
} catch (error) {
void browserCloseCallback();
const logs = browserProcess.getRecentLogs().join('\n');
if (
logs.includes(
'Failed to create a ProcessSingleton for your profile directory',
) ||
// On Windows we will not get logs due to the singleton process
// handover. See
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/process_singleton_win.cc;l=46;drc=fc7952f0422b5073515a205a04ec9c3a1ae81658
(process.platform === 'win32' &&
existsSync(join(launchArgs.userDataDir, 'lockfile')))
) {
throw new Error(
`The browser is already running for ${launchArgs.userDataDir}. Use a different \`userDataDir\` or stop the running browser first.`,
);
}
if (logs.includes('Missing X server') && options.headless === false) {
throw new Error(
`Missing X server to start the headful browser. Either set headless to true or use xvfb-run to run your Puppeteer script.`,
);
}
if (error instanceof BrowsersTimeoutError) {
throw new TimeoutError(error.message);
}
throw error;
}
if (Array.isArray(enableExtensions)) {
if (this.#browser === 'chrome' && !usePipe) {
throw new Error(
'To use `enableExtensions` with a list of paths in Chrome, you must be connected with `--remote-debugging-pipe` (`pipe: true`).',
);
}
await Promise.all([
enableExtensions.map(path => {
return browser.installExtension(path);
}),
]);
}
if (waitForInitialPage) {
await this.waitForPageTarget(browser, timeout);
}
return browser;
}
abstract executablePath(
channel?: ChromeReleaseChannel,
validatePath?: boolean,
): string;
abstract defaultArgs(object: LaunchOptions): string[];
/**
* @internal
*/
protected abstract computeLaunchArguments(
options: LaunchOptions,
): Promise<ResolvedLaunchArgs>;
/**
* @internal
*/
protected abstract cleanUserDataDir(
path: string,
opts: {isTemp: boolean},
): Promise<void>;
/**
* @internal
*/
protected async closeBrowser(
browserProcess: ReturnType<typeof launch>,
cdpConnection?: Connection,
): Promise<void> {
if (cdpConnection) {
// Attempt to close the browser gracefully
try {
await cdpConnection.closeBrowser();
await browserProcess.hasClosed();
} catch (error) {
debugError(error);
await browserProcess.close();
}
} else {
// Wait for a possible graceful shutdown.
await firstValueFrom(
race(
from(browserProcess.hasClosed()),
timer(5000).pipe(
map(() => {
return from(browserProcess.close());
}),
),
),
);
}
}
/**
* @internal
*/
protected async waitForPageTarget(
browser: Browser,
timeout: number,
): Promise<void> {
try {
await browser.waitForTarget(
t => {
return t.type() === 'page';
},
{timeout},
);
} catch (error) {
await browser.close();
throw error;
}
}
/**
* @internal
*/
protected async createCdpSocketConnection(
browserProcess: ReturnType<typeof launch>,
opts: {
timeout: number;
protocolTimeout: number | undefined;
slowMo: number;
idGenerator: GetIdFn;
},
): Promise<Connection> {
const browserWSEndpoint = await browserProcess.waitForLineOutput(
CDP_WEBSOCKET_ENDPOINT_REGEX,
opts.timeout,
);
const transport = await WebSocketTransport.create(browserWSEndpoint);
return new Connection(
browserWSEndpoint,
transport,
opts.slowMo,
opts.protocolTimeout,
/* rawErrors */ false,
opts.idGenerator,
);
}
/**
* @internal
*/
protected async createCdpPipeConnection(
browserProcess: ReturnType<typeof launch>,
opts: {
timeout: number;
protocolTimeout: number | undefined;
slowMo: number;
idGenerator: GetIdFn;
},
): Promise<Connection> {
// stdio was assigned during start(), and the 'pipe' option there adds the
// 4th and 5th items to stdio array
const {3: pipeWrite, 4: pipeRead} = browserProcess.nodeProcess.stdio;
const transport = new PipeTransport(
pipeWrite as NodeJS.WritableStream,
pipeRead as NodeJS.ReadableStream,
);
return new Connection(
'',
transport,
opts.slowMo,
opts.protocolTimeout,
/* rawErrors */ false,
opts.idGenerator,
);
}
/**
* @internal
*/
protected async createBiDiOverCdpBrowser(
browserProcess: ReturnType<typeof launch>,
cdpConnection: Connection,
closeCallback: BrowserCloseCallback,
opts: {
defaultViewport: Viewport | null;
acceptInsecureCerts?: boolean;
networkEnabled: boolean;
},
): Promise<Browser> {
const bidiOnly = process.env['PUPPETEER_WEBDRIVER_BIDI_ONLY'] === 'true';
const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
const bidiConnection = await BiDi.connectBidiOverCdp(cdpConnection);
return await BiDi.BidiBrowser.create({
connection: bidiConnection,
// Do not provide CDP connection to Browser, if BiDi-only mode is enabled. This
// would restrict Browser to use only BiDi endpoint.
cdpConnection: bidiOnly ? undefined : cdpConnection,
closeCallback,
process: browserProcess.nodeProcess,
defaultViewport: opts.defaultViewport,
acceptInsecureCerts: opts.acceptInsecureCerts,
networkEnabled: opts.networkEnabled,
});
}
/**
* @internal
*/
protected async createBiDiBrowser(
browserProcess: ReturnType<typeof launch>,
closeCallback: BrowserCloseCallback,
opts: {
timeout: number;
protocolTimeout: number | undefined;
slowMo: number;
idGenerator: GetIdFn;
defaultViewport: Viewport | null;
acceptInsecureCerts?: boolean;
networkEnabled?: boolean;
},
): Promise<Browser> {
const browserWSEndpoint =
(await browserProcess.waitForLineOutput(
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
opts.timeout,
)) + '/session';
const transport = await WebSocketTransport.create(browserWSEndpoint);
const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
const bidiConnection = new BiDi.BidiConnection(
browserWSEndpoint,
transport,
opts.idGenerator,
opts.slowMo,
opts.protocolTimeout,
);
return await BiDi.BidiBrowser.create({
connection: bidiConnection,
closeCallback,
process: browserProcess.nodeProcess,
defaultViewport: opts.defaultViewport,
acceptInsecureCerts: opts.acceptInsecureCerts,
networkEnabled: opts.networkEnabled ?? true,
});
}
/**
* @internal
*/
protected getProfilePath(): string {
return join(
this.puppeteer.configuration.temporaryDirectory ?? tmpdir(),
`puppeteer_dev_${this.browser}_profile-`,
);
}
/**
* @internal
*/
resolveExecutablePath(
headless?: boolean | 'shell',
validatePath = true,
): string {
let executablePath = this.puppeteer.configuration.executablePath;
if (executablePath) {
if (validatePath && !existsSync(executablePath)) {
throw new Error(
`Tried to find the browser at the configured path (${executablePath}), but no executable was found.`,
);
}
return executablePath;
}
function puppeteerBrowserToInstalledBrowser(
browser?: SupportedBrowser,
headless?: boolean | 'shell',
) {
switch (browser) {
case 'chrome':
if (headless === 'shell') {
return InstalledBrowser.CHROMEHEADLESSSHELL;
}
return InstalledBrowser.CHROME;
case 'firefox':
return InstalledBrowser.FIREFOX;
}
return InstalledBrowser.CHROME;
}
const browserType = puppeteerBrowserToInstalledBrowser(
this.browser,
headless,
);
executablePath = computeExecutablePath({
cacheDir: this.puppeteer.defaultDownloadPath!,
browser: browserType,
buildId: this.puppeteer.browserVersion,
});
if (validatePath && !existsSync(executablePath)) {
const configVersion =
this.puppeteer.configuration?.[this.browser]?.version;
if (configVersion) {
throw new Error(
`Tried to find the browser at the configured path (${executablePath}) for version ${configVersion}, but no executable was found.`,
);
}
switch (this.browser) {
case 'chrome':
throw new Error(
`Could not find Chrome (ver. ${this.puppeteer.browserVersion}). This can occur if either\n` +
` 1. you did not perform an installation before running the script (e.g. \`npx puppeteer browsers install ${browserType}\`) or\n` +
` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.',
);
case 'firefox':
throw new Error(
`Could not find Firefox (rev. ${this.puppeteer.browserVersion}). This can occur if either\n` +
' 1. you did not perform an installation for Firefox before running the script (e.g. `npx puppeteer browsers install firefox`) or\n' +
` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` +
'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.',
);
}
}
return executablePath;
}
}

335
node_modules/puppeteer-core/src/node/ChromeLauncher.ts generated vendored Normal file
View File

@@ -0,0 +1,335 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {mkdtemp} from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import {
computeSystemExecutablePath,
Browser as SupportedBrowsers,
} from '@puppeteer/browsers';
import type {Browser} from '../api/Browser.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {BrowserLauncher, type ResolvedLaunchArgs} from './BrowserLauncher.js';
import {
convertPuppeteerChannelToBrowsersChannel,
type ChromeReleaseChannel,
type LaunchOptions,
} from './LaunchOptions.js';
import type {PuppeteerNode} from './PuppeteerNode.js';
import {rm} from './util/fs.js';
/**
* @internal
*/
export class ChromeLauncher extends BrowserLauncher {
constructor(puppeteer: PuppeteerNode) {
super(puppeteer, 'chrome');
}
override launch(options: LaunchOptions = {}): Promise<Browser> {
if (
this.puppeteer.configuration.logLevel === 'warn' &&
process.platform === 'darwin' &&
process.arch === 'x64'
) {
const cpus = os.cpus();
if (cpus[0]?.model.includes('Apple')) {
console.warn(
[
'\x1B[1m\x1B[43m\x1B[30m',
'Degraded performance warning:\x1B[0m\x1B[33m',
'Launching Chrome on Mac Silicon (arm64) from an x64 Node installation results in',
'Rosetta translating the Chrome binary, even if Chrome is already arm64. This would',
'result in huge performance issues. To resolve this, you must run Puppeteer with',
'a version of Node built for arm64.',
].join('\n '),
);
}
}
return super.launch(options);
}
/**
* @internal
*/
override async computeLaunchArguments(
options: LaunchOptions = {},
): Promise<ResolvedLaunchArgs> {
const {
ignoreDefaultArgs = false,
args = [],
pipe = false,
debuggingPort,
channel,
executablePath,
} = options;
const chromeArguments = [];
if (!ignoreDefaultArgs) {
chromeArguments.push(...this.defaultArgs(options));
} else if (Array.isArray(ignoreDefaultArgs)) {
chromeArguments.push(
...this.defaultArgs(options).filter(arg => {
return !ignoreDefaultArgs.includes(arg);
}),
);
} else {
chromeArguments.push(...args);
}
if (
!chromeArguments.some(argument => {
return argument.startsWith('--remote-debugging-');
})
) {
if (pipe) {
assert(
!debuggingPort,
'Browser should be launched with either pipe or debugging port - not both.',
);
chromeArguments.push('--remote-debugging-pipe');
} else {
chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}
}
let isTempUserDataDir = false;
// Check for the user data dir argument, which will always be set even
// with a custom directory specified via the userDataDir option.
let userDataDirIndex = chromeArguments.findIndex(arg => {
return arg.startsWith('--user-data-dir');
});
if (userDataDirIndex < 0) {
isTempUserDataDir = true;
chromeArguments.push(
`--user-data-dir=${await mkdtemp(this.getProfilePath())}`,
);
userDataDirIndex = chromeArguments.length - 1;
}
const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1];
assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed');
let chromeExecutable = executablePath;
if (!chromeExecutable) {
assert(
channel || !this.puppeteer._isPuppeteerCore,
`An \`executablePath\` or \`channel\` must be specified for \`puppeteer-core\``,
);
chromeExecutable = channel
? this.executablePath(channel)
: this.resolveExecutablePath(options.headless ?? true);
}
return {
executablePath: chromeExecutable,
args: chromeArguments,
isTempUserDataDir,
userDataDir,
};
}
/**
* @internal
*/
override async cleanUserDataDir(
path: string,
opts: {isTemp: boolean},
): Promise<void> {
if (opts.isTemp) {
try {
await rm(path);
} catch (error) {
debugError(error);
throw error;
}
}
}
override defaultArgs(options: LaunchOptions = {}): string[] {
// See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
const userDisabledFeatures = getFeatures(
'--disable-features',
options.args,
);
if (options.args && userDisabledFeatures.length > 0) {
removeMatchingFlags(options.args, '--disable-features');
}
const turnOnExperimentalFeaturesForTesting =
process.env['PUPPETEER_TEST_EXPERIMENTAL_CHROME_FEATURES'] === 'true';
// Merge default disabled features with user-provided ones, if any.
const disabledFeatures = [
'Translate',
// AcceptCHFrame disabled because of crbug.com/1348106.
'AcceptCHFrame',
'MediaRouter',
'OptimizationHints',
'RenderDocument', // https://crbug.com/444150315
'IPH_ReadingModePageActionLabel', // b/479237585
'ReadAnythingOmniboxChip', // b/479237585
...(turnOnExperimentalFeaturesForTesting
? []
: [
// https://crbug.com/1492053
'ProcessPerSiteUpToMainFrameThreshold',
// https://github.com/puppeteer/puppeteer/issues/10715
'IsolateSandboxedIframes',
]),
...userDisabledFeatures,
].filter(feature => {
return feature !== '';
});
const userEnabledFeatures = getFeatures('--enable-features', options.args);
if (options.args && userEnabledFeatures.length > 0) {
removeMatchingFlags(options.args, '--enable-features');
}
// Merge default enabled features with user-provided ones, if any.
const enabledFeatures = [
'PdfOopif',
// Add features to enable by default here.
...userEnabledFeatures,
].filter(feature => {
return feature !== '';
});
const chromeArguments = [
'--allow-pre-commit-input',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-crash-reporter', // No crash reporting in CfT.
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-hang-monitor',
'--disable-infobars',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-search-engine-choice-screen',
'--disable-sync',
'--enable-automation',
'--export-tagged-pdf',
'--force-color-profile=srgb',
'--generate-pdf-document-outline',
'--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--use-mock-keychain',
`--disable-features=${disabledFeatures.join(',')}`,
`--enable-features=${enabledFeatures.join(',')}`,
].filter(arg => {
return arg !== '';
});
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir,
enableExtensions = false,
} = options;
if (userDataDir) {
chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
}
if (devtools) {
chromeArguments.push('--auto-open-devtools-for-tabs');
}
if (headless) {
chromeArguments.push(
headless === 'shell' ? '--headless' : '--headless=new',
'--hide-scrollbars',
'--mute-audio',
);
}
chromeArguments.push(
enableExtensions
? '--enable-unsafe-extension-debugging'
: '--disable-extensions',
);
if (
args.every(arg => {
return arg.startsWith('-');
})
) {
chromeArguments.push('about:blank');
}
chromeArguments.push(...args);
return chromeArguments;
}
override executablePath(
channel?: ChromeReleaseChannel,
validatePath = true,
): string {
if (channel) {
return computeSystemExecutablePath({
browser: SupportedBrowsers.CHROME,
channel: convertPuppeteerChannelToBrowsersChannel(channel),
});
} else {
return this.resolveExecutablePath(undefined, validatePath);
}
}
}
/**
* Extracts all features from the given command-line flag
* (e.g. `--enable-features`, `--enable-features=`).
*
* Example input:
* ["--enable-features=NetworkService,NetworkServiceInProcess", "--enable-features=Foo"]
*
* Example output:
* ["NetworkService", "NetworkServiceInProcess", "Foo"]
*
* @internal
*/
export function getFeatures(flag: string, options: string[] = []): string[] {
return options
.filter(s => {
return s.startsWith(flag.endsWith('=') ? flag : `${flag}=`);
})
.map(s => {
return s.split(new RegExp(`${flag}=\\s*`))[1]?.trim();
})
.filter(s => {
return s;
}) as string[];
}
/**
* Removes all elements in-place from the given string array
* that match the given command-line flag.
*
* @internal
*/
export function removeMatchingFlags(array: string[], flag: string): string[] {
const regex = new RegExp(`^${flag}=.*`);
let i = 0;
while (i < array.length) {
if (regex.test(array[i]!)) {
array.splice(i, 1);
} else {
i++;
}
}
return array;
}

217
node_modules/puppeteer-core/src/node/FirefoxLauncher.ts generated vendored Normal file
View File

@@ -0,0 +1,217 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import {rename, unlink, mkdtemp} from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import {Browser as SupportedBrowsers, createProfile} from '@puppeteer/browsers';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {BrowserLauncher, type ResolvedLaunchArgs} from './BrowserLauncher.js';
import type {LaunchOptions} from './LaunchOptions.js';
import type {PuppeteerNode} from './PuppeteerNode.js';
import {rm} from './util/fs.js';
/**
* @internal
*/
export class FirefoxLauncher extends BrowserLauncher {
constructor(puppeteer: PuppeteerNode) {
super(puppeteer, 'firefox');
}
static getPreferences(
extraPrefsFirefox?: Record<string, unknown>,
): Record<string, unknown> {
return {
...extraPrefsFirefox,
// Force all web content to use a single content process. TODO: remove
// this once Firefox supports mouse event dispatch from the main frame
// context. See https://bugzilla.mozilla.org/show_bug.cgi?id=1773393.
'fission.webContentIsolationStrategy': 0,
};
}
/**
* @internal
*/
override async computeLaunchArguments(
options: LaunchOptions = {},
): Promise<ResolvedLaunchArgs> {
const {
ignoreDefaultArgs = false,
args = [],
executablePath,
pipe = false,
extraPrefsFirefox = {},
debuggingPort = null,
} = options;
const firefoxArguments = [];
if (!ignoreDefaultArgs) {
firefoxArguments.push(...this.defaultArgs(options));
} else if (Array.isArray(ignoreDefaultArgs)) {
firefoxArguments.push(
...this.defaultArgs(options).filter(arg => {
return !ignoreDefaultArgs.includes(arg);
}),
);
} else {
firefoxArguments.push(...args);
}
if (
!firefoxArguments.some(argument => {
return argument.startsWith('--remote-debugging-');
})
) {
if (pipe) {
assert(
debuggingPort === null,
'Browser should be launched with either pipe or debugging port - not both.',
);
}
firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}
let userDataDir: string | undefined;
let isTempUserDataDir = true;
// Check for the profile argument, which will always be set even
// with a custom directory specified via the userDataDir option.
const profileArgIndex = firefoxArguments.findIndex(arg => {
return ['-profile', '--profile'].includes(arg);
});
if (profileArgIndex !== -1) {
userDataDir = firefoxArguments[profileArgIndex + 1];
if (!userDataDir) {
throw new Error(`Missing value for profile command line argument`);
}
// When using a custom Firefox profile it needs to be populated
// with required preferences.
isTempUserDataDir = false;
} else {
userDataDir = await mkdtemp(this.getProfilePath());
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
}
await createProfile(SupportedBrowsers.FIREFOX, {
path: userDataDir,
preferences: FirefoxLauncher.getPreferences(extraPrefsFirefox),
});
let firefoxExecutable: string;
if (this.puppeteer._isPuppeteerCore || executablePath) {
assert(
executablePath,
`An \`executablePath\` must be specified for \`puppeteer-core\``,
);
firefoxExecutable = executablePath;
} else {
firefoxExecutable = this.executablePath(undefined);
}
return {
isTempUserDataDir,
userDataDir,
args: firefoxArguments,
executablePath: firefoxExecutable,
};
}
/**
* @internal
*/
override async cleanUserDataDir(
userDataDir: string,
opts: {isTemp: boolean},
): Promise<void> {
if (opts.isTemp) {
try {
await rm(userDataDir);
} catch (error) {
debugError(error);
throw error;
}
} else {
try {
const backupSuffix = '.puppeteer';
const backupFiles = ['prefs.js', 'user.js'];
const results = await Promise.allSettled(
backupFiles.map(async file => {
const prefsBackupPath = path.join(userDataDir, file + backupSuffix);
if (fs.existsSync(prefsBackupPath)) {
const prefsPath = path.join(userDataDir, file);
await unlink(prefsPath);
await rename(prefsBackupPath, prefsPath);
}
}),
);
for (const result of results) {
if (result.status === 'rejected') {
throw result.reason;
}
}
} catch (error) {
debugError(error);
}
}
}
override executablePath(_: unknown, validatePath = true): string {
return this.resolveExecutablePath(
undefined,
/* validatePath=*/ validatePath,
);
}
override defaultArgs(options: LaunchOptions = {}): string[] {
const {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null,
} = options;
const firefoxArguments = [];
switch (os.platform()) {
case 'darwin':
firefoxArguments.push('--foreground');
break;
case 'win32':
firefoxArguments.push('--wait-for-browser');
break;
}
if (userDataDir) {
firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir);
}
if (headless) {
firefoxArguments.push('--headless');
}
if (devtools) {
firefoxArguments.push('--devtools');
}
if (
args.every(arg => {
return arg.startsWith('-');
})
) {
firefoxArguments.push('about:blank');
}
firefoxArguments.push(...args);
return firefoxArguments;
}
}

162
node_modules/puppeteer-core/src/node/LaunchOptions.ts generated vendored Normal file
View File

@@ -0,0 +1,162 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {ChromeReleaseChannel as BrowsersChromeReleaseChannel} from '@puppeteer/browsers';
import type {
ChromeReleaseChannel,
ConnectOptions,
} from '../common/ConnectOptions.js';
import type {SupportedBrowser} from '../common/SupportedBrowser.js';
export type {ChromeReleaseChannel};
/**
* @internal
*/
export function convertPuppeteerChannelToBrowsersChannel(
channel: ChromeReleaseChannel,
): BrowsersChromeReleaseChannel {
switch (channel) {
case 'chrome':
return BrowsersChromeReleaseChannel.STABLE;
case 'chrome-dev':
return BrowsersChromeReleaseChannel.DEV;
case 'chrome-beta':
return BrowsersChromeReleaseChannel.BETA;
case 'chrome-canary':
return BrowsersChromeReleaseChannel.CANARY;
}
}
/**
* Generic launch options that can be passed when launching any browser.
* @public
*/
export interface LaunchOptions extends ConnectOptions {
/**
* If specified for Chrome, looks for a regular Chrome installation at a known
* system location instead of using the bundled Chrome binary.
*/
channel?: ChromeReleaseChannel;
/**
* Path to a browser executable to use instead of the bundled browser. Note
* that Puppeteer is only guaranteed to work with the bundled browser, so use
* this setting at your own risk.
*
* @remarks
* When using this is recommended to set the `browser` property as well
* as Puppeteer will default to `chrome` by default.
*/
executablePath?: string;
/**
* If `true`, do not use `puppeteer.defaultArgs()` when creating a browser. If
* an array is provided, these args will be filtered out. Use this with care -
* you probably want the default arguments Puppeteer uses.
* @defaultValue `false`
*/
ignoreDefaultArgs?: boolean | string[];
/**
* If `true`, avoids passing default arguments to the browser that would
* prevent extensions from being enabled. Passing a list of strings will
* load the provided paths as unpacked extensions.
*/
enableExtensions?: boolean | string[];
/**
* Close the browser process on `Ctrl+C`.
* @defaultValue `true`
*/
handleSIGINT?: boolean;
/**
* Close the browser process on `SIGTERM`.
* @defaultValue `true`
*/
handleSIGTERM?: boolean;
/**
* Close the browser process on `SIGHUP`.
* @defaultValue `true`
*/
handleSIGHUP?: boolean;
/**
* Maximum time in milliseconds to wait for the browser to start.
* Pass `0` to disable the timeout.
* @defaultValue `30_000` (30 seconds).
*/
timeout?: number;
/**
* If true, pipes the browser process stdout and stderr to `process.stdout`
* and `process.stderr`.
* @defaultValue `false`
*/
dumpio?: boolean;
/**
* Specify environment variables that will be visible to the browser.
* @defaultValue The contents of `process.env`.
*/
env?: Record<string, string | undefined>;
/**
* Connect to a browser over a pipe instead of a WebSocket. Only supported
* with Chrome.
*
* @defaultValue `false`
*/
pipe?: boolean;
/**
* Which browser to launch.
* @defaultValue `chrome`
*/
browser?: SupportedBrowser;
/**
* {@link https://searchfox.org/mozilla-release/source/modules/libpref/init/all.js | Additional preferences } that can be passed when launching with Firefox.
*/
extraPrefsFirefox?: Record<string, unknown>;
/**
* Whether to wait for the initial page to be ready.
* Useful when a user explicitly disables that (e.g. `--no-startup-window` for Chrome).
* @defaultValue `true`
*/
waitForInitialPage?: boolean;
/**
* Whether to run the browser in headless mode.
*
* @remarks
*
* - `true` launches the browser in the
* {@link https://developer.chrome.com/articles/new-headless/ | new headless}
* mode.
*
* - `'shell'` launches
* {@link https://developer.chrome.com/blog/chrome-headless-shell | shell}
* known as the old headless mode.
*
* @defaultValue `true`
*/
headless?: boolean | 'shell';
/**
* Path to a user data directory.
* {@link https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/user_data_dir.md | see the Chromium docs}
* for more info.
*/
userDataDir?: string;
/**
* Whether to auto-open a DevTools panel for each tab. If this is set to
* `true`, then `headless` will be forced to `false`.
* @defaultValue `false`
*/
devtools?: boolean;
/**
* Specify the debugging port number to use
*/
debuggingPort?: number;
/**
* Additional command line arguments to pass to the browser instance.
*/
args?: string[];
/**
* If provided, the browser will be closed when the signal is aborted.
*/
signal?: AbortSignal;
}

View File

@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2018 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import NodeWebSocket from 'ws';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {packageVersion} from '../util/version.js';
/**
* @internal
*/
export class NodeWebSocketTransport implements ConnectionTransport {
static create(
url: string,
headers?: Record<string, string>,
): Promise<NodeWebSocketTransport> {
return new Promise((resolve, reject) => {
const ws = new NodeWebSocket(url, [], {
followRedirects: true,
perMessageDeflate: false,
allowSynchronousEvents: false,
maxPayload: 256 * 1024 * 1024, // 256Mb
headers: {
'User-Agent': `Puppeteer ${packageVersion}`,
...headers,
},
});
ws.addEventListener('open', () => {
return resolve(new NodeWebSocketTransport(ws));
});
ws.addEventListener('error', reject);
});
}
#ws: NodeWebSocket;
onmessage?: (message: NodeWebSocket.Data) => void;
onclose?: () => void;
constructor(ws: NodeWebSocket) {
this.#ws = ws;
this.#ws.addEventListener('message', event => {
if (this.onmessage) {
this.onmessage.call(null, event.data);
}
});
this.#ws.addEventListener('close', () => {
if (this.onclose) {
this.onclose.call(null);
}
});
// Silently ignore all errors - we don't know what to do with them.
this.#ws.addEventListener('error', () => {});
}
send(message: string): void {
this.#ws.send(message);
}
close(): void {
this.#ws.close();
}
}

95
node_modules/puppeteer-core/src/node/PipeTransport.ts generated vendored Normal file
View File

@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2018 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {DisposableStack} from '../util/disposable.js';
/**
* @internal
*/
export class PipeTransport implements ConnectionTransport {
#pipeWrite: NodeJS.WritableStream;
#subscriptions = new DisposableStack();
#isClosed = false;
#pendingMessage: Buffer[] = [];
onclose?: () => void;
onmessage?: (value: string) => void;
constructor(
pipeWrite: NodeJS.WritableStream,
pipeRead: NodeJS.ReadableStream,
) {
this.#pipeWrite = pipeWrite;
const pipeReadEmitter = this.#subscriptions.use(
// NodeJS event emitters don't support `*` so we need to typecast
// As long as we don't use it we should be OK.
new EventEmitter(
pipeRead as unknown as EventEmitter<Record<string, any>>,
),
);
pipeReadEmitter.on('data', buffer => {
return this.#dispatch(buffer);
});
pipeReadEmitter.on('close', () => {
if (this.onclose) {
this.onclose.call(null);
}
});
pipeReadEmitter.on('error', debugError);
const pipeWriteEmitter = this.#subscriptions.use(
// NodeJS event emitters don't support `*` so we need to typecast
// As long as we don't use it we should be OK.
new EventEmitter(
pipeWrite as unknown as EventEmitter<Record<string, any>>,
),
);
pipeWriteEmitter.on('error', debugError);
}
send(message: string): void {
assert(!this.#isClosed, '`PipeTransport` is closed.');
this.#pipeWrite.write(message);
this.#pipeWrite.write('\0');
}
#dispatch(buffer: Buffer<ArrayBuffer>): void {
assert(!this.#isClosed, '`PipeTransport` is closed.');
this.#pendingMessage.push(buffer);
if (buffer.indexOf('\0') === -1) {
return;
}
const concatBuffer = Buffer.concat(this.#pendingMessage);
let start = 0;
let end = concatBuffer.indexOf('\0');
while (end !== -1) {
const message = concatBuffer.toString(undefined, start, end);
setImmediate(() => {
if (this.onmessage) {
this.onmessage.call(null, message);
}
});
start = end + 1;
end = concatBuffer.indexOf('\0', start);
}
if (start >= concatBuffer.length) {
this.#pendingMessage = [];
} else {
this.#pendingMessage = [concatBuffer.subarray(start)];
}
}
close(): void {
this.#isClosed = true;
this.#subscriptions.dispose();
}
}

360
node_modules/puppeteer-core/src/node/PuppeteerNode.ts generated vendored Normal file
View File

@@ -0,0 +1,360 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {
Browser as browsers_SupportedBrowser,
resolveBuildId,
detectBrowserPlatform,
getInstalledBrowsers,
uninstall,
} from '@puppeteer/browsers';
import type {Browser} from '../api/Browser.js';
import type {Configuration} from '../common/Configuration.js';
import type {ConnectOptions} from '../common/ConnectOptions.js';
import {type CommonPuppeteerSettings, Puppeteer} from '../common/Puppeteer.js';
import type {SupportedBrowser} from '../common/SupportedBrowser.js';
import {PUPPETEER_REVISIONS} from '../revisions.js';
import type {BrowserLauncher} from './BrowserLauncher.js';
import {ChromeLauncher} from './ChromeLauncher.js';
import {FirefoxLauncher} from './FirefoxLauncher.js';
import type {ChromeReleaseChannel, LaunchOptions} from './LaunchOptions.js';
/**
* Extends the main {@link Puppeteer} class with Node specific behaviour for
* fetching and downloading browsers.
*
* If you're using Puppeteer in a Node environment, this is the class you'll get
* when you run `require('puppeteer')` (or the equivalent ES `import`).
*
* @remarks
* The most common method to use is {@link PuppeteerNode.launch | launch}, which
* is used to launch and connect to a new browser instance.
*
* See {@link Puppeteer | the main Puppeteer class} for methods common to all
* environments, such as {@link Puppeteer.connect}.
*
* @example
* The following is a typical example of using Puppeteer to drive automation:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://www.google.com');
* // other actions...
* await browser.close();
* ```
*
* Once you have created a `page` you have access to a large API to interact
* with the page, navigate, or find certain elements in that page.
* The {@link Page | `page` documentation} lists all the available methods.
*
* @public
*/
export class PuppeteerNode extends Puppeteer {
#launcher?: BrowserLauncher;
#lastLaunchedBrowser?: SupportedBrowser;
/**
* @internal
*/
defaultBrowserRevision: string;
/**
* @internal
*/
configuration: Configuration = {};
/**
* @internal
*/
constructor(
settings: {
configuration?: Configuration;
} & CommonPuppeteerSettings,
) {
const {configuration, ...commonSettings} = settings;
super(commonSettings);
if (configuration) {
this.configuration = configuration;
}
switch (this.configuration.defaultBrowser) {
case 'firefox':
this.defaultBrowserRevision = PUPPETEER_REVISIONS.firefox;
break;
default:
this.configuration.defaultBrowser = 'chrome';
this.defaultBrowserRevision = PUPPETEER_REVISIONS.chrome;
break;
}
this.connect = this.connect.bind(this);
this.launch = this.launch.bind(this);
this.executablePath = this.executablePath.bind(this);
this.defaultArgs = this.defaultArgs.bind(this);
this.trimCache = this.trimCache.bind(this);
}
/**
* This method attaches Puppeteer to an existing browser instance.
*
* @param options - Set of configurable options to set on the browser.
* @returns Promise which resolves to browser instance.
*/
override connect(options: ConnectOptions): Promise<Browser> {
return super.connect(options);
}
/**
* Launches a browser instance with given arguments and options when
* specified.
*
* When using with `puppeteer-core`,
* {@link LaunchOptions.executablePath | options.executablePath} or
* {@link LaunchOptions.channel | options.channel} must be provided.
*
* @example
* You can use {@link LaunchOptions.ignoreDefaultArgs | options.ignoreDefaultArgs}
* to filter out `--mute-audio` from default arguments:
*
* ```ts
* const browser = await puppeteer.launch({
* ignoreDefaultArgs: ['--mute-audio'],
* });
* ```
*
* @remarks
* Puppeteer can also be used to control the Chrome browser, but it works best
* with the version of Chrome for Testing downloaded by default.
* There is no guarantee it will work with any other version. If Google Chrome
* (rather than Chrome for Testing) is preferred, a
* {@link https://www.google.com/chrome/browser/canary.html | Chrome Canary}
* or
* {@link https://www.chromium.org/getting-involved/dev-channel | Dev Channel}
* build is suggested. See
* {@link https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/ | this article}
* for a description of the differences between Chromium and Chrome.
* {@link https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md | This article}
* describes some differences for Linux users. See
* {@link https://developer.chrome.com/blog/chrome-for-testing/ | this doc} for the description
* of Chrome for Testing.
*
* @param options - Options to configure launching behavior.
*/
launch(options: LaunchOptions = {}): Promise<Browser> {
const {browser = this.defaultBrowser} = options;
this.#lastLaunchedBrowser = browser;
switch (browser) {
case 'chrome':
this.defaultBrowserRevision = PUPPETEER_REVISIONS.chrome;
break;
case 'firefox':
this.defaultBrowserRevision = PUPPETEER_REVISIONS.firefox;
break;
default:
throw new Error(`Unknown product: ${browser}`);
}
this.#launcher = this.#getLauncher(browser);
return this.#launcher.launch(options);
}
/**
* @internal
*/
#getLauncher(browser: SupportedBrowser): BrowserLauncher {
if (this.#launcher && this.#launcher.browser === browser) {
return this.#launcher;
}
switch (browser) {
case 'chrome':
return new ChromeLauncher(this);
case 'firefox':
return new FirefoxLauncher(this);
default:
throw new Error(`Unknown product: ${browser}`);
}
}
/**
* The default executable path for a given ChromeReleaseChannel.
*/
executablePath(channel: ChromeReleaseChannel): string;
/**
* The default executable path given LaunchOptions.
*/
executablePath(options: LaunchOptions): string;
/**
* The default executable path.
*/
executablePath(): string;
executablePath(optsOrChannel?: ChromeReleaseChannel | LaunchOptions): string {
if (optsOrChannel === undefined) {
return this.#getLauncher(this.lastLaunchedBrowser).executablePath(
undefined,
/* validatePath= */ false,
);
}
if (typeof optsOrChannel === 'string') {
return this.#getLauncher('chrome').executablePath(
optsOrChannel,
/* validatePath= */ false,
);
}
return this.#getLauncher(
optsOrChannel.browser ?? this.lastLaunchedBrowser,
).resolveExecutablePath(optsOrChannel.headless, /* validatePath= */ false);
}
/**
* @internal
*/
get browserVersion(): string {
return (
this.configuration?.[this.lastLaunchedBrowser]?.version ??
this.defaultBrowserRevision!
);
}
/**
* The default download path for puppeteer. For puppeteer-core, this
* code should never be called as it is never defined.
*
* @internal
*/
get defaultDownloadPath(): string | undefined {
return this.configuration.cacheDirectory;
}
/**
* The name of the browser that was last launched.
*/
get lastLaunchedBrowser(): SupportedBrowser {
return this.#lastLaunchedBrowser ?? this.defaultBrowser;
}
/**
* The name of the browser that will be launched by default. For
* `puppeteer`, this is influenced by your configuration. Otherwise, it's
* `chrome`.
*/
get defaultBrowser(): SupportedBrowser {
return this.configuration.defaultBrowser ?? 'chrome';
}
/**
* @deprecated Do not use as this field as it does not take into account
* multiple browsers of different types. Use
* {@link PuppeteerNode.defaultBrowser | defaultBrowser} or
* {@link PuppeteerNode.lastLaunchedBrowser | lastLaunchedBrowser}.
*
* @returns The name of the browser that is under automation.
*/
get product(): string {
return this.lastLaunchedBrowser;
}
/**
* @param options - Set of configurable options to set on the browser.
*
* @returns The default arguments that the browser will be launched with.
*/
defaultArgs(options: LaunchOptions = {}): string[] {
return this.#getLauncher(
options.browser ?? this.lastLaunchedBrowser,
).defaultArgs(options);
}
/**
* Removes all non-current Firefox and Chrome binaries in the cache directory
* identified by the provided Puppeteer configuration. The current browser
* version is determined by resolving PUPPETEER_REVISIONS from Puppeteer
* unless `configuration.browserRevision` is provided.
*
* @remarks
*
* Note that the method does not check if any other Puppeteer versions
* installed on the host that use the same cache directory require the
* non-current binaries.
*
* @public
*/
async trimCache(): Promise<void> {
const platform = detectBrowserPlatform();
if (!platform) {
throw new Error('The current platform is not supported.');
}
const cacheDir = this.configuration.cacheDirectory!;
const installedBrowsers = await getInstalledBrowsers({
cacheDir,
});
const puppeteerBrowsers: Array<{
product: SupportedBrowser;
browser: browsers_SupportedBrowser;
currentBuildId: string;
}> = [
{
product: 'chrome',
browser: browsers_SupportedBrowser.CHROME,
currentBuildId: '',
},
{
product: 'firefox',
browser: browsers_SupportedBrowser.FIREFOX,
currentBuildId: '',
},
];
// Resolve current buildIds.
await Promise.all(
puppeteerBrowsers.map(async item => {
const tag =
this.configuration?.[item.product]?.version ??
PUPPETEER_REVISIONS[item.product];
item.currentBuildId = await resolveBuildId(item.browser, platform, tag);
}),
);
const currentBrowserBuilds = new Set(
puppeteerBrowsers.map(browser => {
return `${browser.browser}_${browser.currentBuildId}`;
}),
);
const currentBrowsers = new Set(
puppeteerBrowsers.map(browser => {
return browser.browser;
}),
);
for (const installedBrowser of installedBrowsers) {
// Don't uninstall browsers that are not managed by Puppeteer yet.
if (!currentBrowsers.has(installedBrowser.browser)) {
continue;
}
// Keep the browser build used by the current Puppeteer installation.
if (
currentBrowserBuilds.has(
`${installedBrowser.browser}_${installedBrowser.buildId}`,
)
) {
continue;
}
await uninstall({
browser: installedBrowser.browser,
platform,
cacheDir,
buildId: installedBrowser.buildId,
});
}
}
}

350
node_modules/puppeteer-core/src/node/ScreenRecorder.ts generated vendored Normal file
View File

@@ -0,0 +1,350 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ChildProcessWithoutNullStreams} from 'node:child_process';
import {spawn, spawnSync} from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import {dirname} from 'node:path';
import {PassThrough} from 'node:stream';
import debug from 'debug';
import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
import {
bufferCount,
concatMap,
filter,
from,
fromEvent,
lastValueFrom,
map,
takeUntil,
tap,
} from '../../third_party/rxjs/rxjs.js';
import {CDPSessionEvent} from '../api/CDPSession.js';
import type {BoundingBox} from '../api/ElementHandle.js';
import type {Page, VideoFormat} from '../api/Page.js';
import {debugError, fromEmitterEvent} from '../common/util.js';
import {guarded} from '../util/decorators.js';
import {asyncDisposeSymbol} from '../util/disposable.js';
const CRF_VALUE = 30;
const DEFAULT_FPS = 30;
const debugFfmpeg = debug('puppeteer:ffmpeg');
/**
* @internal
*/
export interface ScreenRecorderOptions {
ffmpegPath?: string;
speed?: number;
crop?: BoundingBox;
format?: VideoFormat;
fps?: number;
loop?: number;
delay?: number;
quality?: number;
colors?: number;
scale?: number;
path?: `${string}.${VideoFormat}`;
overwrite?: boolean;
}
/**
* @public
*/
export class ScreenRecorder extends PassThrough {
#page: Page;
#process: ChildProcessWithoutNullStreams;
#controller = new AbortController();
#lastFrame: Promise<readonly [Buffer, number]>;
#fps: number;
/**
* @internal
*/
constructor(
page: Page,
width: number,
height: number,
{
ffmpegPath,
speed,
scale,
crop,
format,
fps,
loop,
delay,
quality,
colors,
path,
overwrite,
}: ScreenRecorderOptions = {},
) {
super({allowHalfOpen: false});
ffmpegPath ??= 'ffmpeg';
format ??= 'webm';
fps ??= DEFAULT_FPS;
// Maps 0 to -1 as ffmpeg maps 0 to infinity.
loop ||= -1;
delay ??= -1;
quality ??= CRF_VALUE;
colors ??= 256;
overwrite ??= true;
this.#fps = fps;
// Tests if `ffmpeg` exists.
const {error} = spawnSync(ffmpegPath);
if (error) {
throw error;
}
const filters = [
`crop='min(${width},iw):min(${height},ih):0:0'`,
`pad=${width}:${height}:0:0`,
];
if (speed) {
filters.push(`setpts=${1 / speed}*PTS`);
}
if (crop) {
filters.push(`crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}`);
}
if (scale) {
filters.push(`scale=iw*${scale}:-1:flags=lanczos`);
}
const formatArgs = this.#getFormatArgs(
format,
fps,
loop,
delay,
quality,
colors,
);
const vf = formatArgs.indexOf('-vf');
if (vf !== -1) {
filters.push(formatArgs.splice(vf, 2).at(-1) ?? '');
}
// Ensure provided output directory path exists.
if (path) {
fs.mkdirSync(dirname(path), {recursive: overwrite});
}
this.#process = spawn(
ffmpegPath,
// See https://trac.ffmpeg.org/wiki/Encode/VP9 for more information on flags.
[
['-loglevel', 'error'],
// Reduces general buffering.
['-avioflags', 'direct'],
// Reduces initial buffering while analyzing input fps and other stats.
[
'-fpsprobesize',
'0',
'-probesize',
'32',
'-analyzeduration',
'0',
'-fflags',
'nobuffer',
],
// Forces input to be read from standard input, and forces png input
// image format.
['-f', 'image2pipe', '-vcodec', 'png', '-i', 'pipe:0'],
// No audio
['-an'],
// This drastically reduces stalling when cpu is overbooked. By default
// VP9 tries to use all available threads?
['-threads', '1'],
// Specifies the frame rate we are giving ffmpeg.
['-framerate', `${fps}`],
// Disable bitrate.
['-b:v', '0'],
// Specifies the encoding and format we are using.
formatArgs,
// Filters to ensure the images are piped correctly,
// combined with any format-specific filters.
['-vf', filters.join()],
// Overwrite output, or exit immediately if file already exists.
[overwrite ? '-y' : '-n'],
'pipe:1',
].flat(),
{stdio: ['pipe', 'pipe', 'pipe']},
);
this.#process.stdout.pipe(this);
this.#process.stderr.on('data', (data: Buffer) => {
debugFfmpeg(data.toString('utf8'));
});
this.#page = page;
const {client} = this.#page.mainFrame();
client.once(CDPSessionEvent.Disconnected, () => {
void this.stop().catch(debugError);
});
this.#lastFrame = lastValueFrom(
fromEmitterEvent(client, 'Page.screencastFrame').pipe(
tap(event => {
void client.send('Page.screencastFrameAck', {
sessionId: event.sessionId,
});
}),
filter(event => {
return event.metadata.timestamp !== undefined;
}),
map(event => {
return {
buffer: Buffer.from(event.data, 'base64'),
timestamp: event.metadata.timestamp!,
};
}),
bufferCount(2, 1) as OperatorFunction<
{buffer: Buffer; timestamp: number},
[
{buffer: Buffer; timestamp: number},
{buffer: Buffer; timestamp: number},
]
>,
concatMap(([{timestamp: previousTimestamp, buffer}, {timestamp}]) => {
return from(
Array<Buffer>(
Math.round(fps * Math.max(timestamp - previousTimestamp, 0)),
).fill(buffer),
);
}),
map(buffer => {
void this.#writeFrame(buffer);
return [buffer, performance.now()] as const;
}),
takeUntil(fromEvent(this.#controller.signal, 'abort')),
),
{defaultValue: [Buffer.from([]), performance.now()] as const},
);
}
#getFormatArgs(
format: VideoFormat,
fps: number | 'source_fps',
loop: number,
delay: number,
quality: number,
colors: number,
): string[] {
const libvpx = [
['-vcodec', 'vp9'],
// Sets the quality. Lower the better.
['-crf', `${quality}`],
// Sets the quality and how efficient the compression will be.
[
'-deadline',
'realtime',
'-cpu-used',
`${Math.min(os.cpus().length / 2, 8)}`,
],
];
switch (format) {
case 'webm':
return [
...libvpx,
// Sets the format
['-f', 'webm'],
].flat();
case 'gif':
fps = DEFAULT_FPS === fps ? 20 : 'source_fps';
if (loop === Infinity) {
loop = 0;
}
if (delay !== -1) {
// ms to cs
delay /= 10;
}
return [
// Sets the frame rate and uses a custom palette generated from the
// input.
[
'-vf',
`fps=${fps},split[s0][s1];[s0]palettegen=stats_mode=diff:max_colors=${colors}[p];[s1][p]paletteuse=dither=bayer`,
],
// Sets the number of times to loop playback.
['-loop', `${loop}`],
// Sets the delay between iterations of a loop.
['-final_delay', `${delay}`],
// Sets the format
['-f', 'gif'],
].flat();
case 'mp4':
return [
...libvpx,
// Fragment file during stream to avoid errors.
['-movflags', 'hybrid_fragmented'],
// Sets the format
['-f', 'mp4'],
].flat();
}
}
@guarded()
async #writeFrame(buffer: Buffer) {
const error = await new Promise<Error | null | undefined>(resolve => {
this.#process.stdin.write(buffer, resolve);
});
if (error) {
console.log(`ffmpeg failed to write: ${error.message}.`);
}
}
/**
* Stops the recorder.
*
* @public
*/
@guarded()
async stop(): Promise<void> {
if (this.#controller.signal.aborted) {
return;
}
// Stopping the screencast will flush the frames.
await this.#page._stopScreencast().catch(debugError);
this.#controller.abort();
// Repeat the last frame for the remaining frames.
const [buffer, timestamp] = await this.#lastFrame;
await Promise.all(
Array<Buffer>(
Math.max(
1,
Math.round((this.#fps * (performance.now() - timestamp)) / 1000),
),
)
.fill(buffer)
.map(this.#writeFrame.bind(this)),
);
// Close stdin to notify FFmpeg we are done.
this.#process.stdin.end();
await new Promise(resolve => {
this.#process.once('close', resolve);
});
}
/**
* @internal
*/
override async [asyncDisposeSymbol](): Promise<void> {
await this.stop();
}
}

13
node_modules/puppeteer-core/src/node/node.ts generated vendored Normal file
View File

@@ -0,0 +1,13 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export * from './ChromeLauncher.js';
export * from './FirefoxLauncher.js';
export type * from './LaunchOptions.js';
export * from './PipeTransport.js';
export * from './BrowserLauncher.js';
export * from './PuppeteerNode.js';
export * from './ScreenRecorder.js';

27
node_modules/puppeteer-core/src/node/util/fs.ts generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
const rmOptions = {
force: true,
recursive: true,
maxRetries: 5,
};
/**
* @internal
*/
export async function rm(path: string): Promise<void> {
await fs.promises.rm(path, rmOptions);
}
/**
* @internal
*/
export function rmSync(path: string): void {
fs.rmSync(path, rmOptions);
}