@vpalmisano/webrtcperf 4.6.2 → 4.7.2

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.6.2",
3
+ "version": "4.7.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/vpalmisano/webrtcperf.git"
@@ -51,23 +51,24 @@
51
51
  },
52
52
  "license": "AGPL-3.0-or-later",
53
53
  "dependencies": {
54
- "@google/genai": "^1.22.0",
55
- "@puppeteer/browsers": "^2.10.10",
54
+ "@google/genai": "^1.46.0",
55
+ "@modelcontextprotocol/sdk": "^1.27.1",
56
+ "@puppeteer/browsers": "^2.13.0",
56
57
  "@vpalmisano/throttler": "0.0.14",
57
- "@vpalmisano/webrtcperf-js": "^1.1.16",
58
- "axios": "^1.12.2",
58
+ "@vpalmisano/webrtcperf-js": "^1.2.3",
59
+ "axios": "^1.13.5",
59
60
  "chalk-template": "^1.1.2",
60
61
  "change-case": "^4.1.2",
61
62
  "compression": "^1.8.1",
62
63
  "convict": "^6.2.4",
63
64
  "convict-format-with-validator": "^6.2.0",
64
65
  "debug-level": "^4.1.1",
65
- "dockerode": "^4.0.9",
66
- "express": "^5.1.0",
66
+ "dockerode": "^4.0.10",
67
+ "express": "^5.2.1",
67
68
  "express-basic-auth": "^1.2.1",
68
69
  "fast-stats": "^0.0.7",
69
70
  "form-data": "^4.0.4",
70
- "googleapis": "^160.0.0",
71
+ "googleapis": "^164.1.0",
71
72
  "ipaddr.js": "^2.2.0",
72
73
  "json5": "^2.2.3",
73
74
  "lorem-ipsum": "^2.0.8",
@@ -78,16 +79,17 @@
78
79
  "pidtree": "^0.6.0",
79
80
  "pidusage": "^4.0.1",
80
81
  "prom-client": "^15.1.3",
81
- "puppeteer": "^24.23.0",
82
- "puppeteer-core": "^24.23.0",
82
+ "puppeteer": "^24.40.0",
83
+ "puppeteer-core": "^24.40.0",
83
84
  "puppeteer-extra": "^3.3.6",
84
85
  "puppeteer-extra-plugin-stealth": "^2.11.2",
85
86
  "puppeteer-intercept-and-modify-requests": "^1.3.1",
86
87
  "sprintf-js": "^1.1.3",
87
88
  "tar-fs": "^3.1.1",
88
89
  "toml": "^3.0.0",
89
- "ws": "^8.18.3",
90
- "yaml": "^2.8.1"
90
+ "ws": "^8.20.0",
91
+ "yaml": "^2.8.1",
92
+ "zod": "3"
91
93
  },
92
94
  "devDependencies": {
93
95
  "@eslint/js": "^9.37.0",
@@ -122,7 +124,7 @@
122
124
  "ts-loader": "^9.5.4",
123
125
  "typedoc": "^0.28.13",
124
126
  "typescript": "^5.9.3",
125
- "typescript-eslint": "^8.45.0",
127
+ "typescript-eslint": "^8.46.2",
126
128
  "webpack": "^5.102.0",
127
129
  "webpack-cli": "^6.0.1",
128
130
  "webpack-node-externals": "^3.0.0",
package/src/app.ts CHANGED
@@ -26,6 +26,7 @@ import { markedTerminal } from 'marked-terminal'
26
26
  import { EventEmitter } from 'events'
27
27
  import { runWithDocker } from './docker'
28
28
  import { plotDetailedStatsDashboard } from './plot'
29
+ import { mcpRunner } from './mcp'
29
30
 
30
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
32
  const { marked } = require('marked')
@@ -243,6 +244,7 @@ async function main(): Promise<void> {
243
244
  process.exit(0)
244
245
  }
245
246
 
247
+ // Handle plot command.
246
248
  if (process.argv.includes('--plot')) {
247
249
  process.argv = process.argv.filter(s => s !== '--plot')
248
250
  try {
@@ -254,6 +256,13 @@ async function main(): Promise<void> {
254
256
  process.exit(0)
255
257
  }
256
258
 
259
+ // Handle MCP command.
260
+ if (process.argv.includes('--mcp')) {
261
+ process.argv = process.argv.filter(s => s !== '--mcp')
262
+ await mcpRunner()
263
+ return
264
+ }
265
+
257
266
  let configs: Config[]
258
267
 
259
268
  // Handle prompt.
package/src/config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import convict, { addFormats, SchemaObj } from 'convict'
2
+ import { z } from 'zod'
2
3
  import { ipaddress, url } from 'convict-format-with-validator'
3
4
  import { existsSync } from 'fs'
4
5
  import os from 'os'
@@ -472,8 +473,9 @@ the following global variables will be attached to the \`webrtcperf\` global obj
472
473
  \`WEBRTC_PERF_SESSION\` the session number (0-indexed); \
473
474
  \`WEBRTC_PERF_TAB\` the tab number inside the same session (0-indexed); \
474
475
  \`WEBRTC_PERF_INDEX\` the page absolute index (0-indexed).
475
- Suggested values:
476
- - With meet.google.com: https://raw.githubusercontent.com/vpalmisano/webrtcperf/refs/heads/devel/examples/google-meet.js
476
+ Suggested values for automated testing:
477
+ - meet.google.com: https://raw.githubusercontent.com/vpalmisano/webrtcperf/refs/heads/devel/examples/google-meet.js
478
+ - meet.livekit.io: https://raw.githubusercontent.com/vpalmisano/webrtcperf/refs/heads/devel/examples/livekit.js
477
479
  `,
478
480
  format: String,
479
481
  default: '',
@@ -916,6 +918,40 @@ export function getConfigDocs(): ConfigDocs {
916
918
  return formatDocs({}, null, convict(configSchema).getSchema())
917
919
  }
918
920
 
921
+ /**
922
+ * Returns a Zod schema for the config object, with all parameters optional and described.
923
+ * Uses the config default values when a field is omitted. Useful for MCP tools and other
924
+ * consumers that need validated partial config.
925
+ */
926
+ export function getConfigZodSchema(): z.ZodObject<Record<string, z.ZodTypeAny>> {
927
+ const schema = convict(configSchema).getSchema()
928
+ const shape: Record<string, z.ZodTypeAny> = {}
929
+ for (const [key, value] of Object.entries(schema._cvtProperties)) {
930
+ const prop = value as SchemaObj & { doc?: string; format?: unknown; default?: unknown }
931
+ const doc = prop.doc ?? key
932
+ const def = prop.default
933
+ const format = prop.format
934
+ let zodType: z.ZodTypeAny
935
+ if (format === String || format === 'string') {
936
+ zodType = z.string()
937
+ } else if (format === 'nat' || format === Number) {
938
+ zodType = z.number()
939
+ } else if (format === 'float') {
940
+ zodType = z.number()
941
+ } else if (format === 'Boolean' || format === 'boolean') {
942
+ zodType = z.boolean()
943
+ } else if (format === 'index') {
944
+ zodType = z.union([z.string(), z.number(), z.boolean()])
945
+ } else if (Array.isArray(format)) {
946
+ zodType = z.enum(format as [string, ...string[]])
947
+ } else {
948
+ zodType = z.union([z.string(), z.number(), z.boolean()])
949
+ }
950
+ shape[key] = def !== undefined ? zodType.default(def).describe(doc) : zodType.optional().describe(doc)
951
+ }
952
+ return z.object(shape)
953
+ }
954
+
919
955
  const _schemaProperties = convict(configSchema).getProperties()
920
956
 
921
957
  /** [[include:config.md]] */
package/src/mcp.ts ADDED
@@ -0,0 +1,248 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { getSessionThrottleIndex, startThrottle } from '@vpalmisano/throttler'
4
+ import { z } from 'zod'
5
+
6
+ import { loadConfig } from './config'
7
+ import type { Config } from './config'
8
+ import { Session } from './session'
9
+ import { Stats } from './stats'
10
+ import { MediaPath, prepareFakeMedia } from './media'
11
+ import { logger } from './utils'
12
+
13
+ const log = logger('webrtcperf:mcp')
14
+
15
+ const mcpServer = new McpServer({
16
+ name: 'webrtcperf',
17
+ version: '1.0.0',
18
+ })
19
+
20
+ let stats: Stats
21
+ let statsReady: Promise<void>
22
+
23
+ async function getStats(): Promise<Stats> {
24
+ if (!stats) {
25
+ const [defaultConfig] = await loadConfig(undefined, {
26
+ showStats: false,
27
+ })
28
+ if (!defaultConfig.startTimestamp) {
29
+ defaultConfig.startTimestamp = Date.now()
30
+ }
31
+ stats = new Stats(defaultConfig)
32
+ stats.on('stats', () => {
33
+ mcpServer.server.sendResourceUpdated({ uri: 'webrtcperf://stats' })
34
+ })
35
+ statsReady = stats.start()
36
+ }
37
+ await statsReady
38
+ return stats
39
+ }
40
+
41
+ async function startSessionHandler(args: {
42
+ config: Record<string, unknown>
43
+ }): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
44
+ const { config } = args
45
+ log.debug('startSessionHandler', config)
46
+ const stats = await getStats()
47
+ const configs = await loadConfig(undefined, config as Partial<Config>)
48
+ const sessionConfig = configs[0]
49
+ const tabsPerSession = sessionConfig.tabsPerSession ?? 1
50
+ const id = stats.consumeSessionId(tabsPerSession)
51
+ const throttleIndex = getSessionThrottleIndex(id)
52
+ const spawnRate = (sessionConfig as Config & { spawnRate?: number }).spawnRate ?? 1
53
+ const spawnPeriod = 1000 / spawnRate
54
+ const throttleConfig = (sessionConfig as Config & { throttleConfig?: string }).throttleConfig
55
+ if (throttleConfig) {
56
+ await startThrottle(throttleConfig)
57
+ }
58
+ const mediaPaths: MediaPath[] = []
59
+ const videoPath = sessionConfig.videoPath
60
+ if (videoPath) {
61
+ for (const vp of videoPath.split(',')) {
62
+ const ret = await prepareFakeMedia({ ...sessionConfig, videoPath: vp })
63
+ mediaPaths.push(ret)
64
+ }
65
+ }
66
+ const mediaPath = mediaPaths.length ? mediaPaths[id % mediaPaths.length] : undefined
67
+
68
+ if (sessionConfig.url.startsWith('https://meet.google.com')) {
69
+ if (!sessionConfig.scriptPath) {
70
+ sessionConfig.scriptPath =
71
+ 'https://raw.githubusercontent.com/vpalmisano/webrtcperf/refs/heads/devel/examples/google-meet.js'
72
+ }
73
+ if (!sessionConfig.scriptParams) {
74
+ sessionConfig.scriptParams = '{"enableMic": true, "enableCam": true}'
75
+ }
76
+ if (!sessionConfig.debuggingPort) {
77
+ sessionConfig.debuggingPort = 9000
78
+ }
79
+ } else if (sessionConfig.url.startsWith('https://meet.livekit.io')) {
80
+ if (!sessionConfig.scriptPath) {
81
+ sessionConfig.scriptPath =
82
+ 'https://raw.githubusercontent.com/vpalmisano/webrtcperf/refs/heads/devel/examples/livekit.js'
83
+ }
84
+ }
85
+
86
+ const session = new Session({
87
+ ...sessionConfig,
88
+ throttleIndex,
89
+ spawnPeriod,
90
+ mediaPath,
91
+ id,
92
+ })
93
+ session.once('stop', () => {
94
+ setTimeout(() => startSessionHandler({ config }).catch(() => {}), spawnPeriod)
95
+ })
96
+ stats.addSession(session)
97
+ try {
98
+ await session.start()
99
+ } catch (err) {
100
+ stats.removeSession(session.id)
101
+ throw err
102
+ }
103
+ if (sessionConfig.runDuration) {
104
+ setTimeout(() => {
105
+ session.removeAllListeners()
106
+ session.stop()
107
+ stats.removeSession(session.id)
108
+ }, sessionConfig.runDuration * 1000)
109
+ }
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text' as const,
114
+ text: JSON.stringify({ message: 'Session created', id }, null, 2),
115
+ },
116
+ ],
117
+ }
118
+ }
119
+
120
+ async function stopSessionHandler(args: { id: number }): Promise<{
121
+ content: Array<{ type: 'text'; text: string }>
122
+ }> {
123
+ const { id } = args
124
+ log.debug('stopSessionHandler', id)
125
+ const s = await getStats()
126
+ const session = s.sessions.get(id)
127
+ if (!session) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text' as const,
132
+ text: JSON.stringify({ message: 'Session not found', id }, null, 2),
133
+ },
134
+ ],
135
+ }
136
+ }
137
+ session.removeAllListeners()
138
+ s.removeSession(id)
139
+ await session.stop()
140
+ return {
141
+ content: [
142
+ {
143
+ type: 'text' as const,
144
+ text: JSON.stringify({ message: 'Session deleted', id }),
145
+ },
146
+ ],
147
+ }
148
+ }
149
+
150
+ async function getSessionsHandler(): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
151
+ const s = await getStats()
152
+ const sessions = Array.from(s.sessions.entries()).map(([id, session]) => ({
153
+ id,
154
+ stats: session.stats,
155
+ }))
156
+ log.debug('getSessionsHandler', sessions)
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text' as const,
161
+ text: JSON.stringify({ sessions, count: sessions.length }),
162
+ },
163
+ ],
164
+ }
165
+ }
166
+
167
+ async function resetStatsHandler(): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
168
+ const s = await getStats()
169
+ log.debug('resetStatsHandler')
170
+ s.resetStats()
171
+ await mcpServer.server.sendResourceUpdated({ uri: 'webrtcperf://stats' })
172
+ return {
173
+ content: [
174
+ {
175
+ type: 'text' as const,
176
+ text: JSON.stringify({ message: 'Stats reset' }),
177
+ },
178
+ ],
179
+ }
180
+ }
181
+
182
+ async function getStatsReadHandler(uri: URL): Promise<{
183
+ contents: Array<{ uri: string; mimeType: string; text: string }>
184
+ }> {
185
+ const stats = await getStats()
186
+ log.debug('getStatsReadHandler', uri.href)
187
+ return {
188
+ contents: [
189
+ {
190
+ uri: uri.href,
191
+ mimeType: 'application/json',
192
+ text: JSON.stringify(stats.collectedStats),
193
+ },
194
+ ],
195
+ }
196
+ }
197
+
198
+ mcpServer.registerTool(
199
+ 'start_session',
200
+ {
201
+ description:
202
+ 'Start a new webrtcperf session in-process. Config is the session config object (url, tabsPerSession, throttleConfig, etc.). Returns the created session id.',
203
+ inputSchema: { config: z.record(z.string(), z.unknown()) },
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
+ } as any,
206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
207
+ startSessionHandler as any,
208
+ )
209
+
210
+ mcpServer.registerTool(
211
+ 'stop_session',
212
+ {
213
+ description: 'Stop a webrtcperf session by id.',
214
+ inputSchema: { id: z.number().int().nonnegative() },
215
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
216
+ } as any,
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ stopSessionHandler as any,
219
+ )
220
+
221
+ mcpServer.registerTool(
222
+ 'get_sessions',
223
+ {
224
+ description: 'List current webrtcperf sessions (id and stats for each running session).',
225
+ },
226
+ getSessionsHandler,
227
+ )
228
+
229
+ mcpServer.registerTool(
230
+ 'reset_stats',
231
+ {
232
+ description: 'Reset the collected webrtcperf stats.',
233
+ },
234
+ resetStatsHandler,
235
+ )
236
+
237
+ mcpServer.registerResource(
238
+ 'stats',
239
+ 'webrtcperf://stats',
240
+ { description: 'Get the current webrtcperf stats.' },
241
+ getStatsReadHandler,
242
+ )
243
+
244
+ export async function mcpRunner(): Promise<void> {
245
+ log.debug('mcpRunner')
246
+ const transport = new StdioServerTransport()
247
+ await mcpServer.connect(transport)
248
+ }
package/src/media.ts CHANGED
@@ -10,7 +10,6 @@ export type MediaPath = {
10
10
  video: string
11
11
  audio: string
12
12
  mp4: string
13
- m4a: string
14
13
  }
15
14
 
16
15
  /**
@@ -79,24 +78,19 @@ export async function prepareFakeMedia({
79
78
  const destMp4Path = useFakeMedia
80
79
  ? ''
81
80
  : `${videoCachePath}/${name}_${videoWidth}x${videoHeight}_${videoFramerate}fps.mp4`
82
- const destM4aPath = useFakeMedia ? '' : `${videoCachePath}/${name}.m4a`
83
81
 
84
82
  if (
85
83
  !existsSync(destVideoPath) ||
86
84
  !existsSync(destAudioPath) ||
87
85
  (destMp4Path && !existsSync(destMp4Path)) ||
88
- (destM4aPath && !existsSync(destM4aPath)) ||
89
86
  !videoCacheRaw
90
87
  ) {
91
- log.info(
92
- `Converting ${videoPath} to ${destVideoPath}, ${destAudioPath}${destMp4Path ? `, ${destMp4Path}` : ''}${destM4aPath ? `, ${destM4aPath}` : ''}`,
93
- )
88
+ log.info(`Converting ${videoPath} to ${destVideoPath}, ${destAudioPath}${destMp4Path ? `, ${destMp4Path}` : ''}`)
94
89
  const destVideoPathTmp = `${videoCachePath}/${name}_${videoWidth}x${videoHeight}_${videoFramerate}fps.tmp.${videoFormat}`
95
90
  const destAudioPathTmp = `${videoCachePath}/${name}.tmp.wav`
96
91
  const destMp4PathTmp = useFakeMedia
97
92
  ? ''
98
93
  : `${videoCachePath}/${name}_${videoWidth}x${videoHeight}_${videoFramerate}fps.tmp.mp4`
99
- const destM4aPathTmp = useFakeMedia ? '' : `${videoCachePath}/${name}.tmp.m4a`
100
94
 
101
95
  try {
102
96
  let source = `-i "${videoPath}"`
@@ -119,8 +113,8 @@ export async function prepareFakeMedia({
119
113
  ` ${videoMap} ${destVideoPathTmp}` +
120
114
  ` ${audioMap} -ar 48000 ${destAudioPathTmp}` +
121
115
  (destMp4PathTmp
122
- ? ` ${videoMap} -c:v libx264 -crf 10 -f mp4 -movflags faststart ${destMp4PathTmp}` +
123
- ` ${audioMap} -c:a aac -ar 48000 -b:a 192k -f mp4 -movflags faststart ${destM4aPathTmp}`
116
+ ? ` ${videoMap} -c:v libx264 -crf 10` +
117
+ ` ${audioMap} -c:a aac -ar 48000 -b:a 192k -f mp4 -movflags faststart ${destMp4PathTmp}`
124
118
  : ''),
125
119
  )
126
120
  await fs.rename(destVideoPathTmp, destVideoPath)
@@ -128,9 +122,6 @@ export async function prepareFakeMedia({
128
122
  if (destMp4PathTmp) {
129
123
  await fs.rename(destMp4PathTmp, destMp4Path)
130
124
  }
131
- if (destM4aPathTmp) {
132
- await fs.rename(destM4aPathTmp, destM4aPath)
133
- }
134
125
  } catch (err) {
135
126
  log.error(`Error converting video: ${(err as Error).stack}`)
136
127
  fs.unlink(destVideoPathTmp).catch(e => log.debug(e.message))
@@ -146,6 +137,5 @@ export async function prepareFakeMedia({
146
137
  video: destVideoPath,
147
138
  audio: destAudioPath,
148
139
  mp4: destMp4Path,
149
- m4a: destM4aPath,
150
140
  }
151
141
  }
package/src/rtcstats.ts CHANGED
@@ -81,6 +81,7 @@ export enum PageStatsNames {
81
81
  screenStartFrameDelay = 'screenStartFrameDelay',
82
82
 
83
83
  cpuPressure = 'cpuPressure',
84
+ questionAnswerDelay = 'questionAnswerDelay',
84
85
 
85
86
  videoWidth = 'videoWidth',
86
87
  videoHeight = 'videoHeight',
package/src/session.ts CHANGED
@@ -86,6 +86,7 @@ declare global {
86
86
  screenStartFrameDelay: number
87
87
  }
88
88
  collectCpuPressure: () => number
89
+ collectQuestionAnswerDelay: () => number
89
90
  collectVideoStats: () => {
90
91
  width: number
91
92
  height: number
@@ -817,16 +818,19 @@ try {
817
818
  console.error('[webrtcperf] Error parsing scriptParams:', err);
818
819
  webrtcperf.params = {};
819
820
  };
821
+
822
+ const webrtcperf_getServerUrl = (path, protocol = 'http', query = '') => {
823
+ return protocol + "${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/" + path + "?auth=${this.serverSecret}" + (query ? "&" + query : '')
824
+ }
820
825
  `
821
826
 
822
827
  if (this.serverPort) {
823
828
  cmd += `\
824
- webrtcperf.config.SAVE_MEDIA_URL = "ws${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/?auth=${this.serverSecret}&action=write-stream";
829
+ webrtcperf.config.SAVE_MEDIA_URL = webrtcperf_getServerUrl("", "ws", "action=write-stream");
825
830
  `
826
831
  if (this.mediaPath?.mp4 && !this.useFakeMedia) {
827
832
  cmd += `\
828
- webrtcperf.config.VIDEO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/cache/${path.basename(this.mediaPath.mp4)}?auth=${this.serverSecret}";
829
- webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost:${this.serverPort}/cache/${path.basename(this.mediaPath.m4a)}?auth=${this.serverSecret}";
833
+ webrtcperf.config.MEDIA_URL = webrtcperf_getServerUrl("cache/${path.basename(this.mediaPath.mp4)}");
830
834
  `
831
835
  }
832
836
  }
@@ -1710,6 +1714,7 @@ mv ${logFilePath}.tmp ${logFilePath};
1710
1714
  const pageCpu: Record<string, number> = {}
1711
1715
  const pageMemory: Record<string, number> = {}
1712
1716
  const cpuPressureStats: Record<string, number> = {}
1717
+ const questionAnswerDelayStats: Record<string, number> = {}
1713
1718
 
1714
1719
  const videoWidth: Record<string, number> = {}
1715
1720
  const videoHeight: Record<string, number> = {}
@@ -1738,6 +1743,7 @@ mv ${logFilePath}.tmp ${logFilePath};
1738
1743
  audioEndToEndDelay,
1739
1744
  videoEndToEndDelay,
1740
1745
  cpuPressure,
1746
+ questionAnswerDelay,
1741
1747
  videoStats,
1742
1748
  customMetrics,
1743
1749
  } = await page.evaluate(async () => ({
@@ -1745,6 +1751,7 @@ mv ${logFilePath}.tmp ${logFilePath};
1745
1751
  audioEndToEndDelay: webrtcperf.collectAudioEndToEndStats(),
1746
1752
  videoEndToEndDelay: webrtcperf.collectVideoEndToEndStats(),
1747
1753
  cpuPressure: webrtcperf.collectCpuPressure(),
1754
+ questionAnswerDelay: webrtcperf.collectQuestionAnswerDelay(),
1748
1755
  videoStats: webrtcperf.collectVideoStats(),
1749
1756
  customMetrics: 'collectCustomMetrics' in window ? collectCustomMetrics() : null,
1750
1757
  }))
@@ -1811,6 +1818,7 @@ mv ${logFilePath}.tmp ${logFilePath};
1811
1818
  }
1812
1819
 
1813
1820
  if (cpuPressure !== undefined) cpuPressureStats[pageKey] = cpuPressure
1821
+ if (questionAnswerDelay !== undefined) questionAnswerDelayStats[pageKey] = questionAnswerDelay
1814
1822
  if (videoStats) {
1815
1823
  videoWidth[pageKey] = videoStats.width
1816
1824
  videoHeight[pageKey] = videoStats.height
@@ -1912,6 +1920,7 @@ mv ${logFilePath}.tmp ${logFilePath};
1912
1920
  wsRecvBytes: wsRecvBytesStats,
1913
1921
  wsRecvLatency: wsRecvLatencyStats,
1914
1922
  cpuPressure: cpuPressureStats,
1923
+ questionAnswerDelay: questionAnswerDelayStats,
1915
1924
  videoWidth,
1916
1925
  videoHeight,
1917
1926
  videoBufferedTime,
package/src/stats.ts CHANGED
@@ -1682,6 +1682,15 @@ export class Stats extends events.EventEmitter {
1682
1682
  }
1683
1683
  }
1684
1684
 
1685
+ /**
1686
+ * Reset the stats.
1687
+ */
1688
+ resetStats(): void {
1689
+ log.debug('resetStats')
1690
+ this.collectedStats = this.initCollectedStats()
1691
+ this.externalCollectedStats.clear()
1692
+ }
1693
+
1685
1694
  async stop() {
1686
1695
  if (!this.running) return
1687
1696
  this.running = false
@@ -1716,7 +1725,6 @@ export class Stats extends events.EventEmitter {
1716
1725
  this.metrics = {}
1717
1726
  }
1718
1727
 
1719
- this.collectedStats = this.initCollectedStats()
1720
- this.externalCollectedStats.clear()
1728
+ this.resetStats()
1721
1729
  }
1722
1730
  }