@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,1220 @@
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.FFProbeProcess = exports.PeerConnectionExternal = exports.Scheduler = exports.LoggerInterface = exports.Log = void 0;
40
+ exports.logger = logger;
41
+ exports.resolvePackagePath = resolvePackagePath;
42
+ exports.sha256 = sha256;
43
+ exports.getProcessStats = getProcessStats;
44
+ exports.getSocketStats = getSocketStats;
45
+ exports.getSystemStats = getSystemStats;
46
+ exports.startUpdateSystemStats = startUpdateSystemStats;
47
+ exports.stopTimers = stopTimers;
48
+ exports.sleep = sleep;
49
+ exports.startRandomActivateAudio = startRandomActivateAudio;
50
+ exports.stopRandomActivateAudio = stopRandomActivateAudio;
51
+ exports.randomActivateAudio = randomActivateAudio;
52
+ exports.downloadUrl = downloadUrl;
53
+ exports.uploadUrl = uploadUrl;
54
+ exports.hideAuth = hideAuth;
55
+ exports.registerExitHandler = registerExitHandler;
56
+ exports.unregisterExitHandler = unregisterExitHandler;
57
+ exports.runExitHandlersNow = runExitHandlersNow;
58
+ exports.checkChromeExecutable = checkChromeExecutable;
59
+ exports.clampMinMax = clampMinMax;
60
+ exports.runShellCommand = runShellCommand;
61
+ exports.resolveIP = resolveIP;
62
+ exports.stripColors = stripColors;
63
+ exports.systemGpuStats = systemGpuStats;
64
+ exports.toTitleCase = toTitleCase;
65
+ exports.getFiles = getFiles;
66
+ exports.toPrecision = toPrecision;
67
+ exports.getDefaultNetworkInterface = getDefaultNetworkInterface;
68
+ exports.checkNetworkInterface = checkNetworkInterface;
69
+ exports.portForwarder = portForwarder;
70
+ exports.pageScreenshot = pageScreenshot;
71
+ exports.enabledForSession = enabledForSession;
72
+ exports.increaseKey = increaseKey;
73
+ exports.chunkedPromiseAll = chunkedPromiseAll;
74
+ exports.maybeNumber = maybeNumber;
75
+ exports.ffprobe = ffprobe;
76
+ exports.buildIvfHeader = buildIvfHeader;
77
+ exports.ffmpeg = ffmpeg;
78
+ exports.analyzeColors = analyzeColors;
79
+ exports.waitStopProcess = waitStopProcess;
80
+ exports.getDockerLogsPath = getDockerLogsPath;
81
+ const browsers_1 = require("@puppeteer/browsers");
82
+ const child_process_1 = require("child_process");
83
+ const axios_1 = __importDefault(require("axios"));
84
+ const crypto_1 = require("crypto");
85
+ const dns = __importStar(require("dns"));
86
+ const form_data_1 = __importDefault(require("form-data"));
87
+ const fs_1 = __importStar(require("fs"));
88
+ const https_1 = require("https");
89
+ const ipaddrJs = __importStar(require("ipaddr.js"));
90
+ const net_1 = __importDefault(require("net"));
91
+ const node_cache_1 = __importDefault(require("node-cache"));
92
+ const OSUtils = __importStar(require("node-os-utils"));
93
+ const os_1 = __importStar(require("os"));
94
+ const path_1 = __importStar(require("path"));
95
+ const pidtree_1 = __importDefault(require("pidtree"));
96
+ const pidusage_1 = __importDefault(require("pidusage"));
97
+ const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
98
+ // eslint-disable-next-line
99
+ const ps = require('pidusage/lib/ps');
100
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
101
+ exports.Log = require('debug-level').Log;
102
+ function logger(name, options = {}) {
103
+ return new exports.Log(name, { splitLine: false, ...options });
104
+ }
105
+ class LoggerInterface {
106
+ name;
107
+ logInit(args) {
108
+ if (this.name) {
109
+ args.unshift(`[${this.name}]`);
110
+ }
111
+ }
112
+ debug(...args) {
113
+ this.logInit(args);
114
+ log.debug(...args);
115
+ }
116
+ info(...args) {
117
+ this.logInit(args);
118
+ log.info(...args);
119
+ }
120
+ warn(...args) {
121
+ this.logInit(args);
122
+ log.warn(...args);
123
+ }
124
+ error(...args) {
125
+ this.logInit(args);
126
+ log.error(...args);
127
+ }
128
+ log(...args) {
129
+ this.logInit(args);
130
+ log.log(...args);
131
+ }
132
+ }
133
+ exports.LoggerInterface = LoggerInterface;
134
+ const log = logger('webrtcperf:utils');
135
+ /**
136
+ * Resolves the absolute path from the package installation directory.
137
+ * @param relativePath The relative path.
138
+ * @returns The absolute path.
139
+ */
140
+ function resolvePackagePath(relativePath) {
141
+ if ('__nexe' in process) {
142
+ return relativePath;
143
+ }
144
+ if (process.env.WEBPACK) {
145
+ return path_1.default.join(path_1.default.dirname(__filename), relativePath);
146
+ }
147
+ const libPath = require.resolve(relativePath);
148
+ if (fs_1.default.existsSync(libPath)) {
149
+ return libPath;
150
+ }
151
+ for (const d of ['..', '../..']) {
152
+ const p = path_1.default.join(__dirname, d, relativePath);
153
+ if (fs_1.default.existsSync(p)) {
154
+ return require.resolve(p);
155
+ }
156
+ }
157
+ throw new Error(`resolvePackagePath: ${relativePath} not found`);
158
+ }
159
+ /**
160
+ * Calculates the sha256 sum.
161
+ * @param data The string input
162
+ */
163
+ function sha256(data) {
164
+ return (0, crypto_1.createHash)('sha256').update(data).digest('hex');
165
+ }
166
+ const ProcessStatsCache = new node_cache_1.default({ stdTTL: 5, checkperiod: 10 });
167
+ const ProcessChildrenCache = new node_cache_1.default({ stdTTL: 15, checkperiod: 15 });
168
+ /**
169
+ * Returns the process stats.
170
+ * @param pid The process pid
171
+ * @param children If process children should be taken into account.
172
+ * @returns
173
+ */
174
+ async function getProcessStats(pid = 0, children = false) {
175
+ const processPid = pid || process.pid;
176
+ let stat = ProcessStatsCache.get(processPid);
177
+ if (stat) {
178
+ return stat;
179
+ }
180
+ const pidStats = await (0, pidusage_1.default)(processPid);
181
+ if (pidStats) {
182
+ stat = {
183
+ cpu: pidStats.cpu,
184
+ memory: pidStats.memory / 1e6,
185
+ };
186
+ }
187
+ else {
188
+ stat = { cpu: 0, memory: 0 };
189
+ }
190
+ if (children) {
191
+ try {
192
+ let childrenPids = ProcessChildrenCache.get(processPid);
193
+ if (!childrenPids?.length) {
194
+ childrenPids = await (0, pidtree_1.default)(processPid);
195
+ if (childrenPids?.length) {
196
+ ProcessChildrenCache.set(processPid, childrenPids);
197
+ }
198
+ }
199
+ if (childrenPids?.length) {
200
+ const pidStats = await (0, pidusage_1.default)(childrenPids);
201
+ for (const p of childrenPids) {
202
+ if (pidStats[p]) {
203
+ stat.cpu += pidStats[p].cpu;
204
+ stat.memory += pidStats[p].memory / 1e6;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ catch (err) {
210
+ log.error(`getProcessStats children error: ${err.stack}`);
211
+ }
212
+ }
213
+ ProcessStatsCache.set(processPid, stat);
214
+ return stat;
215
+ }
216
+ async function getSocketStats(processPid) {
217
+ const stats = { recvBytes: 0, sendBytes: 0 };
218
+ try {
219
+ const { stdout } = await runShellCommand(`ss -nOHpti | { grep pid=${processPid} || true; }`);
220
+ for (const { groups } of stdout.matchAll(/bytes_sent:(?<sendBytes>\d+).+bytes_received:(?<recvBytes>\d+)/g)) {
221
+ if (!groups)
222
+ continue;
223
+ const recvBytes = parseInt(groups.recvBytes);
224
+ const sendBytes = parseInt(groups.sendBytes);
225
+ stats.recvBytes += recvBytes;
226
+ stats.sendBytes += sendBytes;
227
+ }
228
+ }
229
+ catch (err) {
230
+ log.error(`socketStats error: ${err.stack}`);
231
+ }
232
+ return stats;
233
+ }
234
+ // System stats.
235
+ const SystemStatsCache = new node_cache_1.default({ stdTTL: 30, checkperiod: 60 });
236
+ async function updateSystemStats() {
237
+ const [cpu, memInfo, gpuStats] = await Promise.all([OSUtils.cpu.free(10000), OSUtils.mem.info(), systemGpuStats()]);
238
+ const stat = {
239
+ usedCpu: 100 - cpu,
240
+ usedMemory: 100 - memInfo.freeMemPercentage,
241
+ usedGpu: gpuStats.gpu,
242
+ usedGpuMemory: gpuStats.mem,
243
+ };
244
+ SystemStatsCache.set('default', stat);
245
+ }
246
+ let systemStatsInterval = null;
247
+ function getSystemStats() {
248
+ if (!systemStatsInterval) {
249
+ startUpdateSystemStats();
250
+ }
251
+ return SystemStatsCache.get('default');
252
+ }
253
+ function startUpdateSystemStats() {
254
+ if (systemStatsInterval) {
255
+ return;
256
+ }
257
+ systemStatsInterval = setInterval(updateSystemStats, 5000);
258
+ }
259
+ function stopTimers() {
260
+ if (systemStatsInterval) {
261
+ clearInterval(systemStatsInterval);
262
+ systemStatsInterval = null;
263
+ }
264
+ }
265
+ /**
266
+ * Sleeps for the specified amount of time.
267
+ * @param ms
268
+ */
269
+ function sleep(ms) {
270
+ return new Promise(resolve => setTimeout(() => resolve(), ms));
271
+ }
272
+ let randomActivateAudioTimeoutId = null;
273
+ let randomActivateAudioRunning = false;
274
+ function startRandomActivateAudio(sessions, randomAudioPeriod, randomAudioProbability, randomAudioRange) {
275
+ if (randomActivateAudioRunning)
276
+ return;
277
+ randomActivateAudioRunning = true;
278
+ void randomActivateAudio(sessions, randomAudioPeriod, randomAudioProbability, randomAudioRange);
279
+ }
280
+ function stopRandomActivateAudio() {
281
+ randomActivateAudioRunning = false;
282
+ if (randomActivateAudioTimeoutId)
283
+ clearTimeout(randomActivateAudioTimeoutId);
284
+ }
285
+ /**
286
+ * Randomly activate audio from one tab at time.
287
+ * @param sessions The sessions Map
288
+ * @param randomAudioPeriod If set, the function will be called in loop
289
+ * @param randomAudioProbability The activation probability
290
+ * @param randomAudioRange The number of pages to include into the automation
291
+ */
292
+ async function randomActivateAudio(sessions, randomAudioPeriod, randomAudioProbability, randomAudioRange) {
293
+ if (!randomAudioPeriod || !randomActivateAudioRunning) {
294
+ return;
295
+ }
296
+ try {
297
+ let pages = [];
298
+ for (const session of sessions.values()) {
299
+ const sessionPages = [...session.pages.values()];
300
+ if (randomAudioRange) {
301
+ if (session.id > randomAudioRange) {
302
+ break;
303
+ }
304
+ sessionPages.splice(randomAudioRange - session.id);
305
+ }
306
+ pages = pages.concat(sessionPages);
307
+ }
308
+ // Remove pages with no audio tracks.
309
+ for (const [i, page] of pages.entries()) {
310
+ if (!page) {
311
+ continue;
312
+ }
313
+ let active = 0;
314
+ try {
315
+ active = await page.evaluate(() => getActiveAudioTracks().length);
316
+ }
317
+ catch (err) {
318
+ log.error(`randomActivateAudio error: ${err.stack}`);
319
+ }
320
+ if (!active) {
321
+ pages[i] = null;
322
+ }
323
+ }
324
+ const pagesWithAudio = pages.filter(p => !!p);
325
+ //
326
+ const index = Math.floor(Math.random() * pagesWithAudio.length);
327
+ const enable = Math.round(100 * Math.random()) <= randomAudioProbability;
328
+ log.debug('randomActivateAudio %j', {
329
+ pages: pagesWithAudio.length,
330
+ randomAudioProbability,
331
+ index,
332
+ enable,
333
+ });
334
+ for (const [i, page] of pagesWithAudio.entries()) {
335
+ try {
336
+ if (i === index) {
337
+ log.debug(`Changing audio in page ${i + 1}/${pagesWithAudio.length} (enable: ${enable})`);
338
+ await page.evaluate(async (enable) => {
339
+ if (typeof publisherSetMuted !== 'undefined') {
340
+ await publisherSetMuted(!enable);
341
+ }
342
+ else {
343
+ getActiveAudioTracks().forEach(track => {
344
+ track.enabled = enable;
345
+ // track.dispatchEvent(new Event('custom-enabled'));
346
+ });
347
+ }
348
+ }, enable);
349
+ }
350
+ else {
351
+ await page.evaluate(async () => {
352
+ if (typeof publisherSetMuted !== 'undefined') {
353
+ await publisherSetMuted(true);
354
+ }
355
+ else {
356
+ getActiveAudioTracks().forEach(track => {
357
+ track.enabled = false;
358
+ // track.dispatchEvent(new Event('custom-enabled'));
359
+ });
360
+ }
361
+ });
362
+ }
363
+ }
364
+ catch (err) {
365
+ log.error(`randomActivateAudio in page ${i + 1}/${pagesWithAudio.length} error: ${err.stack}`);
366
+ }
367
+ }
368
+ }
369
+ catch (err) {
370
+ log.error(`randomActivateAudio error: ${err.stack}`);
371
+ }
372
+ finally {
373
+ if (randomActivateAudioRunning) {
374
+ const nextTime = randomAudioPeriod * (1 + Math.random());
375
+ if (randomActivateAudioTimeoutId)
376
+ clearTimeout(randomActivateAudioTimeoutId);
377
+ randomActivateAudioTimeoutId = setTimeout(randomActivateAudio, nextTime * 1000, sessions, randomAudioPeriod, randomAudioProbability, randomAudioRange);
378
+ }
379
+ }
380
+ }
381
+ /**
382
+ * Downloads the specified `url` to a local file or returning the file content
383
+ * as {@link DownloadData} object.
384
+ * @param url The remote url to download.
385
+ * @param auth The basic authentication (`user:password`).
386
+ * @param outputLocationPath The file output. If not specified, the download
387
+ * content will be returned as {@link DownloadData} instance.
388
+ * @param range The HTTP byte range to download (e.g. `10-100`).
389
+ * @param timeout The download timeout in milliseconds.
390
+ */
391
+ async function downloadUrl(url, auth, outputLocationPath, range, timeout = 60000) {
392
+ log.debug(`downloadUrl url=${url} ${outputLocationPath}`);
393
+ const authParts = auth?.split(':');
394
+ let writer = null;
395
+ if (outputLocationPath) {
396
+ await fs_1.default.promises.mkdir((0, path_1.dirname)(outputLocationPath), {
397
+ recursive: true,
398
+ });
399
+ writer = (0, fs_1.createWriteStream)(outputLocationPath);
400
+ }
401
+ const response = await (0, axios_1.default)({
402
+ method: 'get',
403
+ url,
404
+ auth: authParts
405
+ ? {
406
+ username: authParts[0],
407
+ password: authParts[1],
408
+ }
409
+ : undefined,
410
+ headers: range
411
+ ? {
412
+ Range: `bytes=${range}`,
413
+ }
414
+ : undefined,
415
+ timeout,
416
+ onDownloadProgress: event => {
417
+ log.debug(`downloadUrl fileUrl=${url} progress=${event.progress || event.bytes}`);
418
+ },
419
+ httpsAgent: new https_1.Agent({
420
+ rejectUnauthorized: false,
421
+ }),
422
+ responseType: writer ? 'stream' : 'text',
423
+ });
424
+ if (writer) {
425
+ return new Promise((resolve, reject) => {
426
+ if (!writer) {
427
+ return;
428
+ }
429
+ response.data.pipe(writer);
430
+ let error = null;
431
+ writer.once('error', err => {
432
+ error = err;
433
+ if (writer)
434
+ writer.close();
435
+ reject(err);
436
+ });
437
+ writer.once('close', () => {
438
+ if (!error) {
439
+ resolve();
440
+ }
441
+ });
442
+ });
443
+ }
444
+ else {
445
+ /* log.debug(`downloadUrl ${response.data.length} bytes, headers=${
446
+ JSON.stringify(response.headers)}`); */
447
+ let start = 0;
448
+ let end = 0;
449
+ let total = 0;
450
+ if (response.headers['content-range']) {
451
+ log.debug(`downloadUrl ${response.data.length} bytes, content-range=${response.headers['content-range']}`);
452
+ const contentRange = response.headers['content-range'].split('/');
453
+ const rangeParts = contentRange[0].split('-');
454
+ total = parseInt(contentRange[1]);
455
+ if (rangeParts.length === 2) {
456
+ start = parseInt(rangeParts[0]);
457
+ end = parseInt(rangeParts[1]);
458
+ }
459
+ else if (contentRange[0].startsWith('-')) {
460
+ end = parseInt(rangeParts[0]);
461
+ }
462
+ else if (contentRange[0].endsWith('-')) {
463
+ start = parseInt(rangeParts[0]);
464
+ end = total;
465
+ }
466
+ }
467
+ return {
468
+ data: response.data,
469
+ start,
470
+ end,
471
+ total,
472
+ };
473
+ }
474
+ }
475
+ /**
476
+ * Uploads the file to the specified `url`.
477
+ * @param filePath The file path to upload.
478
+ * @param url The remote url to upload.
479
+ * @param auth The basic authentication (`user:password`).
480
+ */
481
+ async function uploadUrl(filePath, url, auth) {
482
+ log.debug(`uploadUrl ${filePath} to ${url}`);
483
+ const authParts = auth?.split(':');
484
+ const formData = new form_data_1.default();
485
+ formData.append('file', fs_1.default.createReadStream(filePath));
486
+ const response = await (0, axios_1.default)({
487
+ method: 'post',
488
+ url,
489
+ auth: authParts
490
+ ? {
491
+ username: authParts[0],
492
+ password: authParts[1],
493
+ }
494
+ : undefined,
495
+ headers: formData.getHeaders(),
496
+ timeout: 3600 * 1000,
497
+ httpsAgent: new https_1.Agent({
498
+ rejectUnauthorized: false,
499
+ }),
500
+ responseType: 'text',
501
+ data: formData,
502
+ });
503
+ return response.data;
504
+ }
505
+ const HideAuthRegExp = new RegExp('(http[s]{0,1}://)(.+?:.+?@)', 'g');
506
+ /**
507
+ * Hides the authentication part from an HTTP url.
508
+ * @param data
509
+ */
510
+ function hideAuth(data) {
511
+ if (!data) {
512
+ return data;
513
+ }
514
+ return data.replace(HideAuthRegExp, '$1');
515
+ }
516
+ const exitHandlers = new Set();
517
+ /**
518
+ * Register an {@link ExitHandler} callback that will be executed at the
519
+ * nodejs process exit.
520
+ * @param exitHandler
521
+ */
522
+ function registerExitHandler(exitHandler) {
523
+ exitHandlers.add(exitHandler);
524
+ }
525
+ /**
526
+ * Un-registers the {@link ExitHandler} callback.
527
+ * @param exitHandler
528
+ */
529
+ function unregisterExitHandler(exitHandler) {
530
+ exitHandlers.delete(exitHandler);
531
+ }
532
+ const runExitHandlers = async (signal) => {
533
+ let i = 0;
534
+ for (const exitHandler of exitHandlers.values()) {
535
+ const id = `${i + 1}/${exitHandlers.size}`;
536
+ log.debug(`running exitHandler ${id}`);
537
+ try {
538
+ await exitHandler(signal);
539
+ log.debug(` exitHandler ${id} done`);
540
+ }
541
+ catch (err) {
542
+ log.error(`exitHandler ${id} error: ${err}`);
543
+ }
544
+ i++;
545
+ }
546
+ exitHandlers.clear();
547
+ };
548
+ let runExitHandlersPromise = null;
549
+ /**
550
+ * Runs the registered exit handlers immediately.
551
+ * @param signal The process exit signal.
552
+ */
553
+ async function runExitHandlersNow(signal) {
554
+ if (!runExitHandlersPromise) {
555
+ runExitHandlersPromise = runExitHandlers(signal);
556
+ }
557
+ await runExitHandlersPromise;
558
+ stopTimers();
559
+ }
560
+ const SIGNALS = [
561
+ 'beforeExit',
562
+ 'uncaughtException',
563
+ 'unhandledRejection',
564
+ 'SIGHUP',
565
+ 'SIGINT',
566
+ 'SIGQUIT',
567
+ 'SIGILL',
568
+ 'SIGTRAP',
569
+ 'SIGABRT',
570
+ 'SIGBUS',
571
+ 'SIGFPE',
572
+ 'SIGUSR1',
573
+ 'SIGSEGV',
574
+ 'SIGUSR2',
575
+ 'SIGTERM',
576
+ ];
577
+ process.setMaxListeners(process.getMaxListeners() + SIGNALS.length);
578
+ SIGNALS.forEach(event => process.once(event, async (signal) => {
579
+ if (signal instanceof Error) {
580
+ log.error(`Exit on error: ${signal.stack || signal.message}`);
581
+ }
582
+ else {
583
+ log.debug(`Exit on signal: ${signal}`);
584
+ }
585
+ await runExitHandlersNow(signal);
586
+ process.exit(0);
587
+ }));
588
+ /**
589
+ * Downloads the configured chrome executable if it doesn't exists into the
590
+ * `$HOME/.webrtcperf/chrome` directory.
591
+ * @returns The revision info.
592
+ */
593
+ async function checkChromeExecutable() {
594
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
595
+ const { loadConfig } = require('./config');
596
+ const config = loadConfig();
597
+ const cacheDir = path_1.default.join(os_1.default.homedir(), '.webrtcperf/chrome');
598
+ const fixSemVer = (v) => v.split('.').slice(0, 3).join('.');
599
+ const browsers = await (0, browsers_1.getInstalledBrowsers)({ cacheDir });
600
+ const revisions = browsers.map(b => fixSemVer(b.buildId));
601
+ const browser = browsers_1.Browser.CHROME;
602
+ revisions.sort((0, browsers_1.getVersionComparator)(browser));
603
+ log.debug(`Available chrome versions: ${revisions}`);
604
+ const requiredRevision = config.chromiumVersion;
605
+ if (!requiredRevision)
606
+ throw new Error('Chromium version not set');
607
+ if (!revisions.includes(fixSemVer(requiredRevision))) {
608
+ log.info(`Downloading chrome ${requiredRevision}...`);
609
+ let progress = 0;
610
+ await (0, browsers_1.install)({
611
+ browser,
612
+ buildId: requiredRevision,
613
+ cacheDir,
614
+ downloadProgressCallback: (downloadedBytes, totalBytes) => {
615
+ const cur = Math.round((100 * downloadedBytes) / totalBytes);
616
+ if (cur - progress > 1) {
617
+ progress = cur;
618
+ log.info(` ${progress}%`);
619
+ }
620
+ },
621
+ });
622
+ log.info(`Downloading chrome ${requiredRevision} done.`);
623
+ }
624
+ return (0, browsers_1.computeExecutablePath)({
625
+ browser,
626
+ cacheDir,
627
+ buildId: requiredRevision,
628
+ });
629
+ }
630
+ function clampMinMax(value, min, max) {
631
+ return Math.max(Math.min(value, max), min);
632
+ }
633
+ /** Runs the shell command asynchronously. */
634
+ async function runShellCommand(cmd, verbose = false, maxBuffer = 1024 * 1024) {
635
+ if (verbose)
636
+ log.debug(`runShellCommand cmd: ${cmd}`);
637
+ return new Promise((resolve, reject) => {
638
+ const p = (0, child_process_1.spawn)(cmd, {
639
+ shell: true,
640
+ stdio: ['ignore', 'pipe', 'pipe'],
641
+ detached: true,
642
+ });
643
+ let stdout = '';
644
+ let stderr = '';
645
+ p.stdout.on('data', data => {
646
+ if (maxBuffer && stdout.length > maxBuffer) {
647
+ stdout = stdout.slice(data.length);
648
+ }
649
+ stdout += data;
650
+ });
651
+ p.stderr.on('data', data => {
652
+ if (maxBuffer && stderr.length > maxBuffer) {
653
+ stderr = stderr.slice(data.length);
654
+ }
655
+ stderr += data;
656
+ });
657
+ p.once('error', err => reject(err));
658
+ p.once('close', code => {
659
+ if (code !== 0) {
660
+ reject(new Error(`runShellCommand cmd: ${cmd} failed with code ${code}: ${stderr}`));
661
+ }
662
+ else {
663
+ if (verbose)
664
+ log.debug(`runShellCommand cmd: ${cmd} done`, { stdout, stderr });
665
+ resolve({ stdout, stderr });
666
+ }
667
+ });
668
+ });
669
+ }
670
+ //
671
+ const ipCache = new Map();
672
+ /**
673
+ * Resolves the IP address hostname.
674
+ * @param ip The IP address.
675
+ * @param cacheTime The number of milliseconds to keep the resolved hostname
676
+ * into the memory cache.
677
+ * @returns The IP address hostname.
678
+ */
679
+ async function resolveIP(ip, cacheTime = 60 * 60 * 1000) {
680
+ if (!ip)
681
+ return '';
682
+ if (ipaddrJs.parse(ip).range() === 'private') {
683
+ return ip;
684
+ }
685
+ const timestamp = Date.now();
686
+ const ret = ipCache.get(ip);
687
+ if (!ret || timestamp - ret.timestamp > cacheTime) {
688
+ const host = await Promise.race([
689
+ sleep(1000),
690
+ dns.promises
691
+ .reverse(ip)
692
+ .then(hosts => {
693
+ if (hosts.length) {
694
+ log.debug(`resolveIP ${ip} -> ${hosts.join(', ')}`);
695
+ ipCache.set(ip, {
696
+ host: hosts[0],
697
+ // Keep the value for 10 min.
698
+ timestamp: timestamp + 10 * cacheTime,
699
+ });
700
+ return hosts[0];
701
+ }
702
+ else {
703
+ ipCache.set(ip, { host: ip, timestamp });
704
+ return ip;
705
+ }
706
+ })
707
+ .catch(err => {
708
+ log.debug(`resolveIP error: ${err.stack}`);
709
+ ipCache.set(ip, { host: ip, timestamp });
710
+ }),
711
+ ]);
712
+ return host || ip;
713
+ }
714
+ return ret?.host || ip;
715
+ }
716
+ /**
717
+ * Strips the console logs characters from the provided string.
718
+ * @param str The input string.
719
+ * @returns The strippped string.
720
+ */
721
+ function stripColors(str) {
722
+ // eslint-disable-next-line no-control-regex
723
+ return str.replace(/\x1B[[(?);]{0,2}(;?\d)*./g, '');
724
+ }
725
+ const nvidiaGpuPresent = fs_1.default.existsSync('/usr/bin/nvidia-smi') && fs_1.default.existsSync('/dev/dri');
726
+ const macOS = process.platform === 'darwin' && fs_1.default.existsSync('/usr/sbin/ioreg');
727
+ /**
728
+ * Returns the GPU usage.
729
+ *
730
+ * On Linux, the `nvidia-smi` should be installed if Nvidia card is used.
731
+ * @returns The GPU usage.
732
+ */
733
+ async function systemGpuStats() {
734
+ try {
735
+ if (nvidiaGpuPresent) {
736
+ const { stdout } = await runShellCommand('nvidia-smi --query-gpu=utilization.gpu,utilization.memory --format=csv');
737
+ const line = stdout.split('\n')[1].trim();
738
+ const [gpu, mem] = line.split(',').map(s => parseFloat(s.replace(' %', '')));
739
+ return { gpu, mem };
740
+ }
741
+ else if (macOS) {
742
+ const { stdout } = await runShellCommand('ioreg -r -d 1 -w 0 -c IOAccelerator | grep PerformanceStatistics\\"');
743
+ const stats = JSON.parse(stdout.trim().split(' = ')[1].replace(/=/g, ':'));
744
+ const gpu = stats['Device Utilization %'] || stats['GPU Activity(%)'] || 0;
745
+ return { gpu, mem: 0 };
746
+ }
747
+ }
748
+ catch (err) {
749
+ log.debug(`systemGpuStats error: ${err.stack}`);
750
+ }
751
+ return { gpu: 0, mem: 0 };
752
+ }
753
+ /**
754
+ * Schedules a function call at the specified time interval.
755
+ */
756
+ class Scheduler {
757
+ name;
758
+ interval;
759
+ callback;
760
+ verbose;
761
+ running = false;
762
+ last = 0;
763
+ errorSum = 0;
764
+ statsTimeoutId;
765
+ /**
766
+ * Scheduler.
767
+ * @param name Logging name.
768
+ * @param interval Update interval in seconds.
769
+ * @param callback Callback function.
770
+ * @param verbose Verbose logging.
771
+ */
772
+ constructor(name, interval, callback, verbose = false) {
773
+ this.name = name;
774
+ this.interval = interval * 1000;
775
+ this.callback = callback;
776
+ this.verbose = verbose;
777
+ log.debug(`[${this.name}-scheduler] constructor interval=${this.interval}ms`);
778
+ }
779
+ start() {
780
+ log.debug(`[${this.name}-scheduler] start`);
781
+ this.running = true;
782
+ this.scheduleNext();
783
+ }
784
+ stop() {
785
+ log.debug(`[${this.name}-scheduler] stop`);
786
+ this.running = false;
787
+ if (this.statsTimeoutId) {
788
+ clearTimeout(this.statsTimeoutId);
789
+ }
790
+ }
791
+ scheduleNext() {
792
+ if (!this.running) {
793
+ return;
794
+ }
795
+ const now = Date.now();
796
+ if (this.last) {
797
+ this.errorSum += clampMinMax(now - this.last - this.interval, -this.interval, this.interval);
798
+ if (this.verbose) {
799
+ log.debug(`[${this.name}-scheduler] last=${now - this.last}ms drift=${this.errorSum}ms`);
800
+ }
801
+ }
802
+ this.last = now;
803
+ this.statsTimeoutId = setTimeout(async () => {
804
+ try {
805
+ const now = Date.now();
806
+ await this.callback(now);
807
+ const elapsed = Date.now() - now;
808
+ if (elapsed > this.interval) {
809
+ log.warn(`[${this.name}-scheduler] callback elapsed=${elapsed}ms > ${this.interval}ms`);
810
+ }
811
+ else if (this.verbose) {
812
+ log.debug(`[${this.name}-scheduler] callback elapsed=${elapsed}ms`);
813
+ }
814
+ }
815
+ catch (err) {
816
+ log.error(`[${this.name}-scheduler] callback error: ${err.stack}`, err);
817
+ }
818
+ finally {
819
+ this.scheduleNext();
820
+ }
821
+ }, this.interval - this.errorSum / 2);
822
+ }
823
+ }
824
+ exports.Scheduler = Scheduler;
825
+ //
826
+ class PeerConnectionExternal {
827
+ id;
828
+ process;
829
+ static cache = new Map();
830
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
831
+ constructor(options) {
832
+ this.process = (0, child_process_1.spawn)('sleep', ['600']);
833
+ this.id = this.process.pid || -1;
834
+ log.debug(`PeerConnectionExternal contructor: ${this.id}`, options);
835
+ PeerConnectionExternal.cache.set(this.id, this);
836
+ this.process.stdout.on('data', data => {
837
+ log.debug(`PeerConnectionExternal stdout: ${data}`);
838
+ });
839
+ this.process.stderr.on('data', data => {
840
+ log.debug(`PeerConnectionExternal stderr: ${data}`);
841
+ });
842
+ this.process.on('close', code => {
843
+ log.debug(`PeerConnectionExternal process exited with code ${code}`);
844
+ PeerConnectionExternal.cache.delete(this.id);
845
+ });
846
+ }
847
+ static get(id) {
848
+ return PeerConnectionExternal.cache.get(id);
849
+ }
850
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
851
+ async createOffer(options) {
852
+ log.debug(`PeerConnectionExternal createOffer`, { options });
853
+ return {};
854
+ }
855
+ setLocalDescription(description) {
856
+ log.debug(`PeerConnectionExternal setLocalDescription`, description);
857
+ }
858
+ setRemoteDescription(description) {
859
+ log.debug(`PeerConnectionExternal setRemoteDescription`, description);
860
+ }
861
+ }
862
+ exports.PeerConnectionExternal = PeerConnectionExternal;
863
+ function toTitleCase(s) {
864
+ return s.charAt(0).toUpperCase() + s.slice(1);
865
+ }
866
+ async function getFiles(dir, ext) {
867
+ const dirs = await fs_1.default.promises.readdir(dir, { withFileTypes: true });
868
+ const files = await Promise.all(dirs.map(entry => {
869
+ const res = path_1.default.resolve(dir, entry.name);
870
+ return entry.isDirectory() ? getFiles(res, ext) : res;
871
+ }));
872
+ return Array.prototype.concat(...files).filter(f => f.endsWith(ext));
873
+ }
874
+ /**
875
+ * Format number to the specified precision.
876
+ * @param value value to format
877
+ * @param precision precision
878
+ */
879
+ function toPrecision(value, precision = 3) {
880
+ return (Math.round(value * 10 ** precision) / 10 ** precision).toFixed(precision);
881
+ }
882
+ async function getDefaultNetworkInterface() {
883
+ const { stdout } = await runShellCommand(`ip route | awk '/default/ {print $5; exit}' | tr -d ''`);
884
+ return stdout.trim();
885
+ }
886
+ async function checkNetworkInterface(device) {
887
+ await runShellCommand(`ip route | grep -q "dev ${device}"`);
888
+ }
889
+ async function portForwarder(port, listenInterface) {
890
+ if (!listenInterface) {
891
+ listenInterface = await getDefaultNetworkInterface();
892
+ }
893
+ const controller = new AbortController();
894
+ Object.entries((0, os_1.networkInterfaces)()).forEach(([iface, nets]) => {
895
+ if (listenInterface !== '0.0.0.0' && iface !== listenInterface)
896
+ return;
897
+ if (!nets)
898
+ return;
899
+ for (const n of nets) {
900
+ if (n.internal || n.address === '127.0.0.1' || n.family !== 'IPv4') {
901
+ continue;
902
+ }
903
+ const msg = `portForwarder on ${iface} (${n.address}:${port})`;
904
+ const server = net_1.default
905
+ .createServer(from => {
906
+ const to = net_1.default.createConnection({ host: '127.0.0.1', port });
907
+ from.once('error', err => {
908
+ log.error(`${msg} error: ${err.stack}`);
909
+ to.destroy();
910
+ });
911
+ to.once('error', err => {
912
+ log.error(`${msg} error: ${err.stack}`);
913
+ from.destroy();
914
+ });
915
+ from.pipe(to);
916
+ to.pipe(from);
917
+ })
918
+ .listen({ port, host: n.address, signal: controller.signal });
919
+ server.on('listening', () => {
920
+ log.debug(`${msg} listening`);
921
+ });
922
+ server.once('error', err => {
923
+ log.error(`${msg} error: ${err.stack}`);
924
+ });
925
+ }
926
+ });
927
+ return () => {
928
+ log.debug(`portForwarder on port ${port} stop`);
929
+ controller.abort();
930
+ };
931
+ }
932
+ async function pageScreenshot(url, filePath, width = 1920, height = 1024, selector = 'body', headers, extraCss) {
933
+ log.debug(`pageScreenshot ${url} -> ${filePath}`);
934
+ await fs_1.default.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
935
+ let executablePath = process.env.CHROMIUM_PATH;
936
+ if (!executablePath || !fs_1.default.existsSync(executablePath)) {
937
+ executablePath = await checkChromeExecutable();
938
+ }
939
+ const browser = await puppeteer_core_1.default.launch({
940
+ headless: true,
941
+ executablePath,
942
+ defaultViewport: {
943
+ width,
944
+ height,
945
+ deviceScaleFactor: 1,
946
+ isMobile: false,
947
+ hasTouch: false,
948
+ isLandscape: false,
949
+ },
950
+ args: [
951
+ '--no-sandbox',
952
+ '--disable-setuid-sandbox',
953
+ //'--remote-debugging-port=9222',
954
+ ],
955
+ });
956
+ const page = await browser.newPage();
957
+ if (headers) {
958
+ await page.setExtraHTTPHeaders(headers);
959
+ }
960
+ if (extraCss) {
961
+ await page.evaluateOnNewDocument((css) => {
962
+ document.addEventListener('DOMContentLoaded', () => {
963
+ const style = document.createElement('style');
964
+ style.setAttribute('id', 'webrtcperf-extra-style');
965
+ style.setAttribute('type', 'text/css');
966
+ style.innerHTML = css;
967
+ document.head.appendChild(style);
968
+ });
969
+ }, extraCss);
970
+ }
971
+ await page.goto(url, {
972
+ waitUntil: ['domcontentloaded', 'networkidle0'],
973
+ timeout: 60 * 1000,
974
+ });
975
+ try {
976
+ const element = await page.waitForSelector(selector, {
977
+ visible: true,
978
+ timeout: 15 * 1000,
979
+ });
980
+ if (!element) {
981
+ throw new Error(`pageScreenshot selector "${selector}" not found`);
982
+ }
983
+ await element.screenshot({ path: filePath });
984
+ }
985
+ catch (err) {
986
+ log.error(`pageScreenshot error: ${err.message}`);
987
+ }
988
+ finally {
989
+ await page.close();
990
+ await browser.close();
991
+ }
992
+ }
993
+ function enabledForSession(index, value) {
994
+ if (value === true || value === 'true') {
995
+ return true;
996
+ }
997
+ else if (value === false || value === 'false' || value === undefined) {
998
+ return false;
999
+ }
1000
+ else if (typeof value === 'string') {
1001
+ if (value.includes('-')) {
1002
+ const [start, end] = value.split('-').map(s => parseInt(s));
1003
+ if (isFinite(start) && index < start) {
1004
+ return false;
1005
+ }
1006
+ if (isFinite(end) && index > end) {
1007
+ return false;
1008
+ }
1009
+ return true;
1010
+ }
1011
+ else {
1012
+ const indexes = value
1013
+ .split(',')
1014
+ .map(s => s.trim())
1015
+ .filter(s => s.length)
1016
+ .map(s => parseInt(s));
1017
+ return indexes.includes(index);
1018
+ }
1019
+ }
1020
+ else if (index === value) {
1021
+ return true;
1022
+ }
1023
+ return false;
1024
+ }
1025
+ function increaseKey(o, key, value) {
1026
+ if (value === undefined || !isFinite(value))
1027
+ return;
1028
+ if (o[key] === undefined) {
1029
+ o[key] = 0;
1030
+ }
1031
+ o[key] += value;
1032
+ }
1033
+ async function chunkedPromiseAll(items, f, chunkSize = 1) {
1034
+ const results = Array(items.length);
1035
+ for (let index = 0; index < items.length; index += chunkSize) {
1036
+ await Promise.allSettled(items.slice(index, index + chunkSize).map(async (item, i) => {
1037
+ const res = await f(item, index + i);
1038
+ if (res !== undefined)
1039
+ results[index + i] = res;
1040
+ }));
1041
+ }
1042
+ return results;
1043
+ }
1044
+ function maybeNumber(s) {
1045
+ const n = parseFloat(s);
1046
+ return !isNaN(n) ? n : s;
1047
+ }
1048
+ var FFProbeProcess;
1049
+ (function (FFProbeProcess) {
1050
+ FFProbeProcess["Skip"] = "skip";
1051
+ FFProbeProcess["Stop"] = "stop";
1052
+ })(FFProbeProcess || (exports.FFProbeProcess = FFProbeProcess = {}));
1053
+ /**
1054
+ * It runs the ffprobe command to extract the video/video frames.
1055
+ * @param fpath The file path.
1056
+ * @param kind The kind of the media (video or audio).
1057
+ * @param entries Which entries to show.
1058
+ * @param filters Apply filters.
1059
+ * @param frameProcess A function to process the frame. The function return value could be:
1060
+ * - the modified frame object
1061
+ * - `FFProbeProcess.Skip` to skip the frame from the output
1062
+ * - `FFProbeProcess.Stop` to stop processing and immediately return the collected frames.
1063
+ */
1064
+ async function ffprobe(fpath, kind = 'video', entries = '', filters = '', frameProcess) {
1065
+ const cmd = `\
1066
+ exec ffprobe -loglevel error ${kind === 'video' ? '-select_streams v' : ''} -show_frames -print_format compact \
1067
+ ${entries ? `-show_entries ${entries}` : ''} \
1068
+ -f lavfi -i '${kind === 'video' ? '' : 'a'}movie=${fpath}${filters ? `,${filters}` : ''}'`;
1069
+ const frames = [];
1070
+ let stderr = '';
1071
+ let stopProcessing = false;
1072
+ return new Promise((resolve, reject) => {
1073
+ const p = (0, child_process_1.spawn)(cmd, {
1074
+ shell: true,
1075
+ stdio: ['ignore', 'pipe', 'pipe'],
1076
+ });
1077
+ p.stdout.on('data', data => {
1078
+ if (stopProcessing)
1079
+ return;
1080
+ const frame = data
1081
+ .toString()
1082
+ .split('|')
1083
+ .reduce((prev, cur) => {
1084
+ const [key, value] = cur.split('=');
1085
+ if (value && !key.startsWith('side_datum')) {
1086
+ prev[key.replace(/[:.]/g, '_')] = value;
1087
+ }
1088
+ return prev;
1089
+ }, {});
1090
+ if (frameProcess) {
1091
+ const newFrame = frameProcess(frame);
1092
+ if (newFrame === FFProbeProcess.Skip) {
1093
+ // Skip the frame.
1094
+ }
1095
+ else if (newFrame === FFProbeProcess.Stop) {
1096
+ stopProcessing = true;
1097
+ p.kill('SIGINT');
1098
+ }
1099
+ else {
1100
+ frames.push(newFrame);
1101
+ }
1102
+ }
1103
+ else {
1104
+ frames.push(frame);
1105
+ }
1106
+ });
1107
+ p.stderr.on('data', data => {
1108
+ stderr += data;
1109
+ });
1110
+ p.once('error', err => reject(err));
1111
+ p.once('close', code => {
1112
+ if (code !== 0 && !stopProcessing) {
1113
+ reject(new Error(`${cmd} failed with code ${code}: ${stderr}`));
1114
+ }
1115
+ else {
1116
+ resolve(frames);
1117
+ }
1118
+ });
1119
+ });
1120
+ }
1121
+ function buildIvfHeader(width = 1920, height = 1080, frameRate = 30, fourcc = 'MJPG') {
1122
+ const buffer = Buffer.alloc(32);
1123
+ buffer.write('DKIF', 0, 'utf8');
1124
+ buffer.writeUint16LE(0, 4); // version
1125
+ buffer.writeUint16LE(32, 6); // header size
1126
+ buffer.write(fourcc, 8, 'utf8');
1127
+ buffer.writeUint16LE(width, 12);
1128
+ buffer.writeUint16LE(height, 14);
1129
+ buffer.writeUint32LE(frameRate, 16);
1130
+ buffer.writeUint32LE(1, 20);
1131
+ buffer.writeUint32LE(0, 24); // frame count
1132
+ buffer.writeUint32LE(0, 28); // unused
1133
+ return buffer;
1134
+ }
1135
+ async function ffmpeg(command = 'video', processFn) {
1136
+ const port = 10000 + Math.floor(Math.random() * 10000);
1137
+ const cmd = `exec ffmpeg -hide_banner -loglevel warning ${command} zmq:tcp://127.0.0.1:${port}`;
1138
+ log.debug(`${cmd}`);
1139
+ let stderr = '';
1140
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1141
+ const zmq = require('zeromq');
1142
+ const sub = new zmq.Subscriber();
1143
+ const p = (0, child_process_1.spawn)(cmd, {
1144
+ shell: true,
1145
+ stdio: ['ignore', 'ignore', 'pipe'],
1146
+ });
1147
+ p.stderr.on('data', data => {
1148
+ stderr += data;
1149
+ });
1150
+ p.once('error', err => {
1151
+ sub.close();
1152
+ throw err;
1153
+ });
1154
+ p.once('close', code => {
1155
+ sub.close();
1156
+ if (code !== 0) {
1157
+ throw new Error(`${cmd} failed with code ${code}: ${stderr}`);
1158
+ }
1159
+ });
1160
+ sub.connect(`tcp://127.0.0.1:${port}`);
1161
+ sub.subscribe('');
1162
+ for await (const [msg] of sub) {
1163
+ await processFn(msg);
1164
+ }
1165
+ sub.close();
1166
+ }
1167
+ async function analyzeColors(fpath) {
1168
+ let Y = 0;
1169
+ let U = 0;
1170
+ let V = 0;
1171
+ let SAT = 0;
1172
+ let HUE = 0;
1173
+ let count = 0;
1174
+ await ffprobe(fpath, 'video', 'frame=lavfi.signalstats.YAVG,lavfi.signalstats.UAVG,lavfi.signalstats.VAVG,lavfi.signalstats.SATAVG,lavfi.signalstats.HUEAVG', 'signalstats', frame => {
1175
+ Y += parseFloat(frame.tag_lavfi_signalstats_YAVG);
1176
+ U += parseFloat(frame.tag_lavfi_signalstats_UAVG);
1177
+ V += parseFloat(frame.tag_lavfi_signalstats_VAVG);
1178
+ SAT += parseFloat(frame.tag_lavfi_signalstats_SATAVG);
1179
+ HUE += parseFloat(frame.tag_lavfi_signalstats_HUEAVG);
1180
+ count++;
1181
+ return FFProbeProcess.Skip;
1182
+ });
1183
+ return { YAvg: Y / count, UAvg: U / count, VAvg: V / count, SatAvg: SAT / count, HueAvg: HUE / count };
1184
+ }
1185
+ /**
1186
+ * Wait for the process to stop or kill it after the timeout.
1187
+ * @param pid The process pid
1188
+ * @param timeout The maximum wait time in milliseconds
1189
+ * @returns `true` if the process stopped, `false` if the process was killed.
1190
+ */
1191
+ async function waitStopProcess(pid, timeout = 5000) {
1192
+ log.debug(`waitStopProcess pid: ${pid} timeout: ${timeout}`);
1193
+ const now = Date.now();
1194
+ while (Date.now() - now < timeout) {
1195
+ try {
1196
+ process.kill(pid, 0);
1197
+ await sleep(Math.max(timeout / 10, 200));
1198
+ }
1199
+ catch {
1200
+ return true;
1201
+ }
1202
+ }
1203
+ log.warn(`waitStopProcess pid: ${pid} timeout`);
1204
+ try {
1205
+ process.kill(pid, 'SIGKILL');
1206
+ }
1207
+ catch {
1208
+ return true;
1209
+ }
1210
+ return false;
1211
+ }
1212
+ async function getDockerLogsPath() {
1213
+ const containerId = await fs_1.default.promises.readFile(`${os_1.default.homedir()}/.webrtcperf/docker.id`, 'utf-8');
1214
+ const logPath = `/var/lib/docker/containers/${containerId}/${containerId}-json.log`;
1215
+ if (!fs_1.default.existsSync(logPath)) {
1216
+ throw new Error(`docker logs path ${logPath} not found`);
1217
+ }
1218
+ return logPath;
1219
+ }
1220
+ //# sourceMappingURL=utils.js.map