@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,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
|