@vpalmisano/webrtcperf 4.0.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.
Files changed (53) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +296 -0
  3. package/app.min.js +2 -0
  4. package/build/src/app.d.ts +6 -0
  5. package/build/src/app.js +207 -0
  6. package/build/src/app.js.map +1 -0
  7. package/build/src/config.d.ts +104 -0
  8. package/build/src/config.js +880 -0
  9. package/build/src/config.js.map +1 -0
  10. package/build/src/generate-config-docs.d.ts +1 -0
  11. package/build/src/generate-config-docs.js +41 -0
  12. package/build/src/generate-config-docs.js.map +1 -0
  13. package/build/src/index.d.ts +9 -0
  14. package/build/src/index.js +26 -0
  15. package/build/src/index.js.map +1 -0
  16. package/build/src/media.d.ts +33 -0
  17. package/build/src/media.js +113 -0
  18. package/build/src/media.js.map +1 -0
  19. package/build/src/rtcstats.d.ts +302 -0
  20. package/build/src/rtcstats.js +418 -0
  21. package/build/src/rtcstats.js.map +1 -0
  22. package/build/src/server.d.ts +173 -0
  23. package/build/src/server.js +639 -0
  24. package/build/src/server.js.map +1 -0
  25. package/build/src/session.d.ts +277 -0
  26. package/build/src/session.js +1552 -0
  27. package/build/src/session.js.map +1 -0
  28. package/build/src/stats.d.ts +243 -0
  29. package/build/src/stats.js +1383 -0
  30. package/build/src/stats.js.map +1 -0
  31. package/build/src/utils.d.ts +249 -0
  32. package/build/src/utils.js +1220 -0
  33. package/build/src/utils.js.map +1 -0
  34. package/build/src/visqol.d.ts +6 -0
  35. package/build/src/visqol.js +61 -0
  36. package/build/src/visqol.js.map +1 -0
  37. package/build/src/vmaf.d.ts +83 -0
  38. package/build/src/vmaf.js +624 -0
  39. package/build/src/vmaf.js.map +1 -0
  40. package/build/tsconfig.tsbuildinfo +1 -0
  41. package/package.json +129 -0
  42. package/src/app.ts +241 -0
  43. package/src/config.ts +852 -0
  44. package/src/generate-config-docs.ts +47 -0
  45. package/src/index.ts +9 -0
  46. package/src/media.ts +151 -0
  47. package/src/rtcstats.ts +507 -0
  48. package/src/server.ts +645 -0
  49. package/src/session.ts +1908 -0
  50. package/src/stats.ts +1668 -0
  51. package/src/utils.ts +1295 -0
  52. package/src/visqol.ts +62 -0
  53. package/src/vmaf.ts +771 -0
@@ -0,0 +1,1552 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Session = void 0;
40
+ const throttler_1 = require("@vpalmisano/throttler");
41
+ const assert_1 = __importDefault(require("assert"));
42
+ const axios_1 = __importDefault(require("axios"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const events_1 = __importDefault(require("events"));
45
+ const fs_1 = __importDefault(require("fs"));
46
+ const json5_1 = __importDefault(require("json5"));
47
+ const lorem_ipsum_1 = require("lorem-ipsum");
48
+ const node_cache_1 = __importDefault(require("node-cache"));
49
+ const os_1 = __importDefault(require("os"));
50
+ const path_1 = __importDefault(require("path"));
51
+ const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
52
+ const puppeteer_intercept_and_modify_requests_1 = require("puppeteer-intercept-and-modify-requests");
53
+ const sdpTransform = __importStar(require("sdp-transform"));
54
+ const zlib_1 = require("zlib");
55
+ const rtcstats_1 = require("./rtcstats");
56
+ const stats_1 = require("./stats");
57
+ const utils_1 = require("./utils");
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
59
+ const NavigatorHardwareConcurrency = require('puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency');
60
+ const log = (0, utils_1.logger)('webrtcperf:session');
61
+ const PageLogColors = {
62
+ error: 'red',
63
+ warn: 'yellow',
64
+ info: 'cyan',
65
+ log: 'grey',
66
+ debug: 'white',
67
+ requestfailed: 'magenta',
68
+ };
69
+ /**
70
+ * Implements a test session instance running on a browser instance.
71
+ */
72
+ class Session extends events_1.default {
73
+ chromiumUrl;
74
+ chromiumPath;
75
+ chromiumFieldTrials;
76
+ windowWidth;
77
+ windowHeight;
78
+ deviceScaleFactor;
79
+ display;
80
+ /* private readonly audioRedForOpus: boolean */
81
+ mediaPath;
82
+ videoWidth;
83
+ videoHeight;
84
+ videoFramerate;
85
+ useFakeMedia;
86
+ enableGpu;
87
+ enableBrowserLogging;
88
+ startTimestamp;
89
+ sessions;
90
+ tabsPerSession;
91
+ spawnPeriod;
92
+ statsInterval;
93
+ disabledVideoCodecs;
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ localStorage;
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ sessionStorage;
98
+ clearCookies;
99
+ scriptPath;
100
+ showPageLog;
101
+ pageLogFilter;
102
+ pageLogPath;
103
+ userAgent;
104
+ evaluateAfter;
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ exposedFunctions;
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ scriptParams;
109
+ blockedUrls;
110
+ extraHeaders;
111
+ responseModifiers = {};
112
+ downloadResponses = [];
113
+ extraCSS;
114
+ cookies = [];
115
+ overridePermissions = [];
116
+ hardwareConcurrency;
117
+ debuggingPort;
118
+ debuggingAddress;
119
+ randomAudioPeriod;
120
+ maxVideoDecoders;
121
+ maxVideoDecodersAt;
122
+ incognito;
123
+ serverPort;
124
+ serverSecret;
125
+ serverUseHttps;
126
+ running = false;
127
+ browser;
128
+ context;
129
+ stopPortForwarder;
130
+ /** The numeric id assigned to the session. */
131
+ id;
132
+ /** The throttle configuration index assigned to the session. */
133
+ throttleIndex;
134
+ /** The test page url. */
135
+ url;
136
+ /** The url query. */
137
+ urlQuery;
138
+ /**
139
+ * The custom URL handler. This is the path to a JavaScript module (.mjs) exporting the function.
140
+ * The function itself takes an object as input with the following parameters:
141
+ *
142
+ * @typedef {Object} CustomUrlHandler
143
+ * @property {string} id - The identifier for the URL.
144
+ * @property {string} sessions - The number of sessions.
145
+ * @property {string} tabIndex - The index of the current tab.
146
+ * @property {string} tabsPerSession - The number of tabs per session.
147
+ * @property {string} index - The index for the URL.
148
+ * @property {string} pid - The process identifier for the URL.
149
+ *
150
+ * @type {string} path - The path to the JavaScript file containing the function:
151
+ * (params: CustomUrlHandler) => Promise<string>
152
+ */
153
+ customUrlHandler;
154
+ /**
155
+ * Imported custom URL handler function.
156
+ * @typedef {Object} CustomUrlHandler
157
+ * @property {number} id - The identifier for the URL.
158
+ * @property {number} sessions - The number of sessions.
159
+ * @property {number} tabIndex - The index of the current tab.
160
+ * @property {number} tabsPerSession - The number of tabs per session.
161
+ * @property {number} index - The index for the URL.
162
+ * @property {number} pid - The process identifier for the URL.
163
+ * @property {Record<string, string>} env - The process environment.
164
+ *
165
+ * @type {string} path - The path to the JavaScript file containing the function:
166
+ * (params: CustomUrlHandler) => Promise<string>
167
+ */
168
+ customUrlHandlerFn;
169
+ /** The latest stats extracted from page. */
170
+ stats = {};
171
+ /** The browser opened pages. */
172
+ pages = new Map();
173
+ httpResourcesStats = new Map();
174
+ /** The browser opened pages metrics. */
175
+ pagesMetrics = new Map();
176
+ /** The page warnings count. */
177
+ pageWarnings = 0;
178
+ /** The page errors count. */
179
+ pageErrors = 0;
180
+ screensharePage;
181
+ static jsonFetchCache = new node_cache_1.default({
182
+ stdTTL: 30,
183
+ checkperiod: 15,
184
+ });
185
+ constructor({ chromiumUrl, chromiumPath, chromiumFieldTrials, windowWidth, windowHeight, deviceScaleFactor, display,
186
+ /* audioRedForOpus, */
187
+ url, urlQuery, customUrlHandler, customUrlHandlerFn, mediaPath, videoWidth, videoHeight, videoFramerate, useFakeMedia, enableGpu, enableBrowserLogging, startTimestamp, sessions, tabsPerSession, spawnPeriod, statsInterval, disabledVideoCodecs, localStorage, sessionStorage, clearCookies, scriptPath, showPageLog, pageLogFilter, pageLogPath, userAgent, id, throttleIndex, evaluateAfter, exposedFunctions, scriptParams, blockedUrls, extraHeaders, responseModifiers, downloadResponses, extraCSS, cookies, overridePermissions, hardwareConcurrency, debuggingPort, debuggingAddress, randomAudioPeriod, maxVideoDecoders, maxVideoDecodersAt, incognito, serverPort, serverSecret, serverUseHttps, }) {
188
+ super();
189
+ log.debug('constructor', { id });
190
+ this.id = id;
191
+ this.chromiumUrl = chromiumUrl;
192
+ this.chromiumPath = chromiumPath || undefined;
193
+ this.chromiumFieldTrials = chromiumFieldTrials || undefined;
194
+ this.windowWidth = windowWidth || 1920;
195
+ this.windowHeight = windowHeight || 1080;
196
+ this.deviceScaleFactor = deviceScaleFactor || 1;
197
+ this.debuggingPort = debuggingPort || 0;
198
+ this.debuggingAddress = debuggingAddress || '';
199
+ this.display = display;
200
+ /* this.audioRedForOpus = !!audioRedForOpus */
201
+ this.url = url;
202
+ this.urlQuery = urlQuery;
203
+ if (!this.urlQuery && url.includes('?')) {
204
+ const parts = url.split('?', 2);
205
+ this.url = parts[0];
206
+ this.urlQuery = parts[1];
207
+ }
208
+ this.customUrlHandler = customUrlHandler;
209
+ this.customUrlHandlerFn = customUrlHandlerFn;
210
+ this.mediaPath = mediaPath;
211
+ this.videoWidth = videoWidth;
212
+ this.videoHeight = videoHeight;
213
+ this.videoFramerate = videoFramerate;
214
+ this.useFakeMedia = useFakeMedia;
215
+ this.enableGpu = enableGpu;
216
+ this.enableBrowserLogging = (0, utils_1.enabledForSession)(this.id, enableBrowserLogging);
217
+ this.startTimestamp = startTimestamp || Date.now();
218
+ this.sessions = sessions || 1;
219
+ this.tabsPerSession = tabsPerSession || 1;
220
+ (0, assert_1.default)(this.tabsPerSession >= 1, 'tabsPerSession should be >= 1');
221
+ this.spawnPeriod = spawnPeriod || 1000;
222
+ this.statsInterval = statsInterval || 10;
223
+ if (disabledVideoCodecs) {
224
+ this.disabledVideoCodecs = disabledVideoCodecs
225
+ .split(',')
226
+ .map(s => s.trim())
227
+ .filter(s => s.length);
228
+ }
229
+ else {
230
+ this.disabledVideoCodecs = [];
231
+ }
232
+ if (localStorage) {
233
+ try {
234
+ this.localStorage = json5_1.default.parse(localStorage);
235
+ }
236
+ catch (err) {
237
+ log.error(`error parsing localStorage: ${err.stack}`);
238
+ this.localStorage = null;
239
+ }
240
+ }
241
+ if (sessionStorage) {
242
+ try {
243
+ this.sessionStorage = json5_1.default.parse(sessionStorage);
244
+ }
245
+ catch (err) {
246
+ log.error(`error parsing sessionStorage: ${err.stack}`);
247
+ this.sessionStorage = null;
248
+ }
249
+ }
250
+ this.clearCookies = clearCookies;
251
+ this.scriptPath = scriptPath;
252
+ this.showPageLog = showPageLog;
253
+ this.pageLogFilter = pageLogFilter;
254
+ this.pageLogPath = pageLogPath;
255
+ this.userAgent = userAgent;
256
+ this.randomAudioPeriod = randomAudioPeriod;
257
+ this.maxVideoDecoders = maxVideoDecoders;
258
+ this.maxVideoDecodersAt = maxVideoDecodersAt;
259
+ this.incognito = incognito;
260
+ this.serverPort = serverPort;
261
+ this.serverSecret = serverSecret;
262
+ this.serverUseHttps = serverUseHttps;
263
+ this.throttleIndex = throttleIndex;
264
+ this.evaluateAfter = evaluateAfter || [];
265
+ this.exposedFunctions = exposedFunctions || {};
266
+ if (scriptParams) {
267
+ try {
268
+ this.scriptParams = json5_1.default.parse(scriptParams);
269
+ }
270
+ catch (err) {
271
+ log.error(`error parsing scriptParams '${scriptParams}': ${err.stack}`);
272
+ throw err;
273
+ }
274
+ }
275
+ else {
276
+ this.scriptParams = {};
277
+ }
278
+ this.blockedUrls = (blockedUrls || '')
279
+ .split(',')
280
+ .map(s => s.trim())
281
+ .filter(s => s.length);
282
+ // Always block sentry.io.
283
+ this.blockedUrls.push('ingest.sentry.io');
284
+ if (extraHeaders) {
285
+ try {
286
+ this.extraHeaders = json5_1.default.parse(extraHeaders);
287
+ }
288
+ catch (err) {
289
+ log.error(`error parsing extraHeaders: ${err.stack}`);
290
+ this.extraHeaders = undefined;
291
+ }
292
+ }
293
+ else {
294
+ this.extraHeaders = undefined;
295
+ }
296
+ if (responseModifiers) {
297
+ try {
298
+ const parsed = json5_1.default.parse(responseModifiers);
299
+ Object.entries(parsed).forEach(([url, replacements]) => {
300
+ if (!Array.isArray(replacements)) {
301
+ throw new Error(`responseModifiers replacements should be an array of { search, replace, body, headers } objects: ${replacements}`);
302
+ }
303
+ this.responseModifiers[url] = replacements.map(({ search, replace, file, headers }) => ({
304
+ search: search ? new RegExp(search, 'g') : undefined,
305
+ replace,
306
+ file,
307
+ headers,
308
+ }));
309
+ });
310
+ }
311
+ catch (err) {
312
+ throw new Error(`error parsing responseModifiers "${responseModifiers}": ${err.stack}`);
313
+ }
314
+ }
315
+ if (downloadResponses) {
316
+ try {
317
+ const parsed = json5_1.default.parse(downloadResponses);
318
+ if (!Array.isArray(parsed))
319
+ throw new Error(`downloadResponses should be an array: ${downloadResponses}`);
320
+ parsed.forEach(({ urlPattern, output, append }) => {
321
+ this.downloadResponses.push({ urlPattern: (0, puppeteer_intercept_and_modify_requests_1.getUrlPatternRegExp)(urlPattern), output, append });
322
+ });
323
+ }
324
+ catch (err) {
325
+ throw new Error(`error parsing downloadResponses "${downloadResponses}": ${err.stack}`);
326
+ }
327
+ }
328
+ this.extraCSS = extraCSS;
329
+ if (cookies) {
330
+ try {
331
+ this.cookies = json5_1.default.parse(cookies);
332
+ }
333
+ catch (err) {
334
+ log.error(`error parsing cookies: ${err.stack}`);
335
+ }
336
+ }
337
+ if (overridePermissions) {
338
+ this.overridePermissions = overridePermissions
339
+ .split(',')
340
+ .map(s => s.trim())
341
+ .filter(s => s.length);
342
+ }
343
+ this.hardwareConcurrency = hardwareConcurrency;
344
+ }
345
+ /**
346
+ * Returns the chromium browser launch args
347
+ * @return the args list
348
+ */
349
+ getBrowserArgs(env) {
350
+ // https://peter.sh/experiments/chromium-command-line-switches/
351
+ // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/fieldtrial_testing_config.json;l=8877?q=%20fieldtrial_testing_config.json&ss=chromium
352
+ const args = [
353
+ '--no-sandbox',
354
+ '--no-zygote',
355
+ '--ignore-certificate-errors',
356
+ '--no-user-gesture-required',
357
+ '--autoplay-policy=no-user-gesture-required',
358
+ '--disable-infobars',
359
+ '--allow-running-insecure-content',
360
+ `--unsafely-treat-insecure-origin-as-secure=http://${new URL(this.url || 'http://localhost').host}`,
361
+ '--disable-web-security',
362
+ '--disable-features=IsolateOrigins,Translate,CalculateNativeWinOcclusion',
363
+ '--disable-background-timer-throttling',
364
+ '--disable-backgrounding-occluded-windows',
365
+ '--disable-renderer-backgrounding',
366
+ '--disable-site-isolation-trials',
367
+ '--enable-usermedia-screen-capturing',
368
+ '--allow-http-screen-capture',
369
+ `--remote-debugging-port=${this.debuggingPort ? this.debuggingPort + this.id : 0}`,
370
+ '--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,VaapiVideoDecodeLinuxGL,ElementCapture',
371
+ `--window-size=${this.windowWidth},${this.windowHeight}`,
372
+ ];
373
+ let fieldTrials = this.chromiumFieldTrials || '';
374
+ if (this.enableBrowserLogging && this.pageLogPath) {
375
+ const pageLogDir = path_1.default.dirname(this.pageLogPath);
376
+ const eventLogPath = path_1.default.resolve(pageLogDir, `webrtc-event-logging-${this.id}`);
377
+ fs_1.default.mkdirSync(eventLogPath, { recursive: true });
378
+ args.push('--enable-logging', '--vmodule=*/webrtc/*=5', '--v=0', `--webrtc-event-logging=${eventLogPath}`);
379
+ fieldTrials = 'WebRTC-RtcEventLogNewFormat/Disabled/' + fieldTrials;
380
+ env.CHROME_LOG_FILE = path_1.default.resolve(pageLogDir, `chrome-${this.id}.log`);
381
+ }
382
+ if (this.maxVideoDecoders !== -1 && this.id >= this.maxVideoDecodersAt) {
383
+ fieldTrials = `WebRTC-MaxVideoDecoders/${this.maxVideoDecoders}/` + fieldTrials;
384
+ }
385
+ if (fieldTrials.length) {
386
+ args.push(`--force-fieldtrials=${fieldTrials}`);
387
+ }
388
+ if (this.mediaPath) {
389
+ if (this.useFakeMedia) {
390
+ log.debug(`${this.id} using chromium as fake media source`);
391
+ args.push('--use-fake-ui-for-media-stream', `--use-fake-device-for-media-stream=display-media-type=browser,fps=30`, `--use-file-for-fake-video-capture=${this.mediaPath.video}`, `--use-file-for-fake-audio-capture=${this.mediaPath.audio}`);
392
+ }
393
+ else {
394
+ log.debug(`${this.id} using ${this.mediaPath} as fake media source`);
395
+ args.push('--auto-accept-camera-and-microphone-capture', `--auto-select-tab-capture-source-by-title=webrtcperf-screenshare`, '--mute-audio');
396
+ }
397
+ }
398
+ if (this.enableGpu) {
399
+ args.push('--ignore-gpu-blocklist', '--enable-gpu-rasterization', '--enable-zero-copy', '--disable-gpu-sandbox', '--enable-vulkan');
400
+ if (this.enableGpu === 'egl') {
401
+ args.push('--use-gl=egl');
402
+ }
403
+ else {
404
+ args.push('--use-gl=angle', '--use-angle=vulkan');
405
+ }
406
+ }
407
+ else {
408
+ args.push(
409
+ // Disables webgl support.
410
+ '--disable-3d-apis', '--disable-site-isolation-trials');
411
+ }
412
+ return args;
413
+ }
414
+ /**
415
+ * Start
416
+ */
417
+ async start() {
418
+ if (this.running) {
419
+ return;
420
+ }
421
+ this.running = true;
422
+ if (this.browser) {
423
+ log.warn(`${this.id} start: already running`);
424
+ return;
425
+ }
426
+ log.debug(`${this.id} start`);
427
+ if (this.chromiumUrl) {
428
+ // connect to a remote chrome instance
429
+ try {
430
+ this.browser = await puppeteer_core_1.default.connect({
431
+ browserURL: this.chromiumUrl,
432
+ defaultViewport: {
433
+ width: this.windowWidth,
434
+ height: this.windowHeight,
435
+ deviceScaleFactor: this.deviceScaleFactor,
436
+ isMobile: false,
437
+ hasTouch: false,
438
+ isLandscape: false,
439
+ },
440
+ });
441
+ }
442
+ catch (err) {
443
+ log.error(`${this.id} browser connect error: ${err.stack}`);
444
+ return this.stop();
445
+ }
446
+ }
447
+ else {
448
+ // run a browser instance locally
449
+ let executablePath = this.chromiumPath;
450
+ if (!executablePath || !fs_1.default.existsSync(executablePath)) {
451
+ executablePath = await (0, utils_1.checkChromeExecutable)();
452
+ log.debug(`using executablePath=${executablePath}`);
453
+ }
454
+ // Create the process wrapper.
455
+ if (this.throttleIndex > -1 && os_1.default.platform() === 'linux') {
456
+ executablePath = await (0, throttler_1.throttleLauncher)(executablePath, this.throttleIndex);
457
+ }
458
+ const env = { ...process.env };
459
+ if (!this.display) {
460
+ delete env.DISPLAY;
461
+ }
462
+ else {
463
+ env.DISPLAY = this.display;
464
+ }
465
+ const args = this.getBrowserArgs(env);
466
+ const ignoreDefaultArgs = [
467
+ '--disable-dev-shm-usage',
468
+ '--remote-debugging-port',
469
+ //'--hide-scrollbars',
470
+ '--enable-automation',
471
+ '--window-size',
472
+ ];
473
+ log.debug(`[session ${this.id}] Using args:\n ${args.join('\n ')}`);
474
+ log.debug(`[session ${this.id}] Default args:\n ${puppeteer_core_1.default.defaultArgs().join('\n ')}`);
475
+ try {
476
+ this.browser = await puppeteer_core_1.default.launch({
477
+ browser: 'chrome',
478
+ headless: this.display ? false : true,
479
+ executablePath,
480
+ handleSIGINT: false,
481
+ env,
482
+ // dumpio: this.enableBrowserLogging,
483
+ // devtools: true,
484
+ defaultViewport: {
485
+ width: this.windowWidth,
486
+ height: this.windowHeight,
487
+ deviceScaleFactor: this.deviceScaleFactor,
488
+ isMobile: false,
489
+ hasTouch: false,
490
+ isLandscape: false,
491
+ },
492
+ ignoreDefaultArgs,
493
+ args,
494
+ });
495
+ const version = await this.browser.version();
496
+ log.debug(`[session ${this.id}] Using chrome version: ${version}`);
497
+ }
498
+ catch (err) {
499
+ log.error(`[session ${this.id}] Browser launch error: ${err.stack}`);
500
+ return this.stop();
501
+ }
502
+ }
503
+ (0, assert_1.default)(this.browser, 'BrowserNotCreated');
504
+ if (this.debuggingPort && this.debuggingAddress !== '127.0.0.1') {
505
+ this.stopPortForwarder = await (0, utils_1.portForwarder)(this.debuggingPort + this.id, this.debuggingAddress);
506
+ }
507
+ this.browser.once('disconnected', () => {
508
+ log.debug('browser disconnected');
509
+ return this.stop();
510
+ });
511
+ // get GPU infos from chrome://gpu page
512
+ /* if (this.enableGpu) {
513
+ try {
514
+ const page = await this.browser.newPage()
515
+ await page.goto('chrome://gpu')
516
+ const data = await page.evaluate(() =>
517
+ [
518
+ // eslint-disable-next-line no-undef
519
+ ...document.querySelectorAll('ul.feature-status-list > li > span'),
520
+ ].map(
521
+ (e, i) =>
522
+ `${i % 2 === 0 ? '\n- ' : ''}${(e as HTMLSpanElement).innerText}`,
523
+ ),
524
+ )
525
+ await page.close()
526
+ console.log(`GPU infos:${data.join('')}`)
527
+ } catch (err) {
528
+ log.warn(`${this.id} error getting gpu info: %j`, err)
529
+ }
530
+ } */
531
+ // open pages
532
+ for (let i = 0; i < this.tabsPerSession; i++) {
533
+ this.openPage(i).catch(err => log.error(`openPage error: ${err.stack}`));
534
+ if (i < this.tabsPerSession - 1) {
535
+ await (0, utils_1.sleep)(this.spawnPeriod);
536
+ }
537
+ }
538
+ }
539
+ setupPageCmd(index, tabIndex, url) {
540
+ let cmd = `\
541
+ webrtcperf = {};
542
+ webrtcperf.config = {
543
+ START_TIMESTAMP: ${this.startTimestamp},
544
+ WEBRTC_PERF_URL: "${(0, utils_1.hideAuth)(url)}",
545
+ WEBRTC_PERF_SESSION: ${this.id},
546
+ WEBRTC_PERF_TAB_INDEX: ${tabIndex},
547
+ WEBRTC_PERF_INDEX: ${index},
548
+ STATS_INTERVAL: ${this.statsInterval},
549
+ VIDEO_WIDTH: ${this.videoWidth},
550
+ VIDEO_HEIGHT: ${this.videoHeight},
551
+ VIDEO_FRAMERATE: ${this.videoFramerate},
552
+ RANDOM_AUDIO_PERIOD: ${this.randomAudioPeriod},
553
+ USE_FAKE_MEDIA: ${this.useFakeMedia},
554
+ };
555
+ try {
556
+ webrtcperf.params = JSON.parse('${JSON.stringify(this.scriptParams)}' || '{}');
557
+ } catch (err) {
558
+ console.error('[webrtcperf] Error parsing scriptParams:', err);
559
+ webrtcperf.params = {};
560
+ }
561
+ `;
562
+ if (this.serverPort) {
563
+ cmd += `\
564
+ webrtcperf.config.SAVE_MEDIA_URL = "ws${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/?auth=${this.serverSecret}&action=write-stream";
565
+ `;
566
+ if (this.mediaPath?.mp4 && !this.useFakeMedia) {
567
+ cmd += `\
568
+ webrtcperf.config.VIDEO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/cache/${path_1.default.basename(this.mediaPath.mp4)}?auth=${this.serverSecret}";
569
+ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/cache/${path_1.default.basename(this.mediaPath.m4a)}?auth=${this.serverSecret}";
570
+ `;
571
+ }
572
+ }
573
+ if (this.disabledVideoCodecs.length) {
574
+ log.debug('Using disabledVideoCodecs:', this.disabledVideoCodecs);
575
+ cmd += `webrtcperf.config.GET_CAPABILITIES_DISABLED_VIDEO_CODECS = JSON.parse('${JSON.stringify(this.disabledVideoCodecs)}');\n`;
576
+ }
577
+ return cmd;
578
+ }
579
+ /**
580
+ * openPage
581
+ * @param tabIndex
582
+ */
583
+ async openPage(tabIndex) {
584
+ if (!this.browser) {
585
+ return;
586
+ }
587
+ const index = this.id + tabIndex;
588
+ let saveFile = undefined;
589
+ let url = this.url;
590
+ if (!url) {
591
+ if (this.customUrlHandler && !this.customUrlHandlerFn) {
592
+ const customUrlHandlerPath = path_1.default.resolve(process.cwd(), this.customUrlHandler);
593
+ if (!fs_1.default.existsSync(customUrlHandlerPath)) {
594
+ throw new Error(`Custom url handler script not found: "${customUrlHandlerPath}"`);
595
+ }
596
+ this.customUrlHandlerFn = (await import(/* webpackIgnore: true */ customUrlHandlerPath)).default;
597
+ }
598
+ if (!this.customUrlHandlerFn) {
599
+ throw new Error(`Custom url handler function not set`);
600
+ }
601
+ url = await this.customUrlHandlerFn({
602
+ id: this.id,
603
+ sessions: this.sessions,
604
+ tabIndex,
605
+ tabsPerSession: this.tabsPerSession,
606
+ index,
607
+ pid: process.pid,
608
+ env: { ...process.env },
609
+ params: this.scriptParams,
610
+ });
611
+ log.debug(`customUrlHandlerFn: ${url}`);
612
+ }
613
+ if (!url) {
614
+ throw new Error(`Page URL not set`);
615
+ }
616
+ if (this.urlQuery) {
617
+ url += `?${this.urlQuery
618
+ .replace(/\$s/g, String(this.id))
619
+ .replace(/\$S/g, String(this.sessions))
620
+ .replace(/\$t/g, String(tabIndex))
621
+ .replace(/\$T/g, String(this.tabsPerSession))
622
+ .replace(/\$i/g, String(index))
623
+ .replace(/\$p/g, String(process.pid))}`;
624
+ }
625
+ log.debug(`opening page ${index} (session: ${this.id} tab: ${tabIndex}): ${(0, utils_1.hideAuth)(url)}`);
626
+ if (this.incognito) {
627
+ this.context = await this.browser.createBrowserContext();
628
+ }
629
+ else {
630
+ this.context = this.browser.defaultBrowserContext();
631
+ }
632
+ if (this.overridePermissions.length) {
633
+ await this.context.overridePermissions(new URL(url).origin, this.overridePermissions);
634
+ }
635
+ const page = await this.getNewPage(tabIndex);
636
+ await page.setBypassCSP(true);
637
+ if (this.userAgent) {
638
+ await page.setUserAgent(this.userAgent);
639
+ }
640
+ await Promise.all(Object.keys(this.exposedFunctions).map(async (name) => await page.exposeFunction(name, (...args) => this.exposedFunctions[name](...args))));
641
+ // Export config to page.
642
+ let cmd = this.setupPageCmd(index, tabIndex, url);
643
+ if (this.localStorage) {
644
+ log.debug('Using localStorage:', this.localStorage);
645
+ Object.entries(this.localStorage).map(([key, value]) => {
646
+ cmd += `localStorage.setItem('${key}', '${JSON.stringify(value)}');\n`;
647
+ });
648
+ }
649
+ if (this.sessionStorage) {
650
+ log.debug('Using sessionStorage:', this.sessionStorage);
651
+ Object.entries(this.sessionStorage).map(([key, value]) => {
652
+ cmd += `sessionStorage.setItem('${key}', '${JSON.stringify(value)}');\n`;
653
+ });
654
+ }
655
+ log.debug('init command:', cmd);
656
+ await page.evaluateOnNewDocument(cmd);
657
+ // Clear cookies.
658
+ if (this.clearCookies) {
659
+ try {
660
+ const client = await page.target().createCDPSession();
661
+ await client.send('Network.clearBrowserCookies');
662
+ }
663
+ catch (err) {
664
+ log.error(`clearCookies error: ${err.stack}`);
665
+ }
666
+ }
667
+ // Load page script.
668
+ {
669
+ const filePath = (0, utils_1.resolvePackagePath)('@vpalmisano/webrtcperf-js');
670
+ if (!fs_1.default.existsSync(filePath)) {
671
+ throw new Error(`@vpalmisano/webrtcperf-js script not found: ${filePath}`);
672
+ }
673
+ log.debug(`loading @vpalmisano/webrtcperf-js script from: ${filePath}`);
674
+ await page.evaluateOnNewDocument(fs_1.default.readFileSync(filePath, 'utf8'));
675
+ }
676
+ // Execute external script(s).
677
+ if (this.scriptPath) {
678
+ if (this.scriptPath.startsWith('base64:gzip:')) {
679
+ const data = Buffer.from(this.scriptPath.replace('base64:gzip:', ''), 'base64');
680
+ const code = (0, zlib_1.gunzipSync)(data).toString();
681
+ log.debug(`loading script from ${code.length} bytes`);
682
+ await page.evaluateOnNewDocument(code);
683
+ }
684
+ else {
685
+ for (const filePath of this.scriptPath.split(',')) {
686
+ if (!filePath.trim()) {
687
+ continue;
688
+ }
689
+ if (filePath.startsWith('http')) {
690
+ log.debug(`loading custom script from url: ${filePath}`);
691
+ const res = await (0, utils_1.downloadUrl)(filePath);
692
+ if (!res?.data) {
693
+ throw new Error(`Failed to download script from: ${filePath}`);
694
+ }
695
+ await page.evaluateOnNewDocument(res.data);
696
+ }
697
+ else {
698
+ if (!fs_1.default.existsSync(filePath)) {
699
+ log.warn(`custom script not found: ${filePath}`);
700
+ continue;
701
+ }
702
+ log.debug(`loading custom script from file: ${filePath}`);
703
+ await page.evaluateOnNewDocument(fs_1.default.readFileSync(filePath, 'utf8'));
704
+ }
705
+ }
706
+ }
707
+ }
708
+ page.on('dialog', async (dialog) => {
709
+ log.debug(`page ${index + 1} dialog ${dialog.type()}: ${dialog.message()}`);
710
+ try {
711
+ await dialog.accept();
712
+ }
713
+ catch (err) {
714
+ log.debug(`dialog accept error: ${err.message}`);
715
+ }
716
+ try {
717
+ await dialog.dismiss();
718
+ }
719
+ catch (err) {
720
+ log.debug(`dialog dismiss error: ${err.message}`);
721
+ }
722
+ });
723
+ page.once('close', () => {
724
+ log.debug(`page ${index + 1} closed`);
725
+ this.pages.delete(index);
726
+ this.httpResourcesStats.delete(index);
727
+ this.pagesMetrics.delete(index);
728
+ if (saveFile) {
729
+ saveFile.close().catch(err => {
730
+ log.error(`saveFile close error: ${err.stack}`);
731
+ });
732
+ saveFile = undefined;
733
+ }
734
+ if (this.browser && this.running) {
735
+ setTimeout(() => this.openPage(index).catch(err => log.error(`openPage after close error: ${err.stack}`)), 1000);
736
+ }
737
+ });
738
+ // Enable request interception.
739
+ let setRequestInterceptionState = true;
740
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
741
+ const pageCDPSession = page._client();
742
+ await pageCDPSession.send('Network.setBypassServiceWorker', {
743
+ bypass: true,
744
+ });
745
+ const interceptManager = new puppeteer_intercept_and_modify_requests_1.RequestInterceptionManager(pageCDPSession, {
746
+ onError: error => {
747
+ log.error('Request interception error:', error);
748
+ },
749
+ });
750
+ const interceptions = [];
751
+ // Blocked URLs.
752
+ this.blockedUrls.forEach(blockedUrl => {
753
+ interceptions.push({
754
+ urlPattern: blockedUrl,
755
+ modifyRequest: () => ({ errorReason: 'BlockedByClient' }),
756
+ });
757
+ });
758
+ // Add extra headers.
759
+ if (this.extraHeaders) {
760
+ Object.entries(this.extraHeaders).forEach(([url, obj]) => {
761
+ const headers = Object.entries(obj).map(([name, value]) => ({
762
+ name,
763
+ value,
764
+ }));
765
+ interceptions.push({
766
+ urlPattern: url,
767
+ modifyRequest: ({ event }) => {
768
+ log.debug(`adding extraHeaders in: ${event.request.url}`, headers);
769
+ return { headers };
770
+ },
771
+ });
772
+ });
773
+ }
774
+ // Response modifiers.
775
+ Object.entries(this.responseModifiers).forEach(([url, replacements]) => {
776
+ interceptions.push({
777
+ urlPattern: url,
778
+ modifyResponse: async ({ event, body }) => {
779
+ const responseHeaders = event.responseHeaders || [];
780
+ for (const { search, replace, file, headers } of replacements) {
781
+ if (search && replace) {
782
+ log.debug(`using responseModifiers in: ${event.request.url}: ${search.toString()} => ${replace}`);
783
+ body = body?.replace(search, replace);
784
+ }
785
+ else if (file) {
786
+ log.debug(`using responseModifiers in: ${event.request.url}: ${file}`);
787
+ body = await fs_1.default.promises.readFile(file, 'utf8');
788
+ }
789
+ if (headers) {
790
+ for (const [name, value] of Object.entries(headers)) {
791
+ responseHeaders.push({
792
+ name,
793
+ value,
794
+ });
795
+ }
796
+ }
797
+ }
798
+ return { body, responseHeaders };
799
+ },
800
+ });
801
+ });
802
+ await interceptManager.intercept(...interceptions);
803
+ // Download responses.
804
+ if (this.downloadResponses.length) {
805
+ page.on('response', async (response) => {
806
+ if (!response.ok())
807
+ return;
808
+ const url = response.url();
809
+ for (const { urlPattern, output, append } of this.downloadResponses) {
810
+ if (!urlPattern.test(url))
811
+ continue;
812
+ try {
813
+ const data = await response.buffer();
814
+ if (data.byteLength > 0) {
815
+ if (append) {
816
+ const savePath = output.replaceAll('${id}', this.id.toString());
817
+ if (!fs_1.default.existsSync(path_1.default.dirname(savePath))) {
818
+ await fs_1.default.promises.mkdir(path_1.default.dirname(output), { recursive: true });
819
+ }
820
+ log.debug(`appending response body ${data.byteLength} to: ${savePath}`);
821
+ await fs_1.default.promises.appendFile(savePath, data);
822
+ }
823
+ else {
824
+ if (!fs_1.default.existsSync(output)) {
825
+ await fs_1.default.promises.mkdir(output, { recursive: true });
826
+ }
827
+ const savePath = path_1.default.join(output, `${path_1.default.basename(new URL(url).pathname)}`);
828
+ log.debug(`saving response body ${data.byteLength} to: ${savePath}`);
829
+ await fs_1.default.promises.writeFile(savePath, data);
830
+ }
831
+ }
832
+ }
833
+ catch (err) {
834
+ log.error(`downloadResponses error: ${err.stack}`);
835
+ }
836
+ }
837
+ });
838
+ }
839
+ // Allow to change the setRequestInterception state from page.
840
+ const setRequestInterceptionFunction = async (value) => {
841
+ if (value === setRequestInterceptionState) {
842
+ return;
843
+ }
844
+ log.debug(`setRequestInterception to ${value}`);
845
+ try {
846
+ if (!value) {
847
+ await interceptManager.disable();
848
+ }
849
+ else {
850
+ await interceptManager.enable();
851
+ }
852
+ setRequestInterceptionState = value;
853
+ }
854
+ catch (err) {
855
+ log.error(`setRequestInterception error: ${err.stack}`);
856
+ }
857
+ };
858
+ await page.exposeFunction('setRequestInterception', setRequestInterceptionFunction);
859
+ await page.exposeFunction('jsonFetch', async (options, cacheKey = '', cacheTimeout = 0) => {
860
+ if (cacheKey) {
861
+ const ret = Session.jsonFetchCache.get(cacheKey);
862
+ if (ret) {
863
+ return ret;
864
+ }
865
+ }
866
+ try {
867
+ if (options.validStatuses) {
868
+ options.validateStatus = status => options.validStatuses.includes(status);
869
+ }
870
+ const { status, data, headers } = await (0, axios_1.default)(options);
871
+ if (options.responseType === 'stream') {
872
+ if (options.downloadPath && !fs_1.default.existsSync(options.downloadPath)) {
873
+ log.debug(`jsonFetch saving file to: ${options.downloadPath}`, headers['content-disposition']);
874
+ await fs_1.default.promises.mkdir(path_1.default.dirname(options.downloadPath), {
875
+ recursive: true,
876
+ });
877
+ const writer = fs_1.default.createWriteStream(options.downloadPath);
878
+ await new Promise((resolve, reject) => {
879
+ writer.on('error', err => reject(err));
880
+ writer.on('close', () => resolve());
881
+ data.pipe(writer);
882
+ });
883
+ }
884
+ if (cacheKey) {
885
+ Session.jsonFetchCache.set(cacheKey, { status }, cacheTimeout);
886
+ }
887
+ return { status, headers };
888
+ }
889
+ else {
890
+ if (cacheKey) {
891
+ Session.jsonFetchCache.set(cacheKey, { status, data }, cacheTimeout);
892
+ }
893
+ return { status, headers, data };
894
+ }
895
+ }
896
+ catch (err) {
897
+ const error = err.message;
898
+ log.warn(`jsonFetch error: ${error}`);
899
+ return { status: 500, error };
900
+ }
901
+ });
902
+ await page.exposeFunction('readLocalFile', (filePath, encoding) => {
903
+ filePath = path_1.default.resolve(process.cwd(), filePath);
904
+ return fs_1.default.promises.readFile(filePath, encoding);
905
+ });
906
+ // PeerConnectionExternal
907
+ await page.exposeFunction('createPeerConnectionExternal',
908
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
909
+ async (options) => {
910
+ const pc = new utils_1.PeerConnectionExternal(options);
911
+ return { id: pc.id };
912
+ });
913
+ await page.exposeFunction('callPeerConnectionExternalMethod',
914
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
915
+ async (id, name, arg) => {
916
+ const pc = utils_1.PeerConnectionExternal.get(id);
917
+ if (pc) {
918
+ return pc[name](arg);
919
+ }
920
+ });
921
+ // Simulate keypress
922
+ await page.exposeFunction('keypressText', async (selector, text, delay = 20) => {
923
+ await page.type(selector, text, { delay });
924
+ });
925
+ // Simulate mouse clicks
926
+ await page.exposeFunction('mouseClick', async (selector, x = 0, y = 0) => {
927
+ await page.click(selector, { offset: { x, y } });
928
+ });
929
+ const lorem = new lorem_ipsum_1.LoremIpsum({
930
+ sentencesPerParagraph: {
931
+ max: 4,
932
+ min: 1,
933
+ },
934
+ wordsPerSentence: {
935
+ max: 16,
936
+ min: 2,
937
+ },
938
+ });
939
+ await page.exposeFunction('loremIpsum', (count = 1) => lorem.generateSentences(count));
940
+ await page.exposeFunction('keypressRandomText', async (selector, count = 1, prefix = '', suffix = '', delay = 0) => {
941
+ const c = prefix + lorem.generateSentences(count) + suffix;
942
+ const frames = await page.frames();
943
+ for (const frame of frames) {
944
+ const el = await frame.$(selector);
945
+ if (el) {
946
+ await el.focus();
947
+ await frame.type(selector, c, { delay });
948
+ }
949
+ }
950
+ });
951
+ await page.exposeFunction('uploadFileFromUrl', async (fileUrl, selector) => {
952
+ const filename = (0, utils_1.sha256)(fileUrl) + '.' + fileUrl.split('.').slice(-1)[0];
953
+ const filePath = path_1.default.join(os_1.default.homedir(), '.webrtcperf/uploads', filename);
954
+ if (!fs_1.default.existsSync(filePath)) {
955
+ await (0, utils_1.downloadUrl)(fileUrl, undefined, filePath);
956
+ }
957
+ log.debug(`uploadFileFromUrl: ${filePath}`);
958
+ const frames = await page.frames();
959
+ for (const frame of frames) {
960
+ const el = await frame.$(selector);
961
+ if (el) {
962
+ await el.uploadFile(filePath);
963
+ break;
964
+ }
965
+ }
966
+ });
967
+ // add extra styles
968
+ if (this.extraCSS) {
969
+ log.debug(`Add extraCSS: ${this.extraCSS}`);
970
+ try {
971
+ await page.evaluateOnNewDocument((css) => {
972
+ document.addEventListener('DOMContentLoaded', () => {
973
+ const style = document.createElement('style');
974
+ style.setAttribute('id', 'webrtcperf-extra-style');
975
+ style.setAttribute('type', 'text/css');
976
+ style.innerHTML = css;
977
+ document.head.appendChild(style);
978
+ });
979
+ }, this.extraCSS.replace(/important/g, '!important'));
980
+ }
981
+ catch (err) {
982
+ log.error(`Add extraCSS error: ${err.stack}`);
983
+ }
984
+ }
985
+ // add cookies
986
+ if (this.cookies) {
987
+ try {
988
+ await page.setCookie(...this.cookies);
989
+ }
990
+ catch (err) {
991
+ log.error(`Set cookies error: ${err.stack}`);
992
+ }
993
+ }
994
+ // Page logs and errors.
995
+ if (this.pageLogPath) {
996
+ try {
997
+ await fs_1.default.promises.mkdir(path_1.default.dirname(this.pageLogPath), {
998
+ recursive: true,
999
+ });
1000
+ saveFile = await fs_1.default.promises.open(this.pageLogPath, 'a');
1001
+ }
1002
+ catch (err) {
1003
+ log.error(`error opening page log file: ${this.pageLogPath}: ${err.stack}`);
1004
+ }
1005
+ }
1006
+ await page.exposeFunction('webrtcperf_serializedConsoleLog', async (type, text) => {
1007
+ if (this.showPageLog || saveFile) {
1008
+ try {
1009
+ await this.onPageMessage(index, type, text, saveFile);
1010
+ }
1011
+ catch (err) {
1012
+ log.error(`serializedConsoleLog error: ${err.stack}`);
1013
+ }
1014
+ }
1015
+ });
1016
+ if (this.showPageLog || saveFile) {
1017
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1018
+ page.on('pageerror', async (error) => {
1019
+ const text = `pageerror: ${error.message?.message || error.message} - ${error.message?.stack || error.stack}`;
1020
+ await this.onPageMessage(index, 'error', text, saveFile);
1021
+ });
1022
+ page.on('requestfailed', async (request) => {
1023
+ const err = (request.failure()?.errorText || '').trim();
1024
+ if (err === 'net::ERR_ABORTED') {
1025
+ return;
1026
+ }
1027
+ const text = `${request.method()} ${request.url()}: ${err}`;
1028
+ await this.onPageMessage(index, 'requestfailed', text, saveFile);
1029
+ });
1030
+ }
1031
+ await page.exposeFunction('webrtcperf_sdpParse', (sdpStr) => sdpTransform.parse(sdpStr));
1032
+ await page.exposeFunction('webrtcperf_sdpWrite', (sdp) => sdpTransform.write(sdp));
1033
+ await page.exposeFunction('webrtcperf_startFakeScreenshare', async () => {
1034
+ if (!this.browser)
1035
+ return;
1036
+ let screensharePage = page;
1037
+ if (!this.useFakeMedia) {
1038
+ if (!this.screensharePage) {
1039
+ screensharePage = this.screensharePage = await this.browser.newPage();
1040
+ await this.screensharePage.evaluateOnNewDocument(this.setupPageCmd(index, tabIndex, 'about:blank'));
1041
+ await this.screensharePage.evaluateOnNewDocument(fs_1.default.readFileSync((0, utils_1.resolvePackagePath)('@vpalmisano/webrtcperf-js'), 'utf8'));
1042
+ await screensharePage.exposeFunction('webrtcperf_keypressText', async (selector, text, delay = 20) => {
1043
+ await screensharePage.type(selector, text, { delay });
1044
+ });
1045
+ await screensharePage.exposeFunction('webrtcperf_keyPress', async (key) => {
1046
+ await screensharePage.keyboard.press(key);
1047
+ });
1048
+ await screensharePage.goto(`http${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/empty-page?auth=${this.serverSecret}&title=webrtcperf-screenshare`);
1049
+ }
1050
+ }
1051
+ await screensharePage.evaluate(() => webrtcperf.startFakeScreenshare());
1052
+ });
1053
+ await page.exposeFunction('webrtcperf_stopFakeScreenshare', async () => {
1054
+ if (!this.useFakeMedia && this.screensharePage) {
1055
+ await this.screensharePage.close();
1056
+ this.screensharePage = undefined;
1057
+ }
1058
+ else {
1059
+ await page.evaluate(() => webrtcperf.stopFakeScreenshare());
1060
+ }
1061
+ });
1062
+ // HTTP stats.
1063
+ const resourcesStats = {
1064
+ sentBytes: 0,
1065
+ recvBytes: 0,
1066
+ recvLatency: new stats_1.FastStats({ store_data: false }),
1067
+ wsSentBytes: 0,
1068
+ wsRecvBytes: 0,
1069
+ wsRecvLatency: new stats_1.FastStats({ store_data: false }),
1070
+ };
1071
+ this.httpResourcesStats.set(index, resourcesStats);
1072
+ const pendingRequests = new Map();
1073
+ pageCDPSession.on('Network.requestWillBeSent', event => {
1074
+ if (event.request.url.startsWith('data:'))
1075
+ return;
1076
+ const { requestId, request, timestamp } = event;
1077
+ const sentBytes = request.postDataEntries?.reduce((acc, entry) => acc + (entry.bytes?.length || 0), 0);
1078
+ //log.log('Network.requestWillBeSent', event.type, request.url, sentBytes)
1079
+ if (sentBytes)
1080
+ resourcesStats.sentBytes += sentBytes;
1081
+ pendingRequests.set(requestId, { url: request.url, timestamp });
1082
+ });
1083
+ pageCDPSession.on('Network.responseReceived', event => {
1084
+ const request = pendingRequests.get(event.requestId);
1085
+ if (!request)
1086
+ return;
1087
+ const { response } = event;
1088
+ if (response.fromDiskCache) {
1089
+ pendingRequests.delete(event.requestId);
1090
+ return;
1091
+ }
1092
+ resourcesStats.recvBytes += response.encodedDataLength;
1093
+ });
1094
+ pageCDPSession.on('Network.dataReceived', event => {
1095
+ const request = pendingRequests.get(event.requestId);
1096
+ if (!request)
1097
+ return;
1098
+ resourcesStats.recvBytes += event.encodedDataLength;
1099
+ });
1100
+ pageCDPSession.on('Network.loadingFinished', event => {
1101
+ const request = pendingRequests.get(event.requestId);
1102
+ if (!request)
1103
+ return;
1104
+ pendingRequests.delete(event.requestId);
1105
+ const { timestamp } = event;
1106
+ resourcesStats.recvLatency.push(timestamp - request.timestamp);
1107
+ });
1108
+ pageCDPSession.on('Network.webSocketCreated', event => {
1109
+ pendingRequests.set(event.requestId, { url: event.url, timestamp: Date.now() });
1110
+ });
1111
+ pageCDPSession.on('Network.webSocketHandshakeResponseReceived', event => {
1112
+ const request = pendingRequests.get(event.requestId);
1113
+ if (!request)
1114
+ return;
1115
+ pendingRequests.delete(event.requestId);
1116
+ resourcesStats.wsRecvLatency.push((Date.now() - request.timestamp) / 1000);
1117
+ });
1118
+ pageCDPSession.on('Network.webSocketFrameSent', event => {
1119
+ resourcesStats.wsSentBytes += event.response.payloadData.length;
1120
+ });
1121
+ pageCDPSession.on('Network.webSocketFrameReceived', event => {
1122
+ resourcesStats.wsRecvBytes += event.response.payloadData.length;
1123
+ });
1124
+ // hardware concurrency
1125
+ if (this.hardwareConcurrency) {
1126
+ const plugin = NavigatorHardwareConcurrency({ hardwareConcurrency: this.hardwareConcurrency });
1127
+ await plugin.onPageCreated(page);
1128
+ }
1129
+ log.debug(`Page ${index + 1} "${url}" loading`);
1130
+ const pageLoadTime = Date.now();
1131
+ // open the page url
1132
+ try {
1133
+ await page.goto(url, {
1134
+ waitUntil: 'domcontentloaded',
1135
+ timeout: 60 * 1000,
1136
+ });
1137
+ }
1138
+ catch (error) {
1139
+ log.error(`Page ${index + 1} "${url}" load error: ${error.stack}`);
1140
+ await page.close();
1141
+ return;
1142
+ }
1143
+ // add to pages map
1144
+ this.pages.set(index, page);
1145
+ log.debug(`Page ${index + 1} "${url}" loaded in ${(Date.now() - pageLoadTime) / 1000}s`);
1146
+ for (let i = 0; i < this.evaluateAfter.length; i++) {
1147
+ await page.evaluate(
1148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1149
+ this.evaluateAfter[i].pageFunction, ...this.evaluateAfter[i].args);
1150
+ }
1151
+ }
1152
+ async getNewPage(tabIndex) {
1153
+ log.debug(`getNewPage ${tabIndex}`);
1154
+ (0, assert_1.default)(this.context, 'NoBrowserContextCreated');
1155
+ return await this.context.newPage();
1156
+ }
1157
+ async onPageMessage(index, type, text, saveFile) {
1158
+ if (text.endsWith('net::ERR_BLOCKED_BY_CLIENT.Inspector')) {
1159
+ return;
1160
+ }
1161
+ const isBlocked = this.blockedUrls.some(blockedUrl => (type === 'requestfailed' || text.search('FetchError') !== -1) && text.search(blockedUrl) !== -1);
1162
+ if (isBlocked) {
1163
+ return;
1164
+ }
1165
+ const color = PageLogColors[type] || 'grey';
1166
+ const filter = this.pageLogFilter ? new RegExp(this.pageLogFilter, 'ig') : null;
1167
+ if (!filter || text.match(filter)) {
1168
+ const errorOrWarning = ['error', 'warning'].includes(type);
1169
+ const isWebrtcPerf = text.startsWith('[webrtcperf');
1170
+ if (saveFile) {
1171
+ if (!errorOrWarning && !isWebrtcPerf && text.length > 1024) {
1172
+ text = text.slice(0, 1024) + `... +${text.length - 1024} bytes`;
1173
+ }
1174
+ await saveFile.write(`${new Date().toISOString()} [page ${index}] (${type}) ${text}\n`);
1175
+ }
1176
+ if (this.showPageLog) {
1177
+ if (!errorOrWarning && !isWebrtcPerf && text.length > 256) {
1178
+ text = text.slice(0, 256) + `... +${text.length - 256} bytes`;
1179
+ }
1180
+ console.log((0, chalk_1.default) `{bold [page ${index}]} {${color} (${type}) ${text}}`);
1181
+ }
1182
+ if (type === 'error') {
1183
+ this.pageErrors += 1;
1184
+ }
1185
+ else if (type === 'warn') {
1186
+ this.pageWarnings += 1;
1187
+ }
1188
+ }
1189
+ }
1190
+ /**
1191
+ * updateStats
1192
+ */
1193
+ async updateStats() {
1194
+ if (!this.browser) {
1195
+ this.stats = {};
1196
+ return this.stats;
1197
+ }
1198
+ const collectedStats = {};
1199
+ try {
1200
+ const processStats = await (0, utils_1.getProcessStats)();
1201
+ Object.assign(collectedStats, {
1202
+ nodeCpu: processStats.cpu,
1203
+ nodeMemory: processStats.memory,
1204
+ });
1205
+ }
1206
+ catch (err) {
1207
+ log.error(`node getProcessStats error: ${err.stack}`);
1208
+ }
1209
+ try {
1210
+ const systemStats = (0, utils_1.getSystemStats)();
1211
+ if (systemStats) {
1212
+ collectedStats.usedCpu = systemStats.usedCpu;
1213
+ collectedStats.usedMemory = systemStats.usedMemory;
1214
+ collectedStats.usedGpu = systemStats.usedGpu;
1215
+ if (collectedStats.usedCpu > 80) {
1216
+ log.warn(`High system CPU usage: ${collectedStats.usedCpu.toFixed(2)}%`);
1217
+ }
1218
+ if (collectedStats.usedMemory > 80) {
1219
+ log.warn(`High system memory usage: ${collectedStats.usedMemory.toFixed(2)}%`);
1220
+ }
1221
+ }
1222
+ }
1223
+ catch (err) {
1224
+ log.error(`node getSystemStats error: ${err.stack}`);
1225
+ }
1226
+ const browserProcess = this.browser.process();
1227
+ if (browserProcess) {
1228
+ try {
1229
+ const processStats = await (0, utils_1.getProcessStats)(browserProcess.pid, true);
1230
+ Object.assign(collectedStats, processStats);
1231
+ }
1232
+ catch (err) {
1233
+ log.error(`getProcessStats error: ${err.stack}`);
1234
+ }
1235
+ }
1236
+ const pages = {};
1237
+ const peerConnections = {};
1238
+ const peerConnectionConnectionTime = {};
1239
+ const peerConnectionDisconnectionTime = {};
1240
+ const peerConnectionsCreated = {};
1241
+ const peerConnectionsClosed = {};
1242
+ const peerConnectionsConnected = {};
1243
+ const peerConnectionsDisconnected = {};
1244
+ const peerConnectionsFailed = {};
1245
+ const peerConnectionsDelay = {};
1246
+ const audioEndToEndDelayStats = {};
1247
+ const audioStartFrameDelayStats = {};
1248
+ const videoEndToEndDelayStats = {};
1249
+ const screenEndToEndDelayStats = {};
1250
+ const videoStartFrameDelayStats = {};
1251
+ const screenStartFrameDelayStats = {};
1252
+ const httpSentBytesStats = {};
1253
+ const httpRecvBytesStats = {};
1254
+ const httpRecvLatencyStats = {};
1255
+ const wsSentBytesStats = {};
1256
+ const wsRecvBytesStats = {};
1257
+ const wsRecvLatencyStats = {};
1258
+ const pageCpu = {};
1259
+ const pageMemory = {};
1260
+ const cpuPressureStats = {};
1261
+ const videoWidth = {};
1262
+ const videoHeight = {};
1263
+ const videoBufferedTime = {};
1264
+ const videoPlayingTime = {};
1265
+ const videoBufferingTime = {};
1266
+ const videoBufferingEvents = {};
1267
+ const throttleUpValuesRate = {};
1268
+ const throttleUpValuesDelay = {};
1269
+ const throttleUpValuesLoss = {};
1270
+ const throttleUpValuesQueue = {};
1271
+ const throttleDownValuesRate = {};
1272
+ const throttleDownValuesDelay = {};
1273
+ const throttleDownValuesLoss = {};
1274
+ const throttleDownValuesQueue = {};
1275
+ const customStats = {};
1276
+ await Promise.allSettled([...this.pages.entries()].map(async ([pageIndex, page]) => {
1277
+ try {
1278
+ // Collect stats from the page.
1279
+ const { peerConnectionStats, audioEndToEndDelay, videoEndToEndDelay, cpuPressure, videoStats, customMetrics, } = await page.evaluate(async () => ({
1280
+ peerConnectionStats: await webrtcperf.collectPeerConnectionStats(),
1281
+ audioEndToEndDelay: webrtcperf.collectAudioEndToEndStats(),
1282
+ videoEndToEndDelay: webrtcperf.collectVideoEndToEndStats(),
1283
+ cpuPressure: webrtcperf.collectCpuPressure(),
1284
+ videoStats: webrtcperf.collectVideoStats(),
1285
+ customMetrics: 'collectCustomMetrics' in window ? webrtcperf.collectCustomMetrics() : null,
1286
+ }));
1287
+ const { participantName } = peerConnectionStats;
1288
+ const httpResourcesStats = this.httpResourcesStats.get(pageIndex);
1289
+ // Get host from the first collected remote address.
1290
+ if (!peerConnectionStats.signalingHost && peerConnectionStats.stats.length) {
1291
+ const values = Object.values(peerConnectionStats.stats[0]);
1292
+ if (values.length) {
1293
+ peerConnectionStats.signalingHost = await (0, utils_1.resolveIP)(values[0].remoteAddress);
1294
+ }
1295
+ }
1296
+ const { stats, activePeerConnections, signalingHost } = peerConnectionStats;
1297
+ // Calculate stats keys.
1298
+ const hostKey = (0, rtcstats_1.rtcStatKey)({
1299
+ hostName: signalingHost,
1300
+ participantName,
1301
+ });
1302
+ const pageKey = (0, rtcstats_1.rtcStatKey)({
1303
+ pageIndex,
1304
+ hostName: signalingHost,
1305
+ participantName,
1306
+ });
1307
+ // Set pages counter.
1308
+ (0, utils_1.increaseKey)(pages, hostKey, 1);
1309
+ // Set peerConnections counters.
1310
+ (0, utils_1.increaseKey)(peerConnections, pageKey, activePeerConnections);
1311
+ (0, utils_1.increaseKey)(peerConnectionConnectionTime, pageKey, peerConnectionStats.peerConnectionConnectionTime);
1312
+ (0, utils_1.increaseKey)(peerConnectionDisconnectionTime, pageKey, peerConnectionStats.peerConnectionDisconnectionTime);
1313
+ (0, utils_1.increaseKey)(peerConnectionsCreated, pageKey, peerConnectionStats.peerConnectionsCreated);
1314
+ (0, utils_1.increaseKey)(peerConnectionsClosed, pageKey, peerConnectionStats.peerConnectionsClosed);
1315
+ (0, utils_1.increaseKey)(peerConnectionsConnected, pageKey, peerConnectionStats.peerConnectionsConnected);
1316
+ (0, utils_1.increaseKey)(peerConnectionsDisconnected, pageKey, peerConnectionStats.peerConnectionsDisconnected);
1317
+ (0, utils_1.increaseKey)(peerConnectionsFailed, pageKey, peerConnectionStats.peerConnectionsFailed);
1318
+ (0, utils_1.increaseKey)(peerConnectionsDelay, pageKey, peerConnectionStats.peerConnectionsDelay);
1319
+ // E2E stats.
1320
+ if (audioEndToEndDelay) {
1321
+ audioEndToEndDelayStats[pageKey] = audioEndToEndDelay.delay;
1322
+ audioStartFrameDelayStats[pageKey] = audioEndToEndDelay.startFrameDelay;
1323
+ }
1324
+ if (videoEndToEndDelay) {
1325
+ videoEndToEndDelayStats[pageKey] = videoEndToEndDelay.videoDelay;
1326
+ videoStartFrameDelayStats[pageKey] = videoEndToEndDelay.videoStartFrameDelay;
1327
+ screenEndToEndDelayStats[pageKey] = videoEndToEndDelay.screenDelay;
1328
+ screenStartFrameDelayStats[pageKey] = videoEndToEndDelay.screenStartFrameDelay;
1329
+ }
1330
+ // HTTP stats.
1331
+ if (httpResourcesStats) {
1332
+ if (httpResourcesStats.sentBytes > 0)
1333
+ httpSentBytesStats[pageKey] = httpResourcesStats.sentBytes;
1334
+ if (httpResourcesStats.recvBytes > 0)
1335
+ httpRecvBytesStats[pageKey] = httpResourcesStats.recvBytes;
1336
+ if (httpResourcesStats.recvLatency.length)
1337
+ httpRecvLatencyStats[pageKey] = httpResourcesStats.recvLatency.amean();
1338
+ if (httpResourcesStats.wsSentBytes > 0)
1339
+ wsSentBytesStats[pageKey] = httpResourcesStats.wsSentBytes;
1340
+ if (httpResourcesStats.wsRecvBytes > 0)
1341
+ wsRecvBytesStats[pageKey] = httpResourcesStats.wsRecvBytes;
1342
+ if (httpResourcesStats.wsRecvLatency.length)
1343
+ wsRecvLatencyStats[pageKey] = httpResourcesStats.wsRecvLatency.amean();
1344
+ }
1345
+ if (cpuPressure !== undefined)
1346
+ cpuPressureStats[pageKey] = cpuPressure;
1347
+ if (videoStats) {
1348
+ videoWidth[pageKey] = videoStats.width;
1349
+ videoHeight[pageKey] = videoStats.height;
1350
+ videoBufferedTime[pageKey] = videoStats.bufferedTime;
1351
+ videoPlayingTime[pageKey] = videoStats.playingTime;
1352
+ videoBufferingTime[pageKey] = videoStats.bufferingTime;
1353
+ videoBufferingEvents[pageKey] = videoStats.bufferingEvents;
1354
+ }
1355
+ // Collect RTC stats.
1356
+ for (const s of stats) {
1357
+ for (const [trackId, value] of Object.entries(s)) {
1358
+ try {
1359
+ (0, rtcstats_1.updateRtcStats)(collectedStats, pageIndex, trackId, value, signalingHost, participantName);
1360
+ }
1361
+ catch (err) {
1362
+ log.error(`updateRtcStats error for ${trackId}: ${err.stack}`, err);
1363
+ }
1364
+ }
1365
+ }
1366
+ // Collect custom metrics.
1367
+ if (customMetrics) {
1368
+ for (const [name, value] of Object.entries(customMetrics)) {
1369
+ if (!customStats[name]) {
1370
+ customStats[name] = {};
1371
+ }
1372
+ customStats[name][pageKey] = value;
1373
+ }
1374
+ }
1375
+ // Collect page metrics
1376
+ /* const metrics = await page.metrics()
1377
+ if (metrics.Timestamp) {
1378
+ const lastMetrics = this.pagesMetrics.get(pageIndex)
1379
+ if (lastMetrics?.Timestamp) {
1380
+ const elapsedTime = metrics.Timestamp - lastMetrics.Timestamp
1381
+ if (elapsedTime > 10) {
1382
+ const durationDiff =
1383
+ metricsTotalDuration(metrics) -
1384
+ metricsTotalDuration(lastMetrics)
1385
+ const usage = (100 * durationDiff) / elapsedTime
1386
+ pageCpu[pageKey] = usage
1387
+ pageMemory[pageKey] = (metrics.JSHeapUsedSize || 0) / 1e6
1388
+ this.pagesMetrics.set(pageIndex, metrics)
1389
+ }
1390
+ } else {
1391
+ this.pagesMetrics.set(pageIndex, metrics)
1392
+ }
1393
+ } */
1394
+ pageCpu[pageKey] = collectedStats.cpu / this.tabsPerSession;
1395
+ pageMemory[pageKey] = collectedStats.memory / this.tabsPerSession;
1396
+ // Collect throttle metrics
1397
+ const throttleUpValues = (0, throttler_1.getSessionThrottleValues)(this.throttleIndex, 'up');
1398
+ throttleUpValuesRate[pageKey] = throttleUpValues.rate || 0;
1399
+ throttleUpValuesDelay[pageKey] = throttleUpValues.delay || 0;
1400
+ throttleUpValuesLoss[pageKey] = throttleUpValues.loss || 0;
1401
+ throttleUpValuesQueue[pageKey] = throttleUpValues.queue || 0;
1402
+ const throttleDownValues = (0, throttler_1.getSessionThrottleValues)(this.throttleIndex, 'down');
1403
+ throttleDownValuesRate[pageKey] = throttleDownValues.rate || 0;
1404
+ throttleDownValuesDelay[pageKey] = throttleDownValues.delay || 0;
1405
+ throttleDownValuesLoss[pageKey] = throttleDownValues.loss || 0;
1406
+ throttleDownValuesQueue[pageKey] = throttleDownValues.queue || 0;
1407
+ }
1408
+ catch (err) {
1409
+ const error = err;
1410
+ if (error.message.includes('Execution context was destroyed, most likely because of a navigation.')) {
1411
+ log.warn(`collectPeerConnectionStats for page ${pageIndex} error: ${error.message}`);
1412
+ }
1413
+ else {
1414
+ log.error(`collectPeerConnectionStats for page ${pageIndex} error: ${error.stack}`);
1415
+ }
1416
+ }
1417
+ }));
1418
+ Object.assign(collectedStats, {
1419
+ pages,
1420
+ errors: this.pageErrors,
1421
+ warnings: this.pageWarnings,
1422
+ peerConnections,
1423
+ peerConnectionConnectionTime,
1424
+ peerConnectionDisconnectionTime,
1425
+ peerConnectionsConnected,
1426
+ peerConnectionsCreated,
1427
+ peerConnectionsClosed,
1428
+ peerConnectionsDisconnected,
1429
+ peerConnectionsFailed,
1430
+ peerConnectionsDelay,
1431
+ audioEndToEndDelay: audioEndToEndDelayStats,
1432
+ audioStartFrameDelay: audioStartFrameDelayStats,
1433
+ videoEndToEndDelay: videoEndToEndDelayStats,
1434
+ videoStartFrameDelay: videoStartFrameDelayStats,
1435
+ screenEndToEndDelay: screenEndToEndDelayStats,
1436
+ screenStartFrameDelay: screenStartFrameDelayStats,
1437
+ httpSentBytes: httpSentBytesStats,
1438
+ httpRecvBytes: httpRecvBytesStats,
1439
+ httpRecvLatency: httpRecvLatencyStats,
1440
+ wsSentBytes: wsSentBytesStats,
1441
+ wsRecvBytes: wsRecvBytesStats,
1442
+ wsRecvLatency: wsRecvLatencyStats,
1443
+ cpuPressure: cpuPressureStats,
1444
+ videoWidth,
1445
+ videoHeight,
1446
+ videoBufferedTime,
1447
+ videoPlayingTime,
1448
+ videoBufferingTime,
1449
+ videoBufferingEvents,
1450
+ pageCpu,
1451
+ pageMemory,
1452
+ throttleUpRate: throttleUpValuesRate,
1453
+ throttleUpDelay: throttleUpValuesDelay,
1454
+ throttleUpLoss: throttleUpValuesLoss,
1455
+ throttleUpQueue: throttleUpValuesQueue,
1456
+ throttleDownRate: throttleDownValuesRate,
1457
+ throttleDownDelay: throttleDownValuesDelay,
1458
+ throttleDownLoss: throttleDownValuesLoss,
1459
+ throttleDownQueue: throttleDownValuesQueue,
1460
+ ...customStats,
1461
+ });
1462
+ if (pages.size < this.pages.size) {
1463
+ log.warn(`updateStats collected pages ${pages.size} < ${this.pages.size}`);
1464
+ }
1465
+ this.stats = collectedStats;
1466
+ return this.stats;
1467
+ }
1468
+ /**
1469
+ * stop
1470
+ */
1471
+ async stop() {
1472
+ if (!this.running) {
1473
+ return;
1474
+ }
1475
+ this.running = false;
1476
+ log.debug(`${this.id} stop`);
1477
+ if (this.stopPortForwarder) {
1478
+ this.stopPortForwarder();
1479
+ }
1480
+ if (this.browser) {
1481
+ // close the opened tabs
1482
+ log.debug(`${this.id} closing ${this.pages.size} pages`);
1483
+ await Promise.allSettled([...this.pages.values()].map(page => {
1484
+ return page.close({ runBeforeUnload: true });
1485
+ }));
1486
+ if (this.pages.size > 0) {
1487
+ const now = Date.now();
1488
+ const maxWaitTime = 1000 * this.pages.size;
1489
+ while (this.pages.size > 0 && Date.now() - now < maxWaitTime) {
1490
+ log.debug(`${this.id} waiting for ${this.pages.size} pages to close`);
1491
+ await (0, utils_1.sleep)(200);
1492
+ }
1493
+ if (this.pages.size > 0) {
1494
+ log.warn(`${this.id} timeout closing ${this.pages.size} pages`);
1495
+ }
1496
+ }
1497
+ if (this.screensharePage) {
1498
+ await this.screensharePage.close();
1499
+ this.screensharePage = undefined;
1500
+ }
1501
+ this.browser.removeAllListeners();
1502
+ if (this.chromiumUrl) {
1503
+ log.debug(`${this.id} disconnect from browser`);
1504
+ try {
1505
+ await this.browser.disconnect();
1506
+ }
1507
+ catch (err) {
1508
+ log.warn(`${this.id} browser disconnect error: ${err.message}`);
1509
+ }
1510
+ }
1511
+ else {
1512
+ const pid = this.browser.process()?.pid;
1513
+ if (pid) {
1514
+ log.debug(`${this.id} closing browser (pid: ${pid})`);
1515
+ try {
1516
+ await this.browser.close();
1517
+ }
1518
+ catch (err) {
1519
+ log.error(`${this.id} browser close error: ${err.stack}`);
1520
+ }
1521
+ await (0, utils_1.waitStopProcess)(pid, 5000);
1522
+ }
1523
+ }
1524
+ this.pages.clear();
1525
+ this.pagesMetrics.clear();
1526
+ this.browser = undefined;
1527
+ }
1528
+ this.emit('stop', this.id);
1529
+ }
1530
+ /**
1531
+ * pageScreenshot
1532
+ * @param {number} pageIndex
1533
+ * @param {String} format The image format (png|jpeg|webp).
1534
+ * @return {String}
1535
+ */
1536
+ async pageScreenshot(pageIndex = 0, format = 'webp') {
1537
+ log.debug(`pageScreenshot ${this.id}-${pageIndex}`);
1538
+ const index = this.id + pageIndex;
1539
+ const page = this.pages.get(index);
1540
+ if (!page) {
1541
+ throw new Error(`Page ${index} not found`);
1542
+ }
1543
+ const filePath = `/tmp/screenshot-${index}.${format}`;
1544
+ await page.screenshot({
1545
+ path: filePath,
1546
+ fullPage: true,
1547
+ });
1548
+ return filePath;
1549
+ }
1550
+ }
1551
+ exports.Session = Session;
1552
+ //# sourceMappingURL=session.js.map