@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/README.md +18 -0
- package/app.min.js +1 -1
- package/build/src/app.js +8 -0
- package/build/src/app.js.map +1 -1
- package/build/src/config.d.ts +7 -0
- package/build/src/config.js +44 -2
- package/build/src/config.js.map +1 -1
- package/build/src/mcp.d.ts +1 -0
- package/build/src/mcp.js +209 -0
- package/build/src/mcp.js.map +1 -0
- package/build/src/media.d.ts +0 -1
- package/build/src/media.js +3 -10
- package/build/src/media.js.map +1 -1
- package/build/src/rtcstats.d.ts +1 -0
- package/build/src/rtcstats.js +1 -0
- package/build/src/rtcstats.js.map +1 -1
- package/build/src/session.d.ts +1 -0
- package/build/src/session.js +12 -4
- package/build/src/session.js.map +1 -1
- package/build/src/stats.d.ts +4 -0
- package/build/src/stats.js +9 -2
- package/build/src/stats.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -13
- package/src/app.ts +9 -0
- package/src/config.ts +38 -2
- package/src/mcp.ts +248 -0
- package/src/media.ts +3 -13
- package/src/rtcstats.ts +1 -0
- package/src/session.ts +12 -3
- package/src/stats.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpalmisano/webrtcperf",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
55
|
-
"@
|
|
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.
|
|
58
|
-
"axios": "^1.
|
|
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.
|
|
66
|
-
"express": "^5.1
|
|
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": "^
|
|
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.
|
|
82
|
-
"puppeteer-core": "^24.
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
123
|
-
` ${audioMap} -c:a aac -ar 48000 -b:a 192k -f mp4 -movflags faststart ${
|
|
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
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
|
|
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.
|
|
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.
|
|
1720
|
-
this.externalCollectedStats.clear()
|
|
1728
|
+
this.resetStats()
|
|
1721
1729
|
}
|
|
1722
1730
|
}
|