@vpalmisano/webrtcperf 4.5.1 → 4.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vpalmisano/webrtcperf",
3
- "version": "4.5.1",
3
+ "version": "4.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/vpalmisano/webrtcperf.git"
@@ -51,10 +51,10 @@
51
51
  },
52
52
  "license": "AGPL-3.0-or-later",
53
53
  "dependencies": {
54
- "@google/genai": "^1.21.0",
54
+ "@google/genai": "^1.22.0",
55
55
  "@puppeteer/browsers": "^2.10.10",
56
56
  "@vpalmisano/throttler": "0.0.14",
57
- "@vpalmisano/webrtcperf-js": "^1.1.14",
57
+ "@vpalmisano/webrtcperf-js": "^1.1.16",
58
58
  "axios": "^1.12.2",
59
59
  "chalk-template": "^1.1.2",
60
60
  "change-case": "^4.1.2",
@@ -62,7 +62,7 @@
62
62
  "convict": "^6.2.4",
63
63
  "convict-format-with-validator": "^6.2.0",
64
64
  "debug-level": "^4.1.1",
65
- "dockerode": "^4.0.8",
65
+ "dockerode": "^4.0.9",
66
66
  "express": "^5.1.0",
67
67
  "express-basic-auth": "^1.2.1",
68
68
  "fast-stats": "^0.0.7",
@@ -78,8 +78,8 @@
78
78
  "pidtree": "^0.6.0",
79
79
  "pidusage": "^4.0.1",
80
80
  "prom-client": "^15.1.3",
81
- "puppeteer": "^24.22.3",
82
- "puppeteer-core": "^24.22.3",
81
+ "puppeteer": "^24.23.0",
82
+ "puppeteer-core": "^24.23.0",
83
83
  "puppeteer-extra": "^3.3.6",
84
84
  "puppeteer-extra-plugin-stealth": "^2.11.2",
85
85
  "puppeteer-intercept-and-modify-requests": "^1.3.1",
@@ -90,7 +90,7 @@
90
90
  "yaml": "^2.8.1"
91
91
  },
92
92
  "devDependencies": {
93
- "@eslint/js": "^9.36.0",
93
+ "@eslint/js": "^9.37.0",
94
94
  "@types/basic-auth": "^1.1.3",
95
95
  "@types/compression": "^1.8.1",
96
96
  "@types/convict": "^6.1.6",
@@ -98,18 +98,18 @@
98
98
  "@types/dockerode": "^3.3.44",
99
99
  "@types/fast-stats": "^0.0.35",
100
100
  "@types/marked-terminal": "^6.1.1",
101
- "@types/node": "^20.19.17",
101
+ "@types/node": "^20.19.19",
102
102
  "@types/node-os-utils": "^1.3.4",
103
103
  "@types/pidusage": "^2.0.5",
104
104
  "@types/sdp-transform": "^2.15.0",
105
105
  "@types/sprintf-js": "^1.1.4",
106
106
  "@types/tar-fs": "^2.0.4",
107
107
  "@types/ws": "^8.18.1",
108
- "@typescript-eslint/eslint-plugin": "^8.44.1",
109
- "@typescript-eslint/parser": "^8.44.1",
108
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
109
+ "@typescript-eslint/parser": "^8.45.0",
110
110
  "@vpalmisano/typedoc-cookie-consent": "^0.0.4",
111
111
  "@vpalmisano/typedoc-plugin-ga": "^1.0.6",
112
- "eslint": "^9.36.0",
112
+ "eslint": "^9.37.0",
113
113
  "eslint-config-prettier": "^10.1.8",
114
114
  "eslint-import-resolver-typescript": "^4.4.4",
115
115
  "eslint-plugin-import": "^2.32.0",
@@ -121,16 +121,16 @@
121
121
  "terser-webpack-plugin": "^5.3.14",
122
122
  "ts-loader": "^9.5.4",
123
123
  "typedoc": "^0.28.13",
124
- "typescript": "^5.9.2",
125
- "typescript-eslint": "^8.44.1",
126
- "webpack": "^5.101.3",
124
+ "typescript": "^5.9.3",
125
+ "typescript-eslint": "^8.45.0",
126
+ "webpack": "^5.102.0",
127
127
  "webpack-cli": "^6.0.1",
128
128
  "webpack-node-externals": "^3.0.0",
129
129
  "yarn-upgrade-minor": "^1.0.13"
130
130
  },
131
131
  "optionalDependencies": {
132
132
  "chart.js": "^4.5.0",
133
- "chartjs-chart-error-bars": "^4.4.4",
133
+ "chartjs-chart-error-bars": "^4.4.5",
134
134
  "skia-canvas": "^3.0.8",
135
135
  "zeromq": "^6.5.0"
136
136
  }
package/src/app.ts CHANGED
@@ -292,7 +292,7 @@ async function main(): Promise<void> {
292
292
  }
293
293
 
294
294
  const stop = async () => {
295
- console.log('Exiting...')
295
+ log.info('Exiting...')
296
296
  await application.stop(true)
297
297
  }
298
298
  registerExitHandler(() => stop())
@@ -301,12 +301,15 @@ async function main(): Promise<void> {
301
301
 
302
302
  // Command line interface.
303
303
  if (process.stdin && process.stdin.setRawMode) {
304
- console.log('Press [q] to quit or [x] to exit immediately')
304
+ console.log('Press [e] to exit after the current test, [q] to exit immediately or [x] to force exit.')
305
305
  process.stdin.setRawMode(true)
306
306
  process.stdin.resume()
307
307
  process.stdin.on('data', async data => {
308
308
  log.debug('[stdin]', data[0])
309
- if (data[0] === 'q'.charCodeAt(0)) {
309
+ if (data[0] === 'e'.charCodeAt(0)) {
310
+ log.info(`Exiting after the current test (${i + 1}/${total})...`)
311
+ configs.splice(0)
312
+ } else if (data[0] === 'q'.charCodeAt(0)) {
310
313
  try {
311
314
  await stop()
312
315
  } catch (err: unknown) {
@@ -314,6 +317,7 @@ async function main(): Promise<void> {
314
317
  process.exit(1)
315
318
  }
316
319
  } else if (data[0] === 'x'.charCodeAt(0)) {
320
+ log.info('Force exiting...')
317
321
  process.exit(1)
318
322
  }
319
323
  })
package/src/config.ts CHANGED
@@ -442,13 +442,21 @@ on the console. Regexp string allowed.`,
442
442
  arg: 'page-log-path',
443
443
  },
444
444
  enableBrowserLogging: {
445
- doc: `It enables the Chromium browser logging for the specified session indexes. It requires the page log path option to be set. `,
445
+ doc: `It enables the Chromium browser logging for the specified session indexes. It requires the pageLogPath option to be set.`,
446
446
  format: 'index',
447
447
  nullable: true,
448
448
  default: '',
449
449
  env: 'ENABLE_BROWSER_LOGGING',
450
450
  arg: 'enable-browser-logging',
451
451
  },
452
+ enableRtpDump: {
453
+ doc: `It enables the RTP dump for the specified session indexes. It requires the enableBrowserLogging option to be set. The text2pcap utility is required to convert the RTP dump to pcap format.`,
454
+ format: 'index',
455
+ nullable: true,
456
+ default: '',
457
+ env: 'ENABLE_RTP_DUMP',
458
+ arg: 'enable-rtp-dump',
459
+ },
452
460
  userAgent: {
453
461
  doc: `The user agent override.`,
454
462
  format: String,
package/src/docker.ts CHANGED
@@ -4,6 +4,7 @@ import { loadConfig } from './config'
4
4
  import { runShellCommand } from '@vpalmisano/throttler'
5
5
  import os from 'os'
6
6
  import fs from 'fs'
7
+ import { Writable } from 'stream'
7
8
 
8
9
  const log = logger('webrtcperf:docker')
9
10
 
@@ -57,6 +58,10 @@ export async function runWithDocker(argv: string[]) {
57
58
  'VIDEO_CACHE_PATH=/root/.webrtcperf/cache',
58
59
  ]
59
60
 
61
+ if (process.env.URL) {
62
+ env.push(`URL=${process.env.URL}`)
63
+ }
64
+
60
65
  if (configs[0].prometheusPushgateway.startsWith('http://localhost')) {
61
66
  env.push('PROMETHEUS_PUSHGATEWAY=http://pushgateway:9091')
62
67
  }
@@ -110,7 +115,21 @@ export async function runWithDocker(argv: string[]) {
110
115
  })
111
116
 
112
117
  process.stdin.pipe(stream)
113
- stream.pipe(process.stdout)
118
+ stream.pipe(
119
+ new Writable({
120
+ write: (chunk, _encoding, callback) => {
121
+ const s = chunk.toString('utf-8').trim()
122
+ try {
123
+ const data = JSON.parse(s)
124
+ log.info(data.msg.trim())
125
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
126
+ } catch (_err) {
127
+ log.info(s)
128
+ }
129
+ callback()
130
+ },
131
+ }),
132
+ )
114
133
 
115
134
  await new Promise(resolve => {
116
135
  container.wait((err: Error, data: { StatusCode: number }) => {
package/src/plot.ts CHANGED
@@ -175,7 +175,19 @@ function groupByParticipant(rows: StatsRow[]) {
175
175
  return m
176
176
  }
177
177
 
178
+ /**
179
+ * It plots a detailed stats dashboard from a CSV file.
180
+ * @param statsFile The path to the CSV file containing the detailed stats.
181
+ * @param outFile The path to the output HTML file.
182
+ * @returns A promise that resolves when the plot is complete.
183
+ * @example
184
+ * ```bash
185
+ * webrtcperf --plot logs/detailed-stats.csv plot.html
186
+ * ```
187
+ */
178
188
  export async function plotDetailedStatsDashboard(statsFile: string, outFile = 'plot.html') {
189
+ log.info(`Plotting detailed stats from ${statsFile} to ${outFile}`)
190
+
179
191
  const rows = await parseStatsFile(statsFile)
180
192
  if (rows.length === 0) {
181
193
  log.warn('No stats found')
@@ -322,8 +334,16 @@ export async function plotDetailedStatsDashboard(statsFile: string, outFile = 'p
322
334
  build(graph.id, graph.title, graph.yLabel, graph.processValue, graph.width)
323
335
  })
324
336
 
325
- const [_, id, scenario] = path.basename(path.dirname(statsFile)).split('_')
326
- const description = formatThrottleRule(parseThrottleRule(scenario), true, false)
337
+ const dirName = path.basename(path.dirname(statsFile))
338
+ let title = dirName
339
+ let description = ''
340
+ try {
341
+ const [_, id, scenario] = dirName.split('_')
342
+ title = id
343
+ description = formatThrottleRule(parseThrottleRule(scenario), true, false)
344
+ } catch (error) {
345
+ log.debug(`Invalid directory name: ${dirName}`, error)
346
+ }
327
347
 
328
348
  const data = `\
329
349
  <!DOCTYPE html>
@@ -331,7 +351,7 @@ export async function plotDetailedStatsDashboard(statsFile: string, outFile = 'p
331
351
  <head>
332
352
  <meta charset="UTF-8">
333
353
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
334
- <title>${id} (${description})</title>
354
+ <title>${title} (${description})</title>
335
355
  <link rel="icon" href="https://raw.githubusercontent.com/vpalmisano/webrtcperf/devel/media/logo.svg">
336
356
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
337
357
  <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars"></script>
@@ -347,7 +367,7 @@ export async function plotDetailedStatsDashboard(statsFile: string, outFile = 'p
347
367
  <v-app>
348
368
  <v-main>
349
369
  <v-app-bar color="primary" density="compact">
350
- <v-app-bar-title><b>${id}</b> (${description})</v-app-bar-title>
370
+ <v-app-bar-title><b>${title}</b>${description ? ` (${description})` : ''}</v-app-bar-title>
351
371
  <template v-slot:append>
352
372
  <v-select color="primary" :items="participants" v-model="selected" density="compact" variant="solo" hide-details="auto"></v-select>
353
373
  </template>
package/src/rtcstats.ts CHANGED
@@ -260,7 +260,8 @@ export enum RtcStatsMetricNames {
260
260
  videoFirCountSent = 'videoFirCountSent',
261
261
  videoPliCountSent = 'videoPliCountSent',
262
262
  videoDecodeLatency = 'videoDecodeLatency',
263
- //'videoFramesDecoded',
263
+ videoFramesDecoded = 'videoFramesDecoded',
264
+ videoFramesDropped = 'videoFramesDropped',
264
265
  videoRecvFrames = 'videoRecvFrames',
265
266
  videoRecvFps = 'videoRecvFps',
266
267
  videoRecvAvgJitterBufferDelay = 'videoRecvAvgJitterBufferDelay',
@@ -285,7 +286,8 @@ export enum RtcStatsMetricNames {
285
286
  screenFirCountSent = 'screenFirCountSent',
286
287
  screenPliCountSent = 'screenPliCountSent',
287
288
  screenDecodeLatency = 'screenDecodeLatency',
288
- //'screenFramesDecoded',
289
+ screenFramesDecoded = 'screenFramesDecoded',
290
+ screenFramesDropped = 'screenFramesDropped',
289
291
  screenRecvFrames = 'screenRecvFrames',
290
292
  screenRecvFps = 'screenRecvFps',
291
293
  screenRecvAvgJitterBufferDelay = 'screenRecvAvgJitterBufferDelay',
@@ -437,7 +439,8 @@ export function updateRtcStats(
437
439
  })
438
440
  }
439
441
  if (inboundRtp.kind === 'video' && inboundRtp.keyFramesDecoded > 0) {
440
- //setStats(stats, prefix + 'FramesDecoded', key, inboundRtp.framesDecoded
442
+ setStats(stats, (prefix + 'FramesDecoded') as RtcStatsMetricNames, key, inboundRtp.framesDecoded)
443
+ setStats(stats, (prefix + 'FramesDropped') as RtcStatsMetricNames, key, inboundRtp.framesDropped)
441
444
  setStats(stats, (prefix + 'RecvFrames') as RtcStatsMetricNames, key, inboundRtp.framesReceived)
442
445
  setStats(stats, (prefix + 'RecvFps') as RtcStatsMetricNames, key, inboundRtp.framesPerSecond)
443
446
  setStats(stats, (prefix + 'RecvHeight') as RtcStatsMetricNames, key, inboundRtp.frameHeight)
package/src/session.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  portForwarder,
44
44
  resolveIP,
45
45
  resolvePackagePath,
46
+ runShellCommand,
46
47
  sha256,
47
48
  sleep,
48
49
  waitStopProcess,
@@ -147,6 +148,7 @@ export interface SessionParams {
147
148
  useFakeMedia: boolean
148
149
  enableGpu: string
149
150
  enableBrowserLogging: string
151
+ enableRtpDump: string
150
152
  startTimestamp: number
151
153
  sessions: number
152
154
  tabsPerSession: number
@@ -218,6 +220,7 @@ export class Session extends EventEmitter {
218
220
  private readonly useFakeMedia: boolean
219
221
  private readonly enableGpu: string
220
222
  private readonly enableBrowserLogging: boolean
223
+ private readonly enableRtpDump: boolean
221
224
  private readonly startTimestamp: number
222
225
  private readonly sessions: number
223
226
  private readonly tabsPerSession: number
@@ -365,6 +368,7 @@ export class Session extends EventEmitter {
365
368
  useFakeMedia,
366
369
  enableGpu,
367
370
  enableBrowserLogging,
371
+ enableRtpDump,
368
372
  startTimestamp,
369
373
  sessions,
370
374
  tabsPerSession,
@@ -433,6 +437,7 @@ export class Session extends EventEmitter {
433
437
  this.useFakeMedia = useFakeMedia
434
438
  this.enableGpu = enableGpu
435
439
  this.enableBrowserLogging = enabledForSession(this.id, enableBrowserLogging)
440
+ this.enableRtpDump = enabledForSession(this.id, enableRtpDump)
436
441
  this.startTimestamp = startTimestamp || Date.now()
437
442
  this.sessions = sessions || 1
438
443
  this.tabsPerSession = tabsPerSession || 1
@@ -595,12 +600,15 @@ export class Session extends EventEmitter {
595
600
 
596
601
  let fieldTrials = this.chromiumFieldTrials || ''
597
602
 
598
- if (this.enableBrowserLogging && this.pageLogPath) {
603
+ if (this.pageLogPath && this.enableBrowserLogging) {
599
604
  const pageLogDir = path.dirname(this.pageLogPath)
600
605
  const eventLogPath = path.resolve(pageLogDir, `webrtc-event-logging-${this.id}`)
601
606
  fs.mkdirSync(eventLogPath, { recursive: true })
602
607
  args.push('--enable-logging', '--vmodule=*/webrtc/*=5', '--v=0', `--webrtc-event-logging=${eventLogPath}`)
603
608
  fieldTrials = 'WebRTC-RtcEventLogNewFormat/Disabled/' + fieldTrials
609
+ if (this.enableRtpDump) {
610
+ fieldTrials = 'WebRTC-Debugging-RtpDump/Enabled/' + fieldTrials
611
+ }
604
612
  env.CHROME_LOG_FILE = path.resolve(pageLogDir, `chrome-${this.id}.log`)
605
613
  }
606
614
 
@@ -1467,6 +1475,26 @@ Object.defineProperty(window.screen.orientation, 'type', { value: 'landscape-pri
1467
1475
  page.once('close', () => throttleNotifier.off('change', onThrottleChange))
1468
1476
  }
1469
1477
 
1478
+ if (this.pageLogPath && this.enableRtpDump) {
1479
+ page.once('close', async () => {
1480
+ const dirPath = path.dirname(this.pageLogPath)
1481
+ const logFilePath = path.join(dirPath, `chrome-${this.id}.log`)
1482
+ if (fs.existsSync(logFilePath)) {
1483
+ const pcapFilePath = path.join(dirPath, `chrome-${this.id}.pcap`)
1484
+ try {
1485
+ await runShellCommand(`\
1486
+ grep RTP_DUMP ${logFilePath} | text2pcap -D -u 1000,2000 -t %H:%M:%S.%f - ${pcapFilePath};
1487
+ grep -v RTP_DUMP ${logFilePath} > ${logFilePath}.tmp;
1488
+ mv ${logFilePath}.tmp ${logFilePath};
1489
+ `)
1490
+ log.info(`rtp dump saved to: ${pcapFilePath}`)
1491
+ } catch (err) {
1492
+ log.error(`error converting rtp dump to pcap: ${(err as Error).stack}`)
1493
+ }
1494
+ }
1495
+ })
1496
+ }
1497
+
1470
1498
  log.debug(`Page ${index + 1} "${url}" loaded in ${(Date.now() - pageLoadTime) / 1000}s`)
1471
1499
 
1472
1500
  for (let i = 0; i < this.evaluateAfter.length; i++) {