@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.
- package/LICENSE +661 -0
- package/README.md +296 -0
- package/app.min.js +2 -0
- package/build/src/app.d.ts +6 -0
- package/build/src/app.js +207 -0
- package/build/src/app.js.map +1 -0
- package/build/src/config.d.ts +104 -0
- package/build/src/config.js +880 -0
- package/build/src/config.js.map +1 -0
- package/build/src/generate-config-docs.d.ts +1 -0
- package/build/src/generate-config-docs.js +41 -0
- package/build/src/generate-config-docs.js.map +1 -0
- package/build/src/index.d.ts +9 -0
- package/build/src/index.js +26 -0
- package/build/src/index.js.map +1 -0
- package/build/src/media.d.ts +33 -0
- package/build/src/media.js +113 -0
- package/build/src/media.js.map +1 -0
- package/build/src/rtcstats.d.ts +302 -0
- package/build/src/rtcstats.js +418 -0
- package/build/src/rtcstats.js.map +1 -0
- package/build/src/server.d.ts +173 -0
- package/build/src/server.js +639 -0
- package/build/src/server.js.map +1 -0
- package/build/src/session.d.ts +277 -0
- package/build/src/session.js +1552 -0
- package/build/src/session.js.map +1 -0
- package/build/src/stats.d.ts +243 -0
- package/build/src/stats.js +1383 -0
- package/build/src/stats.js.map +1 -0
- package/build/src/utils.d.ts +249 -0
- package/build/src/utils.js +1220 -0
- package/build/src/utils.js.map +1 -0
- package/build/src/visqol.d.ts +6 -0
- package/build/src/visqol.js +61 -0
- package/build/src/visqol.js.map +1 -0
- package/build/src/vmaf.d.ts +83 -0
- package/build/src/vmaf.js +624 -0
- package/build/src/vmaf.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +129 -0
- package/src/app.ts +241 -0
- package/src/config.ts +852 -0
- package/src/generate-config-docs.ts +47 -0
- package/src/index.ts +9 -0
- package/src/media.ts +151 -0
- package/src/rtcstats.ts +507 -0
- package/src/server.ts +645 -0
- package/src/session.ts +1908 -0
- package/src/stats.ts +1668 -0
- package/src/utils.ts +1295 -0
- package/src/visqol.ts +62 -0
- 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
|