@vpalmisano/webrtcperf 4.4.9 → 4.4.11

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.4.9",
3
+ "version": "4.4.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/vpalmisano/webrtcperf.git"
@@ -51,11 +51,11 @@
51
51
  },
52
52
  "license": "AGPL-3.0-or-later",
53
53
  "dependencies": {
54
- "@google/genai": "^1.17.0",
55
- "@puppeteer/browsers": "^2.10.8",
54
+ "@google/genai": "^1.20.0",
55
+ "@puppeteer/browsers": "^2.10.10",
56
56
  "@vpalmisano/throttler": "0.0.14",
57
- "@vpalmisano/webrtcperf-js": "^1.1.13",
58
- "axios": "^1.11.0",
57
+ "@vpalmisano/webrtcperf-js": "^1.1.14",
58
+ "axios": "^1.12.2",
59
59
  "chalk-template": "^1.1.2",
60
60
  "change-case": "^4.1.2",
61
61
  "compression": "^1.8.1",
@@ -67,23 +67,24 @@
67
67
  "express-basic-auth": "^1.2.1",
68
68
  "fast-stats": "^0.0.7",
69
69
  "form-data": "^4.0.4",
70
+ "googleapis": "^160.0.0",
70
71
  "ipaddr.js": "^2.2.0",
71
72
  "json5": "^2.2.3",
72
73
  "lorem-ipsum": "^2.0.8",
73
- "marked": "^16.2.1",
74
+ "marked": "^16.3.0",
74
75
  "marked-terminal": "^7.3.0",
75
76
  "node-cache": "^5.1.2",
76
77
  "node-os-utils": "^1.3.7",
77
78
  "pidtree": "^0.6.0",
78
79
  "pidusage": "^4.0.1",
79
80
  "prom-client": "^15.1.3",
80
- "puppeteer": "^24.19.0",
81
- "puppeteer-core": "^24.19.0",
81
+ "puppeteer": "^24.22.0",
82
+ "puppeteer-core": "^24.22.0",
82
83
  "puppeteer-extra": "^3.3.6",
83
84
  "puppeteer-extra-plugin-stealth": "^2.11.2",
84
85
  "puppeteer-intercept-and-modify-requests": "^1.3.1",
85
86
  "sprintf-js": "^1.1.3",
86
- "tar-fs": "^3.1.0",
87
+ "tar-fs": "^3.1.1",
87
88
  "toml": "^3.0.0",
88
89
  "ws": "^8.18.3",
89
90
  "yaml": "^2.8.1"
@@ -94,18 +95,18 @@
94
95
  "@types/compression": "^1.8.1",
95
96
  "@types/convict": "^6.1.6",
96
97
  "@types/convict-format-with-validator": "^6.0.5",
97
- "@types/dockerode": "^3.3.43",
98
+ "@types/dockerode": "^3.3.44",
98
99
  "@types/fast-stats": "^0.0.35",
99
100
  "@types/marked-terminal": "^6.1.1",
100
- "@types/node": "^20.19.13",
101
+ "@types/node": "^20.19.17",
101
102
  "@types/node-os-utils": "^1.3.4",
102
103
  "@types/pidusage": "^2.0.5",
103
104
  "@types/sdp-transform": "^2.15.0",
104
105
  "@types/sprintf-js": "^1.1.4",
105
106
  "@types/tar-fs": "^2.0.4",
106
107
  "@types/ws": "^8.18.1",
107
- "@typescript-eslint/eslint-plugin": "^8.43.0",
108
- "@typescript-eslint/parser": "^8.43.0",
108
+ "@typescript-eslint/eslint-plugin": "^8.44.0",
109
+ "@typescript-eslint/parser": "^8.44.0",
109
110
  "@vpalmisano/typedoc-cookie-consent": "^0.0.4",
110
111
  "@vpalmisano/typedoc-plugin-ga": "^1.0.6",
111
112
  "eslint": "^9.35.0",
@@ -119,9 +120,9 @@
119
120
  "prettier": "^3.6.2",
120
121
  "terser-webpack-plugin": "^5.3.14",
121
122
  "ts-loader": "^9.5.4",
122
- "typedoc": "^0.28.12",
123
+ "typedoc": "^0.28.13",
123
124
  "typescript": "^5.9.2",
124
- "typescript-eslint": "^8.43.0",
125
+ "typescript-eslint": "^8.44.0",
125
126
  "webpack": "^5.101.3",
126
127
  "webpack-cli": "^6.0.1",
127
128
  "webpack-node-externals": "^3.0.0",
package/src/docker.ts CHANGED
@@ -3,6 +3,7 @@ import { logger, resolvePackagePath } from './utils'
3
3
  import { loadConfig } from './config'
4
4
  import { runShellCommand } from '@vpalmisano/throttler'
5
5
  import os from 'os'
6
+ import fs from 'fs'
6
7
 
7
8
  const log = logger('webrtcperf:docker')
8
9
 
@@ -10,12 +11,22 @@ export async function runWithDocker(argv: string[]) {
10
11
  const docker = new Docker()
11
12
  const configPath = argv[0]
12
13
  if (!configPath) throw new Error('No configuration file specified')
13
- const config = (await loadConfig(configPath))[0]
14
+ const configs = await loadConfig(configPath)
15
+ if (!configs.length) throw new Error('Failed to load configuration file')
14
16
 
15
17
  const startTimestamp = Date.now()
16
18
  const dataDir = process.cwd()
19
+ const tmpDir = os.tmpdir()
17
20
 
18
- const binds: string[] = ['/dev/shm:/dev/shm', `${dataDir}:/data`, '/tmp/webrtcperf-cache:/root/.webrtcperf']
21
+ const jsonConfigPath = `${tmpDir}/webrtcperf-config-${startTimestamp}.json`
22
+ await fs.promises.writeFile(jsonConfigPath, JSON.stringify(configs), 'utf-8')
23
+
24
+ const binds: string[] = [
25
+ '/dev/shm:/dev/shm',
26
+ `${dataDir}:/data`,
27
+ `${tmpDir}/webrtcperf-cache:/root/.webrtcperf`,
28
+ `${jsonConfigPath}:/tmp/config.json:ro`,
29
+ ]
19
30
 
20
31
  if (process.env.DEBUG_SRC) {
21
32
  binds.push(`${resolvePackagePath('app.min.js')}:/app/app.min.js:ro`)
@@ -23,15 +34,15 @@ export async function runWithDocker(argv: string[]) {
23
34
 
24
35
  const portBindings: Docker.PortMap = {}
25
36
  const exposedPorts: { [portAndProtocol: string]: object } = {}
26
- if (config.debuggingPort) {
27
- for (let i = 0; i < config.sessions; i++) {
28
- const port = `${config.debuggingPort + i}/tcp`
29
- portBindings[port] = [{ HostPort: `${config.debuggingPort + i}` }]
37
+ if (configs[0].debuggingPort) {
38
+ for (let i = 0; i < configs[0].sessions; i++) {
39
+ const port = `${configs[0].debuggingPort + i}/tcp`
40
+ portBindings[port] = [{ HostPort: `${configs[0].debuggingPort + i}` }]
30
41
  exposedPorts[port] = {}
31
42
  }
32
43
  }
33
44
 
34
- if (config.throttleConfig && os.platform() === 'linux') {
45
+ if (configs[0].throttleConfig && os.platform() === 'linux') {
35
46
  await runShellCommand('sudo modprobe ifb numifbs=1')
36
47
  }
37
48
 
@@ -45,7 +56,7 @@ export async function runWithDocker(argv: string[]) {
45
56
  `START_TIMESTAMP=${startTimestamp}`,
46
57
  ]
47
58
 
48
- if (config.prometheusPushgateway.startsWith('http://localhost')) {
59
+ if (configs[0].prometheusPushgateway.startsWith('http://localhost')) {
49
60
  env.push('PROMETHEUS_PUSHGATEWAY=http://pushgateway:9091')
50
61
  }
51
62
 
@@ -53,12 +64,14 @@ export async function runWithDocker(argv: string[]) {
53
64
  Image: 'ghcr.io/vpalmisano/webrtcperf:devel',
54
65
  name: 'webrtcperf',
55
66
  WorkingDir: '/data',
56
- Cmd: argv,
67
+ Cmd: ['/tmp/config.json'],
57
68
  HostConfig: {
58
69
  Binds: binds,
59
70
  PortBindings: portBindings,
60
- CapAdd: config.throttleConfig && os.platform() === 'linux' ? ['NET_ADMIN'] : [],
61
- NetworkMode: config.prometheusPushgateway.startsWith('http://localhost') ? 'prometheus-stack_default' : 'bridge',
71
+ CapAdd: configs[0].throttleConfig && os.platform() === 'linux' ? ['NET_ADMIN'] : [],
72
+ NetworkMode: configs[0].prometheusPushgateway.startsWith('http://localhost')
73
+ ? 'prometheus-stack_default'
74
+ : 'bridge',
62
75
  ExtraHosts: process.env.EXTRA_HOSTS ? process.env.EXTRA_HOSTS.split(',').map(h => h.trim()) : [],
63
76
  },
64
77
  Env: env,
@@ -110,4 +123,6 @@ export async function runWithDocker(argv: string[]) {
110
123
  log.error('Docker operation failed:', error)
111
124
  throw error
112
125
  }
126
+
127
+ await fs.promises.unlink(jsonConfigPath)
113
128
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './app'
2
2
  export * from './config'
3
+ export * from './docker'
3
4
  export * from './media'
4
5
  export * from './rtcstats'
5
6
  export * from './server'
@@ -7,3 +8,4 @@ export * from './session'
7
8
  export * from './stats'
8
9
  export * from './utils'
9
10
  export * from './vmaf'
11
+ export * from './scenarios'
@@ -0,0 +1,262 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { FastStats } from './stats'
4
+ import { ThrottleConfig, ThrottleRule } from '@vpalmisano/throttler'
5
+ import { Config } from './config'
6
+ import { Auth, google } from 'googleapis'
7
+ import { logger } from './utils'
8
+
9
+ const log = logger('webrtcperf:scenarios')
10
+
11
+ /**
12
+ * It parses a CSV stats file and returns an array of objects representing each row.
13
+ * @param filePath The path to the CSV stats file.
14
+ * @returns An array of objects where each object represents a row in the CSV file with keys as column headers.
15
+ */
16
+ export async function parseStatsFile(filePath: string) {
17
+ log.debug(`parseStatsFile: ${filePath}`)
18
+ const fileData = await fs.promises.readFile(filePath, 'utf-8')
19
+ const lines = fileData.split('\n')
20
+ const headers = lines[0].split(',')
21
+ const data = lines.slice(1).map(line =>
22
+ line.split(',').reduce(
23
+ (acc, value, index) => {
24
+ if (value !== '') {
25
+ acc[headers[index]] = isNaN(Number(value)) ? value : Number(value)
26
+ }
27
+ return acc
28
+ },
29
+ {} as Record<string, string | number>,
30
+ ),
31
+ )
32
+ return data
33
+ }
34
+
35
+ export type StatsSummary = {
36
+ timestamp: number
37
+ id: string
38
+ scenario: string
39
+ videoRecvBitratePerPixel: FastStats
40
+ videoRecvFps: FastStats
41
+ videoSentFps: FastStats
42
+ }
43
+
44
+ /**
45
+ * It aggregates the stats summary from multiple test runs in a directory.
46
+ * @param options.dirPath Directory path containing test run subdirectories. Default is 'logs'.
47
+ * @param options.senderParticipantName Participant name of the sender. Default is 'Participant-000001'.
48
+ * @param options.receiverParticipantName Participant name of the receiver. Default is 'Participant-000000'.
49
+ * @param options.nameParser Function to parse test directory names. Default splits by '_' and extracts id and scenario.
50
+ * @returns Array of aggregated stats including timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps, and videoSentFps.
51
+ */
52
+ export async function aggregateStatsSummary({
53
+ dirPath = 'logs',
54
+ senderParticipantName = 'Participant-000001',
55
+ receiverParticipantName = 'Participant-000000',
56
+ nameParser = (name: string) => {
57
+ const [_, id, scenario] = name.split('_')
58
+ return { id, scenario }
59
+ },
60
+ }) {
61
+ log.debug(`aggregateStatsSummary: ${dirPath}`)
62
+ const stats: StatsSummary[] = []
63
+ const results = await fs.promises.readdir(dirPath)
64
+ for (const test of results) {
65
+ const filePath = path.join(dirPath, test, 'detailed-stats-summary.csv')
66
+ if (!fs.existsSync(filePath)) continue
67
+ const timestamp = fs.statSync(path.join(dirPath, test)).ctime.getTime()
68
+ const data = await parseStatsFile(filePath)
69
+ const { id, scenario } = nameParser(test)
70
+
71
+ const aggregated = {
72
+ timestamp,
73
+ id,
74
+ scenario,
75
+ videoRecvBitratePerPixel: new FastStats(),
76
+ videoRecvFps: new FastStats(),
77
+ videoSentFps: new FastStats(),
78
+ }
79
+ data.forEach(v => {
80
+ const { participantName, trackId } = v as { participantName: string; trackId: string }
81
+ const metrics = v as Record<string, number>
82
+ if (participantName === receiverParticipantName) {
83
+ if (trackId?.endsWith('-v') && metrics.videoRecvFrames > 0) {
84
+ const videoRecvBitratePerPixel =
85
+ metrics.videoRecvBitrates / (metrics.videoRecvWidth * metrics.videoRecvHeight)
86
+ if (!isNaN(videoRecvBitratePerPixel)) aggregated.videoRecvBitratePerPixel.push(videoRecvBitratePerPixel)
87
+ if (!isNaN(metrics.videoRecvFps)) aggregated.videoRecvFps.push(metrics.videoRecvFps)
88
+ }
89
+ } else if (participantName === senderParticipantName) {
90
+ if (trackId?.endsWith('-v') && metrics.videoSentFrames > 0) {
91
+ if (!isNaN(metrics.videoSentFps)) aggregated.videoSentFps.push(metrics.videoSentFps)
92
+ }
93
+ }
94
+ })
95
+ stats.push(aggregated)
96
+ }
97
+ return stats.sort((a, b) => a.timestamp - b.timestamp)
98
+ }
99
+
100
+ /**
101
+ * It uploads the aggregated stats to a Google Sheet.
102
+ * A valid Google service account credentials file must be specified
103
+ * in the `GOOGLE_CREDENTIALS_PATH` environment variable.
104
+ * @param stats The aggregated stats to upload.
105
+ * @param spreadsheetId The ID of the Google Spreadsheet.
106
+ * @param table The name of the table (sheet) within the spreadsheet. Default is 'data'.
107
+ */
108
+ export async function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadsheetId: string, table = 'data') {
109
+ log.debug(`uploadResultsToGoogleSheet spreadsheetId: ${spreadsheetId} table: ${table}`)
110
+ if (!process.env.GOOGLE_CREDENTIALS_PATH) throw new Error('GOOGLE_CREDENTIALS_PATH environment variable is not set')
111
+ if (!fs.existsSync(process.env.GOOGLE_CREDENTIALS_PATH))
112
+ throw new Error(`Google credentials file not found: ${process.env.GOOGLE_CREDENTIALS_PATH}`)
113
+ if (!stats.length) return
114
+ const auth = new Auth.GoogleAuth({
115
+ keyFile: process.env.GOOGLE_CREDENTIALS_PATH,
116
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
117
+ })
118
+ const sheets = google.sheets({ version: 'v4', auth })
119
+ // Update headers.
120
+ const headers = ['datetime', 'id', 'scenario', 'videoRecvBitratePerPixel', 'videoRecvFps']
121
+ await sheets.spreadsheets.values.update({
122
+ spreadsheetId,
123
+ range: `${table}!A1:E1`,
124
+ valueInputOption: 'USER_ENTERED',
125
+ requestBody: { majorDimension: 'ROWS', values: [headers] },
126
+ })
127
+ // Append values.
128
+ const values = [] as string[][]
129
+ stats.forEach(s => {
130
+ const { timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps } = s
131
+ if (!videoRecvBitratePerPixel.length) return
132
+ const datetime = new Date(timestamp).toLocaleString('en-US', {
133
+ timeZone: 'UTC',
134
+ hourCycle: 'h23',
135
+ })
136
+ values.push([
137
+ datetime,
138
+ id,
139
+ formatThrottleRule(parseThrottleRule(scenario), true),
140
+ videoRecvBitratePerPixel.percentile(95).toFixed(3),
141
+ videoRecvFps.percentile(95).toFixed(3),
142
+ ])
143
+ })
144
+ if (values.length) {
145
+ await sheets.spreadsheets.values.append({
146
+ spreadsheetId,
147
+ range: `${table}!A:E`,
148
+ valueInputOption: 'USER_ENTERED',
149
+ insertDataOption: 'INSERT_ROWS',
150
+ requestBody: { majorDimension: 'ROWS', values },
151
+ })
152
+ }
153
+ }
154
+
155
+ export type ThrottleDirection = 'up' | 'down' | 'bidi'
156
+
157
+ function formatBitrate(bitrate: number | undefined, prefix = ' ') {
158
+ if (bitrate === undefined) return ''
159
+ let suffix = 'Kbps'
160
+ if (bitrate >= 10000) {
161
+ bitrate /= 1000
162
+ suffix = 'Mbps'
163
+ }
164
+ return `${prefix}${bitrate.toFixed(0)}${suffix}`.padStart(8, ' ')
165
+ }
166
+
167
+ function formatLoss(loss: number | undefined, prefix = ' ') {
168
+ return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(2, ' ')}%` : ''
169
+ }
170
+
171
+ function formatDelay(delay: number | undefined, prefix = ' ') {
172
+ return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(3, ' ')}ms` : ''
173
+ }
174
+
175
+ export function formatThrottleRule(throttleRule: ThrottleRule & { direction: ThrottleDirection }, human = false) {
176
+ const { rate, loss, delay, direction } = throttleRule
177
+ return human
178
+ ? `${direction.padEnd(4, ' ')}${formatBitrate(rate)}${formatLoss(loss)}${formatDelay(delay)}`
179
+ : `${direction}-r${rate}-l${loss}-d${delay}`
180
+ }
181
+
182
+ export function parseThrottleRule(throttleDesc: string) {
183
+ const match = throttleDesc.match(/(up|down|bidi)-r(\d+)-l([\d.]+)-d(\d+)/)
184
+ if (!match) throw new Error(`Invalid throttle description: ${throttleDesc}`)
185
+ const direction = match[1] as ThrottleDirection
186
+ const rate = parseInt(match[2])
187
+ const loss = parseInt(match[3])
188
+ const delay = parseInt(match[4])
189
+ return { direction, rate, loss, delay }
190
+ }
191
+
192
+ /**
193
+ * It generates a test configuration with a scenario including 2 participants.
194
+ * The first participant sends video and the second receives it.
195
+ * Both participants send and receive audio.
196
+ * The network conditions are applied according to the specified direction to the sender (`up`),
197
+ * the receiver (`down`) or both (`bidi`).
198
+ * The test is repeated the specified number of times.
199
+ * The output is an array of partial configuration objects that can be used to run the tests
200
+ * with the main application, after merging it with a configuration that includes
201
+ * the destination url (mandatory) and other optional parameters.
202
+ * @param id The unique identifier for the test scenario.
203
+ * @param options.rate The target bandwidth in kbps.
204
+ * @param options.loss The packet loss percentage.
205
+ * @param options.delay The network delay in milliseconds.
206
+ * @param options.direction The direction of the network throttling: 'up', 'down', or 'bidi'.
207
+ * @param repeat The number of times to repeat the test scenario. Default is 1.
208
+ * @returns An array of partial configuration objects for each test scenario.
209
+ */
210
+ export async function twoParticipantsWithRateLossDelay(
211
+ id: string,
212
+ { rate, loss, delay, direction }: { rate: number; loss: number; delay: number; direction: ThrottleDirection },
213
+ repeat: 1,
214
+ ) {
215
+ const throttle: ThrottleConfig = {}
216
+ const queue = 25
217
+ if (direction === 'down' || direction === 'bidi') {
218
+ throttle.down = [
219
+ { rate: 20000, loss: 0, delay: 0, queue },
220
+ { rate, loss, delay, queue, at: 30 },
221
+ ]
222
+ }
223
+ if (direction === 'up' || direction === 'bidi') {
224
+ throttle.up = [
225
+ { rate: 20000, loss: 0, delay, queue },
226
+ { rate, loss, delay, queue, at: 30 },
227
+ ]
228
+ }
229
+ const throttleDesc = formatThrottleRule({ rate, loss, delay, direction })
230
+ const now = Date.now()
231
+ const ret: Partial<Config>[] = []
232
+ for (let i = 0; i < repeat; i++) {
233
+ const basePath = `logs/${now}-${i + 1}_${id}_${throttleDesc}`
234
+ const sessions = direction === 'bidi' ? '0-1' : direction === 'down' ? '0' : '1'
235
+ ret.push({
236
+ sessions: 2,
237
+ runDuration: 60 * 3,
238
+ debuggingPort: 9000,
239
+ prometheusPushgateway: 'http://localhost:9091',
240
+ prometheusPushgatewayJobName: id,
241
+ statsPath: `${basePath}/stats.csv`,
242
+ detailedStatsPath: `${basePath}/detailed-stats.csv`,
243
+ showPageLog: false,
244
+ showStats: false,
245
+ statsInterval: 5,
246
+ scriptParams: JSON.stringify({
247
+ enableMic: '0-1',
248
+ enableCam: '1',
249
+ }),
250
+ throttleConfig: JSON.stringify([
251
+ {
252
+ sessions,
253
+ protocol: 'udp',
254
+ skipSourcePorts: '53,80,443',
255
+ skipDestinationPorts: '53,80,443',
256
+ ...throttle,
257
+ },
258
+ ]),
259
+ })
260
+ }
261
+ return ret
262
+ }
package/src/utils.ts CHANGED
@@ -23,7 +23,6 @@ import pidusage from 'pidusage'
23
23
  import puppeteer, { ImageFormat, Page } from 'puppeteer-core'
24
24
 
25
25
  import { Session } from './session'
26
- import { FastStats } from './stats'
27
26
 
28
27
  // eslint-disable-next-line
29
28
  const ps = require('pidusage/lib/ps')
@@ -1256,75 +1255,3 @@ export async function getDockerLogsPath(): Promise<string> {
1256
1255
  }
1257
1256
  return logPath
1258
1257
  }
1259
-
1260
- export async function parseStatsFile(filePath: string) {
1261
- const fileData = await fs.promises.readFile(filePath, 'utf-8')
1262
- const lines = fileData.split('\n')
1263
- const headers = lines[0].split(',')
1264
- const data = lines.slice(1).map(line =>
1265
- line.split(',').reduce(
1266
- (acc, value, index) => {
1267
- if (value !== '') {
1268
- acc[headers[index]] = isNaN(Number(value)) ? value : Number(value)
1269
- }
1270
- return acc
1271
- },
1272
- {} as Record<string, string | number>,
1273
- ),
1274
- )
1275
- return data
1276
- }
1277
-
1278
- export async function aggregateStatsSummary({
1279
- dirPath = 'logs',
1280
- senderParticipantName = 'Participant-000001',
1281
- receiverParticipantName = 'Participant-000000',
1282
- nameParser = (name: string) => {
1283
- const [destination, scenario] = name.split('_')
1284
- return { destination, scenario }
1285
- },
1286
- }) {
1287
- const stats = [] as {
1288
- timestamp: number
1289
- destination: string
1290
- scenario: string
1291
- videoRecvBitratePerPixel: FastStats
1292
- videoRecvFps: FastStats
1293
- videoSentFps: FastStats
1294
- }[]
1295
- const results = await fs.promises.readdir(dirPath)
1296
- for (const test of results) {
1297
- const filePath = path.join(dirPath, test, 'detailed-stats-summary.csv')
1298
- if (!fs.existsSync(filePath)) continue
1299
- const timestamp = fs.statSync(path.join(dirPath, test)).ctime.getTime()
1300
- const data = await parseStatsFile(filePath)
1301
- const { destination, scenario } = nameParser(test)
1302
-
1303
- const aggregated = {
1304
- timestamp,
1305
- destination,
1306
- scenario,
1307
- videoRecvBitratePerPixel: new FastStats(),
1308
- videoRecvFps: new FastStats(),
1309
- videoSentFps: new FastStats(),
1310
- }
1311
- data.forEach(v => {
1312
- const { participantName, trackId } = v as { participantName: string; trackId: string }
1313
- const metrics = v as Record<string, number>
1314
- if (participantName === receiverParticipantName) {
1315
- if (trackId?.endsWith('-v') && metrics.videoRecvFrames > 0) {
1316
- const videoRecvBitratePerPixel =
1317
- metrics.videoRecvBitrates / (metrics.videoRecvWidth * metrics.videoRecvHeight)
1318
- if (!isNaN(videoRecvBitratePerPixel)) aggregated.videoRecvBitratePerPixel.push(videoRecvBitratePerPixel)
1319
- if (!isNaN(metrics.videoRecvFps)) aggregated.videoRecvFps.push(metrics.videoRecvFps)
1320
- }
1321
- } else if (participantName === senderParticipantName) {
1322
- if (trackId?.endsWith('-v') && metrics.videoSentFrames > 0) {
1323
- if (!isNaN(metrics.videoSentFps)) aggregated.videoSentFps.push(metrics.videoSentFps)
1324
- }
1325
- }
1326
- })
1327
- stats.push(aggregated)
1328
- }
1329
- return stats.sort((a, b) => a.timestamp - b.timestamp)
1330
- }