@vpalmisano/webrtcperf 4.2.2 → 4.3.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/src/app.ts CHANGED
@@ -23,6 +23,7 @@ import { calculateVisqolScore } from './visqol'
23
23
  import { calculateVmafScore, convertToIvf, prepareVideo } from './vmaf'
24
24
  import path from 'path'
25
25
  import { markedTerminal } from 'marked-terminal'
26
+ import { EventEmitter } from 'events'
26
27
 
27
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
29
  const { marked } = require('marked')
@@ -56,141 +57,167 @@ Default value: \`${value.default}\`
56
57
  }
57
58
  }
58
59
 
59
- async function postTest(config: Config): Promise<void> {
60
- // vmaf score.
61
- if (config.vmafPath) {
62
- console.log('Calculating VMAF score...')
63
- try {
64
- await calculateVmafScore(config)
65
- } catch (err: unknown) {
66
- log.error(`vmaf score error: ${(err as Error).stack}`)
60
+ export class Application extends EventEmitter {
61
+ readonly config: Config
62
+ readonly stats: Stats
63
+ readonly server?: Server
64
+ private mediaPaths: MediaPath[] = []
65
+
66
+ constructor(config: Config) {
67
+ super()
68
+ if (!config.startTimestamp) {
69
+ config.startTimestamp = Date.now()
70
+ }
71
+ this.config = config
72
+ this.stats = new Stats(config)
73
+ if (config.serverPort) {
74
+ this.server = new Server(config, this.stats)
67
75
  }
68
76
  }
69
77
 
70
- // visqol score
71
- if (config.visqolPath) {
72
- console.log('Calculating Visqol score...')
73
- try {
74
- await calculateVisqolScore(config)
75
- } catch (err: unknown) {
76
- log.error(`visqol score error: ${(err as Error).stack}`)
78
+ async start() {
79
+ log.debug(`start (runDuration: ${this.config.runDuration})`)
80
+ await this.stats.start()
81
+ if (this.server) {
82
+ await this.server.start()
77
83
  }
78
- }
79
- }
84
+ const config = this.config
80
85
 
81
- export async function setupApplication(config: Config): Promise<{ stats: Stats; stop: () => Promise<void> }> {
82
- if (!config.startTimestamp) {
83
- config.startTimestamp = Date.now()
84
- }
86
+ // Handle vmaf commands.
87
+ if (config.vmafPrepareVideo) {
88
+ await prepareVideo(config, true)
89
+ }
90
+ if (config.vmafProcessVideo) {
91
+ await convertToIvf(
92
+ config.vmafProcessVideo,
93
+ config.vmafVideoCrop,
94
+ config.vmafKeepSourceFiles,
95
+ config.vmafSkipDuplicated,
96
+ )
97
+ }
85
98
 
86
- // Stats.
87
- const stats = new Stats(config)
88
- await stats.start()
99
+ // Handle sessions.
100
+ if (config.sessions > 0) {
101
+ // Prepare fake video and audio.
102
+ if (config.videoPath && !this.mediaPaths.length) {
103
+ for (const videoPath of config.videoPath.split(',')) {
104
+ const ret = await prepareFakeMedia({ ...config, videoPath })
105
+ this.mediaPaths.push(ret)
106
+ }
107
+ }
89
108
 
90
- // Control server.
91
- let server: Server | undefined
92
- if (config.serverPort) {
93
- server = new Server(config, stats)
94
- await server.start()
95
- }
109
+ // Network throttle.
110
+ if (config.throttleConfig) {
111
+ await startThrottle(config.throttleConfig)
112
+ }
96
113
 
97
- // If sessions are set, prepare fake video/audio and start sessions.
98
- if (config.sessions > 0) {
99
- // Prepare fake video and audio.
100
- const mediaPaths: MediaPath[] = []
101
- if (config.videoPath) {
102
- for (const videoPath of config.videoPath.split(',')) {
103
- const ret = await prepareFakeMedia({ ...config, videoPath })
104
- mediaPaths.push(ret)
114
+ // Download browser if necessary.
115
+ if (!config.chromiumUrl && !config.chromiumPath) {
116
+ await checkChromeExecutable()
105
117
  }
106
- }
107
118
 
108
- // Network throttle.
109
- if (config.throttleConfig) {
110
- await startThrottle(config.throttleConfig)
119
+ // Start the local sessions.
120
+ if (config.randomAudioPeriod) {
121
+ startRandomActivateAudio(
122
+ this.stats.sessions,
123
+ config.randomAudioPeriod,
124
+ config.randomAudioProbability,
125
+ config.randomAudioRange,
126
+ )
127
+ }
128
+ const spawnPeriod = 1000 / config.spawnRate
129
+ log.debug(`Starting ${config.sessions} sessions (spawnPeriod: ${spawnPeriod}ms)`)
130
+ const startTime = Date.now()
131
+ for (let i = 0; i < config.sessions; i += 1) {
132
+ const id = this.stats.consumeSessionId(config.tabsPerSession)
133
+ await this.startSession(id, spawnPeriod)
134
+ // If not the last session, sleep.
135
+ if (i < config.sessions - 1) {
136
+ await sleep(spawnPeriod)
137
+ }
138
+ }
139
+ const elapsed = Math.round((Date.now() - startTime) / 1000)
140
+ const spawnRate = (config.sessions * config.tabsPerSession) / elapsed
141
+ log.debug(`${config.sessions * config.tabsPerSession} pages started in ${elapsed}s (${spawnRate.toFixed(2)}/s)`)
111
142
  }
112
143
 
113
- // Download browser if necessary.
114
- if (!config.chromiumUrl && !config.chromiumPath) {
115
- await checkChromeExecutable()
144
+ if (config.runDuration || config.vmafPath || config.visqolPath) {
145
+ setTimeout(() => this.stop(), config.runDuration * 1000)
116
146
  }
147
+ }
117
148
 
118
- // Start session function.
119
- const startLocalSession = async (id: number, spawnPeriod: number): Promise<void> => {
120
- const throttleIndex = getSessionThrottleIndex(id)
121
- const mediaPath = mediaPaths.length ? mediaPaths[id % mediaPaths.length] : undefined
122
- const session = new Session({
123
- ...config,
124
- mediaPath,
125
- spawnPeriod,
126
- id,
127
- throttleIndex,
128
- })
129
- session.once('stop', () => {
130
- console.warn(`Session ${id} stopped, reloading...`)
131
- setTimeout(startLocalSession, spawnPeriod, id)
132
- })
133
- stats.addSession(session)
134
- await session.start()
135
- }
149
+ private async startSession(id: number, spawnPeriod: number) {
150
+ log.debug(`startSession ${id}`)
151
+ const throttleIndex = getSessionThrottleIndex(id)
152
+ const mediaPath = this.mediaPaths.length ? this.mediaPaths[id % this.mediaPaths.length] : undefined
153
+ const session = new Session({
154
+ ...this.config,
155
+ mediaPath,
156
+ spawnPeriod,
157
+ id,
158
+ throttleIndex,
159
+ })
160
+ session.once('stop', () => {
161
+ console.warn(`Session ${id} stopped, reloading...`)
162
+ setTimeout(() => this.startSession(id, spawnPeriod), spawnPeriod)
163
+ })
164
+ this.stats.addSession(session)
165
+ await session.start()
166
+ }
136
167
 
137
- // Start the local sessions.
138
- if (config.randomAudioPeriod) {
139
- startRandomActivateAudio(
140
- stats.sessions,
141
- config.randomAudioPeriod,
142
- config.randomAudioProbability,
143
- config.randomAudioRange,
144
- )
168
+ private async postTest() {
169
+ log.debug('postTest')
170
+
171
+ // vmaf score.
172
+ if (this.config.vmafPath) {
173
+ console.log('Calculating VMAF score...')
174
+ try {
175
+ await calculateVmafScore(this.config)
176
+ } catch (err: unknown) {
177
+ log.error(`vmaf score error: ${(err as Error).stack}`)
178
+ }
145
179
  }
146
- const spawnPeriod = 1000 / config.spawnRate
147
- log.debug(`Starting ${config.sessions} sessions (spawnPeriod: ${spawnPeriod}ms)`)
148
- const startTime = Date.now()
149
- for (let i = 0; i < config.sessions; i += 1) {
150
- const id = stats.consumeSessionId(config.tabsPerSession)
151
- await startLocalSession(id, spawnPeriod)
152
- // If not the last session, sleep
153
- if (i < config.sessions - 1) {
154
- await sleep(spawnPeriod)
180
+
181
+ // visqol score
182
+ if (this.config.visqolPath) {
183
+ console.log('Calculating Visqol score...')
184
+ try {
185
+ await calculateVisqolScore(this.config)
186
+ } catch (err: unknown) {
187
+ log.error(`visqol score error: ${(err as Error).stack}`)
155
188
  }
156
189
  }
157
- const elapsed = Math.round((Date.now() - startTime) / 1000)
158
- const spawnRate = (config.sessions * config.tabsPerSession) / elapsed
159
- log.debug(`${config.sessions * config.tabsPerSession} pages started in ${elapsed}s (${spawnRate.toFixed(2)}/s)`)
160
190
  }
161
191
 
162
- return {
163
- stats,
164
- stop: async (): Promise<void> => {
165
- log.debug('Stopping')
192
+ async stop(canceled = false) {
193
+ log.debug(`stop (canceled: ${canceled})`)
166
194
 
167
- stopRandomActivateAudio()
195
+ stopRandomActivateAudio()
168
196
 
169
- await stats.stop()
197
+ await this.stats.stop()
170
198
 
171
- if (config.throttleConfig) {
172
- await stopThrottle()
173
- }
199
+ if (this.config.throttleConfig) {
200
+ await stopThrottle()
201
+ }
174
202
 
175
- stopTimers()
203
+ stopTimers()
176
204
 
177
- await postTest(config)
205
+ await this.postTest()
178
206
 
179
- // Copy docker logs to data directory.
180
- if (config.pageLogPath) {
181
- try {
182
- const logPath = await getDockerLogsPath()
183
- const dataDir = path.dirname(config.pageLogPath)
184
- await fs.promises.cp(logPath, path.resolve(dataDir, 'docker.log'))
185
- } catch (err: unknown) {
186
- log.debug(`docker logs not found: ${(err as Error).message}`)
187
- }
207
+ // Copy docker logs to data directory.
208
+ if (this.config.pageLogPath) {
209
+ try {
210
+ const logPath = await getDockerLogsPath()
211
+ const dataDir = path.dirname(this.config.pageLogPath)
212
+ await fs.promises.cp(logPath, path.resolve(dataDir, 'docker.log'))
213
+ } catch (err: unknown) {
214
+ log.debug(`docker logs not found: ${(err as Error).message}`)
188
215
  }
216
+ }
189
217
 
190
- server?.stop()
218
+ this.server?.stop()
191
219
 
192
- log.debug('Stopped')
193
- },
220
+ this.emit('stop', canceled)
194
221
  }
195
222
  }
196
223
 
@@ -200,7 +227,7 @@ export async function setupApplication(config: Config): Promise<{ stats: Stats;
200
227
  async function main(): Promise<void> {
201
228
  showHelpOrVersion()
202
229
 
203
- let config: Config
230
+ let configs: Config[]
204
231
 
205
232
  if (process.argv.slice(2).includes('--prompt')) {
206
233
  const params = await loadConfigFromPrompt(
@@ -213,39 +240,36 @@ async function main(): Promise<void> {
213
240
  console.log(json5.stringify(params, null, 2))
214
241
  process.exit(0)
215
242
  }
216
- config = await loadConfig(undefined, params)
243
+ configs = await loadConfig(undefined, params)
217
244
  } else {
218
- config = await loadConfig(process.argv[2])
245
+ configs = await loadConfig(process.argv[2])
219
246
  }
220
247
 
221
- if (config.vmafPrepareVideo) {
222
- await prepareVideo(config, true)
223
- process.exit(0)
224
- }
248
+ if (!configs.length) throw new Error('No configuration found')
225
249
 
226
- if (config.vmafProcessVideo) {
227
- await convertToIvf(
228
- config.vmafProcessVideo,
229
- config.vmafVideoCrop,
230
- config.vmafKeepSourceFiles,
231
- config.vmafSkipDuplicated,
232
- )
233
- process.exit(0)
234
- }
250
+ let application: Application
251
+ const runNext = () => {
252
+ const config = configs.splice(0, 1)[0]
235
253
 
236
- const { stop: stopApplication } = await setupApplication(config)
254
+ application = new Application(config)
255
+ application.once('stop', canceled => {
256
+ if (!canceled && configs.length) {
257
+ log.info(`Application stopped, running next (${configs.length} left)...`)
258
+ runNext()
259
+ } else {
260
+ process.exit(0)
261
+ }
262
+ })
263
+ return application.start()
264
+ }
237
265
 
238
- const stop = async (): Promise<void> => {
266
+ const stop = async () => {
239
267
  console.log('Exiting...')
240
-
241
- await stopApplication()
242
-
243
- process.exit(0)
268
+ await application.stop(true)
244
269
  }
245
270
  registerExitHandler(() => stop())
246
271
 
247
- // Stop after a configured duration.
248
- setTimeout(stop, config.runDuration * 1000)
272
+ await runNext()
249
273
 
250
274
  // Command line interface.
251
275
  if (process.stdin && process.stdin.setRawMode) {
package/src/config.ts CHANGED
@@ -6,6 +6,7 @@ import path, { join } from 'path'
6
6
  import json5 from 'json5'
7
7
  import yaml from 'yaml'
8
8
  import toml from 'toml'
9
+ import fs from 'fs'
9
10
 
10
11
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
12
  const puppeteer = require('puppeteer-core')
@@ -56,8 +57,7 @@ convict.addParser([
56
57
  { extension: 'toml', parse: toml.parse },
57
58
  ])
58
59
 
59
- // config schema
60
- const configSchema = convict({
60
+ const configSchema = {
61
61
  url: {
62
62
  doc: `The page url to load.`,
63
63
  format: String,
@@ -859,7 +859,7 @@ E.g. \`{ w: "iw-10", h: "ih-5", x: "10", y: '5' }\``,
859
859
  env: 'VISQOL_KEEP_SOURCE_FILES',
860
860
  arg: 'visqol-keep-source-files',
861
861
  },
862
- })
862
+ }
863
863
 
864
864
  type ConfigDocs = Record<string, { doc: string; format: string; default: string }>
865
865
 
@@ -897,10 +897,10 @@ function formatDocs(
897
897
  * It returns the formatted configuration docs.
898
898
  */
899
899
  export function getConfigDocs(): ConfigDocs {
900
- return formatDocs({}, null, configSchema.getSchema())
900
+ return formatDocs({}, null, convict(configSchema).getSchema())
901
901
  }
902
902
 
903
- const _schemaProperties = configSchema.getProperties()
903
+ const _schemaProperties = convict(configSchema).getProperties()
904
904
 
905
905
  /** [[include:config.md]] */
906
906
  export type Config = typeof _schemaProperties
@@ -909,7 +909,8 @@ export type Config = typeof _schemaProperties
909
909
  * Loads the config object.
910
910
  */
911
911
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
912
- export async function loadConfig(filePath?: string, values?: any): Promise<Config> {
912
+ export async function loadConfig(filePath?: string, values?: any): Promise<Config[]> {
913
+ const configs: Config[] = []
913
914
  if (filePath) {
914
915
  if (filePath.startsWith('http')) {
915
916
  log.debug(`Loading config from url: ${filePath}`)
@@ -917,41 +918,45 @@ export async function loadConfig(filePath?: string, values?: any): Promise<Confi
917
918
  if (!res?.data) {
918
919
  throw new Error(`Failed to download configuration from: ${filePath}`)
919
920
  }
920
- const values =
921
+ values =
921
922
  res.contentType === 'application/x-yaml'
922
923
  ? yaml.parse(res.data)
923
924
  : res.contentType === 'application/toml'
924
925
  ? toml.parse(res.data)
925
926
  : json5.parse(res.data)
926
- configSchema.load(values)
927
927
  } else if (existsSync(filePath)) {
928
928
  log.debug(`Loading config from local file: ${filePath}`)
929
929
  if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) {
930
930
  const module = await import(/* webpackIgnore: true */ path.resolve(filePath))
931
- configSchema.load(await module.default())
931
+ values = await module.default()
932
932
  } else {
933
- configSchema.loadFile(filePath)
933
+ const data = String(await fs.promises.readFile(filePath))
934
+ values =
935
+ filePath.endsWith('.yml') || filePath.endsWith('.yaml')
936
+ ? yaml.parse(data)
937
+ : filePath.endsWith('.toml')
938
+ ? toml.parse(data)
939
+ : json5.parse(data)
934
940
  }
935
941
  }
936
- } else if (values) {
937
- log.debug('Loading config from values.')
938
- configSchema.load(values)
939
- } else {
940
- log.debug('Loading config from default values.')
941
- configSchema.load({})
942
942
  }
943
-
944
- configSchema.validate({ allowed: 'strict' })
945
- const config = configSchema.getProperties()
946
-
947
- log.debug('Using config:', config)
948
- return config
943
+ if (!Array.isArray(values)) {
944
+ values = [values || {}]
945
+ }
946
+ for (const value of values) {
947
+ const schema = convict(configSchema)
948
+ schema.load(value || {})
949
+ schema.validate({ allowed: 'strict' })
950
+ configs.push(schema.getProperties())
951
+ }
952
+ log.debug('Using config:', configs)
953
+ return configs
949
954
  }
950
955
 
951
956
  function getFunctionDeclaration() {
952
957
  const properties: Record<string, { type: string; description: string; nullable?: boolean }> = {}
953
958
  const required: string[] = []
954
- const schema = configSchema.getSchema()
959
+ const schema = convict(configSchema).getSchema()
955
960
 
956
961
  Object.entries(schema._cvtProperties).forEach(([name, value]) => {
957
962
  const { format, doc, nullable } = value as SchemaObj
package/src/server.ts CHANGED
@@ -11,10 +11,12 @@ import { WebSocketServer } from 'ws'
11
11
  import zlib from 'zlib'
12
12
  import auth from 'basic-auth'
13
13
 
14
- import { loadConfig } from './config'
14
+ import { Config, loadConfig } from './config'
15
15
  import { Session, SessionParams } from './session'
16
16
  import { Stats } from './stats'
17
17
  import { logger, runShellCommand, getDockerLogsPath } from './utils'
18
+ import { getSessionThrottleIndex } from '@vpalmisano/throttler'
19
+ import { MediaPath, prepareFakeMedia } from './media'
18
20
 
19
21
  const log = logger('webrtcperf:server')
20
22
 
@@ -272,7 +274,8 @@ export class Server {
272
274
  private async putSession(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
273
275
  log.debug(`PUT /session`, req.body)
274
276
  try {
275
- const id = this.stats.consumeSessionId()
277
+ const config = req.body as Config
278
+ const id = this.stats.consumeSessionId(config.tabsPerSession)
276
279
  await this.startLocalSession(id, req.body)
277
280
  res.json({
278
281
  message: `Session created`,
@@ -293,10 +296,10 @@ export class Server {
293
296
  private async putSessions(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
294
297
  log.debug(`PUT /sessions`, req.body)
295
298
  try {
296
- const { sessions } = req.body
299
+ const { sessions, tabsPerSession } = req.body as Config
297
300
  const sessionsIds = []
298
301
  for (let i = 0; i < sessions; i++) {
299
- const id = this.stats.sessions.size
302
+ const id = this.stats.consumeSessionId(tabsPerSession)
300
303
  await this.startLocalSession(id, req.body)
301
304
  sessionsIds.push(id)
302
305
  }
@@ -471,12 +474,25 @@ export class Server {
471
474
  * @param config The session configuration.
472
475
  */
473
476
  private async startLocalSession(id: number, config: SessionParams): Promise<Session> {
474
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
475
- const sessionConfig = loadConfig(undefined, config) as any
476
- const session = new Session({ ...sessionConfig, id })
477
+ const configs = await loadConfig(undefined, config)
478
+ const sessionConfig = configs[0]
479
+ const throttleIndex = getSessionThrottleIndex(id)
480
+ const spawnPeriod = 1000 / sessionConfig.spawnRate
481
+
482
+ // Prepare fake video and audio.
483
+ const mediaPaths: MediaPath[] = []
484
+ if (sessionConfig.videoPath) {
485
+ for (const videoPath of sessionConfig.videoPath.split(',')) {
486
+ const ret = await prepareFakeMedia({ ...sessionConfig, videoPath })
487
+ mediaPaths.push(ret)
488
+ }
489
+ }
490
+ const mediaPath = mediaPaths.length ? mediaPaths[id % mediaPaths.length] : undefined
491
+
492
+ const session = new Session({ ...sessionConfig, throttleIndex, spawnPeriod, mediaPath, id })
477
493
  session.once('stop', () => {
478
494
  console.warn(`Session ${id} stopped, reloading...`)
479
- setTimeout(this.startLocalSession.bind(this), sessionConfig.spawnPeriod, id, config)
495
+ setTimeout(this.startLocalSession.bind(this), spawnPeriod, id, config)
480
496
  })
481
497
  this.stats.addSession(session)
482
498
  try {
@@ -510,13 +526,17 @@ export class Server {
510
526
  log.debug('start')
511
527
  if (this.serverUseHttps) {
512
528
  const destDir = path.join(os.homedir(), '.webrtcperf/ssl')
513
- await runShellCommand(
514
- `mkdir -p ${destDir} && openssl req -newkey rsa:2048 -nodes -keyout ${destDir}/domain.key -x509 -days 365 -out ${destDir}/domain.crt -subj "/C=EU/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com"`,
515
- )
529
+ const keyPath = path.join(destDir, 'domain.key')
530
+ const crtPath = path.join(destDir, 'domain.crt')
531
+ if (!fs.existsSync(keyPath) || !fs.existsSync(crtPath)) {
532
+ await runShellCommand(
533
+ `mkdir -p ${destDir} && openssl req -newkey rsa:2048 -nodes -keyout ${keyPath} -x509 -days 365 -out ${crtPath} -subj "/C=EU/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com"`,
534
+ )
535
+ }
516
536
  this.server = _createServer(
517
537
  {
518
- key: fs.readFileSync(`${destDir}/domain.key`),
519
- cert: fs.readFileSync(`${destDir}/domain.crt`),
538
+ key: fs.readFileSync(keyPath),
539
+ cert: fs.readFileSync(crtPath),
520
540
  },
521
541
  this.app,
522
542
  )