@vpalmisano/webrtcperf 4.1.7 → 4.1.10

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/config.ts CHANGED
@@ -3,11 +3,14 @@ import { ipaddress, url } from 'convict-format-with-validator'
3
3
  import { existsSync } from 'fs'
4
4
  import os from 'os'
5
5
  import { join } from 'path'
6
+ import json5 from 'json5'
7
+ import yaml from 'yaml'
8
+ import toml from 'toml'
6
9
 
7
10
  // eslint-disable-next-line @typescript-eslint/no-require-imports
8
11
  const puppeteer = require('puppeteer-core')
9
12
 
10
- import { logger } from './utils'
13
+ import { downloadUrl, logger } from './utils'
11
14
  const log = logger('webrtcperf:config')
12
15
 
13
16
  const float = {
@@ -47,6 +50,12 @@ const index = {
47
50
 
48
51
  addFormats({ ipaddress, url, float, index })
49
52
 
53
+ convict.addParser([
54
+ { extension: 'json', parse: json5.parse },
55
+ { extension: ['yml', 'yaml'], parse: yaml.parse },
56
+ { extension: 'toml', parse: toml.parse },
57
+ ])
58
+
50
59
  // config schema
51
60
  const configSchema = convict({
52
61
  url: {
@@ -70,9 +79,16 @@ session, \`$i\` the tab absolute index.`,
70
79
  },
71
80
  customUrlHandler: {
72
81
  doc: `This argument specifies the file path for the custom page URL handler that will be exported by default. \
73
- The custom page URL handler allows you to define custom URLs that can be used to open your application, \
74
- and provides the following variables for customization: \`$p\`: the process pid, \`$s\`: the session index, \
75
- \`$S\`: the total sessions, \`$t\`: the tab index, \`$T\`: the total tabs per session, \`$i\`: the tab absolute index.
82
+ The custom page URL handler allows you to define custom URLs that can be used to open your application. \
83
+ The handler function will be called with the following variables: \
84
+ - sessions: the total number of sessions; \
85
+ - tabsPerSession: the total number of tabs per session; \
86
+ - id: the session global index (0-indexed); \
87
+ - index: the tab global index (0-indexed); \
88
+ - tabIndex: the tab index in the current session (0-indexed); \
89
+ - pid: the process pid; \
90
+ - env: the environment variables object; \
91
+ - params: the script parameters object. \
76
92
  You can use these variables to create custom URL schemes that suit your application's needs.`,
77
93
  format: String,
78
94
  default: '',
@@ -183,8 +199,8 @@ seconds.`,
183
199
  },
184
200
  randomAudioPeriod: {
185
201
  doc: `If not zero, it specifies the maximum period in seconds after which \
186
- a new random active tab is selected, enabling the getUserMedia audio tracks in \
187
- that tab and disabling all of the other tabs.`,
202
+ a new random active session is selected, enabling the getUserMedia audio tracks in \
203
+ that session and disabling all of the others.`,
188
204
  format: 'nat',
189
205
  default: 0,
190
206
  env: 'RANDOM_AUDIO_PERIOD',
@@ -199,10 +215,11 @@ the selected audio will be activated (value: 0-100).`,
199
215
  arg: 'random-audio-probability',
200
216
  },
201
217
  randomAudioRange: {
202
- doc: `When using random audio period, it defines the number of pages \
203
- to be included into the random selection.`,
204
- format: 'nat',
205
- default: 0,
218
+ doc: `When using random audio period, it defines the session indexes \
219
+ to be included into the random selection (default: include all the sessions).`,
220
+ format: 'index',
221
+ default: 'true',
222
+ nullable: true,
206
223
  env: 'RANDOM_AUDIO_RANGE',
207
224
  arg: 'random-audio-range',
208
225
  },
@@ -274,12 +291,13 @@ The total decoders count is stored into the virtual file \`/dev/shm/chromium-vid
274
291
  env: 'MAX_VIDEO_DECODERS',
275
292
  arg: 'max-video-decoders',
276
293
  },
277
- maxVideoDecodersAt: {
278
- doc: `Applies the maxVideoDecoders option starting from this session \`ID\`.`,
279
- format: Number,
280
- default: -1,
281
- env: 'MAX_VIDEO_DECODERS_AT',
282
- arg: 'max-video-decoders-at',
294
+ maxVideoDecodersRange: {
295
+ doc: `It applies the max video decoders option to the sessions included into this list (default: include all the sessions)`,
296
+ format: 'index',
297
+ default: 'true',
298
+ nullable: true,
299
+ env: 'MAX_VIDEO_DECODERS_RANGE',
300
+ arg: 'max-video-decoders-range',
283
301
  },
284
302
  incognito: {
285
303
  doc: `Runs the browser in incognito mode.`,
@@ -289,13 +307,12 @@ The total decoders count is stored into the virtual file \`/dev/shm/chromium-vid
289
307
  arg: 'incognito',
290
308
  },
291
309
  display: {
292
- doc: `If unset, the browser will run in headless mode.
310
+ doc: `If unset, the browser will run in headless mode, otherwise it will run in normal windowed mode.
293
311
  When running on MacOS or Windows, set it to any not-empty string.
294
312
  On Linux, set it to a valid X server \`DISPLAY\` string (e.g. \`:0\`).`,
295
313
  format: String,
296
314
  default: '',
297
315
  nullable: true,
298
- env: 'DISPLAY',
299
316
  arg: 'display',
300
317
  },
301
318
  /* audioRedForOpus: {
@@ -337,7 +354,7 @@ calculated using \`Date.now()\``,
337
354
  enableDetailedStats: {
338
355
  doc: `If detailed participant metrics values should be collected.`,
339
356
  format: 'index',
340
- default: '',
357
+ default: '0-24',
341
358
  nullable: true,
342
359
  env: 'ENABLE_DETAILED_STATS',
343
360
  arg: 'enable-detailed-stats',
@@ -358,7 +375,7 @@ calculated using \`Date.now()\``,
358
375
  },
359
376
  pageLogFilter: {
360
377
  doc: `If set, only the logs with the matching text will be printed \
361
- on console. Regexp string allowed.`,
378
+ on the console. Regexp string allowed.`,
362
379
  format: String,
363
380
  default: '',
364
381
  nullable: true,
@@ -366,17 +383,25 @@ on console. Regexp string allowed.`,
366
383
  arg: 'page-log-filter',
367
384
  },
368
385
  pageLogPath: {
369
- doc: `If set, page console logs will be saved on the selected file path.`,
386
+ doc: `If set, the page console logs will be saved on the selected file path.`,
370
387
  format: String,
371
388
  default: '',
372
389
  nullable: true,
373
390
  env: 'PAGE_LOG_PATH',
374
391
  arg: 'page-log-path',
375
392
  },
393
+ enableBrowserLogging: {
394
+ doc: `It enables the Chromium browser logging for the specified session indexes. It requires the page log path option to be set. `,
395
+ format: 'index',
396
+ nullable: true,
397
+ default: '',
398
+ env: 'ENABLE_BROWSER_LOGGING',
399
+ arg: 'enable-browser-logging',
400
+ },
376
401
  userAgent: {
377
402
  doc: `The user agent override.`,
378
403
  format: String,
379
- default: '',
404
+ default: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
380
405
  nullable: true,
381
406
  env: 'USER_AGENT',
382
407
  arg: 'user-agent',
@@ -445,14 +470,6 @@ use the host X server instance.`,
445
470
  env: 'ENABLE_GPU',
446
471
  arg: 'enable-gpu',
447
472
  },
448
- enableBrowserLogging: {
449
- doc: `It enables the Chromium browser logging for the specified session indexes.`,
450
- format: 'index',
451
- nullable: true,
452
- default: '',
453
- env: 'ENABLE_BROWSER_LOGGING',
454
- arg: 'enable-browser-logging',
455
- },
456
473
  blockedUrls: {
457
474
  doc: `A comma-separated list of request URLs that will be automatically \
458
475
  blocked.`,
@@ -846,15 +863,30 @@ export type Config = typeof _schemaProperties
846
863
  * Loads the config object.
847
864
  */
848
865
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
849
- export function loadConfig(filePath?: string, values?: any): Config {
850
- if (filePath && existsSync(filePath)) {
851
- log.debug(`Loading config from ${filePath}`)
852
- configSchema.loadFile(filePath)
866
+ export async function loadConfig(filePath?: string, values?: any): Promise<Config> {
867
+ if (filePath) {
868
+ if (filePath.startsWith('http')) {
869
+ log.debug(`Loading config from url: ${filePath}`)
870
+ const res = await downloadUrl(filePath)
871
+ if (!res?.data) {
872
+ throw new Error(`Failed to download configuration from: ${filePath}`)
873
+ }
874
+ const values =
875
+ res.contentType === 'application/x-yaml'
876
+ ? yaml.parse(res.data)
877
+ : res.contentType === 'application/toml'
878
+ ? toml.parse(res.data)
879
+ : json5.parse(res.data)
880
+ configSchema.load(values)
881
+ } else if (existsSync(filePath)) {
882
+ log.debug(`Loading config from local file: ${filePath}`)
883
+ configSchema.loadFile(filePath)
884
+ }
853
885
  } else if (values) {
854
886
  log.debug('Loading config from values.')
855
887
  configSchema.load(values)
856
888
  } else {
857
- log.debug('Using default values.')
889
+ log.debug('Loading config from default values.')
858
890
  configSchema.load({})
859
891
  }
860
892
 
package/src/server.ts CHANGED
@@ -119,14 +119,14 @@ export class Server {
119
119
  log.error(`mkdir ${this.serverData} error: ${err.message}`)
120
120
  })
121
121
  this.app.get('/data', this.getDataArchive.bind(this))
122
- this.app.get('/data/*', this.getData.bind(this))
122
+ this.app.get('/data/:path', this.getData.bind(this))
123
123
  }
124
124
  if (this.videoCachePath) {
125
125
  log.debug(`using videoCachePath: ${this.videoCachePath}`)
126
126
  fs.promises.mkdir(this.videoCachePath, { recursive: true }).catch(err => {
127
127
  log.error(`mkdir ${this.videoCachePath} error: ${err.message}`)
128
128
  })
129
- this.app.get('/cache/*', this.getCache.bind(this))
129
+ this.app.get('/cache/:path', this.getCache.bind(this))
130
130
  }
131
131
 
132
132
  this.app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -429,7 +429,7 @@ export class Server {
429
429
  * content in tar.gz format.
430
430
  */
431
431
  private getData(req: express.Request, res: express.Response, next: express.NextFunction): void {
432
- const paramPath = path.normalize(req.params[0]).replace(/^(\.\.(\/|\\|$))+/, '')
432
+ const paramPath = path.normalize(req.params.path).replace(/^(\.\.(\/|\\|$))+/, '')
433
433
  log.debug(`GET /data/${paramPath}`, req.query)
434
434
  const fpath = path.resolve(this.serverData, paramPath)
435
435
  if (!fs.existsSync(fpath)) {
@@ -453,7 +453,7 @@ export class Server {
453
453
  }
454
454
 
455
455
  private getCache(req: express.Request, res: express.Response, next: express.NextFunction): void {
456
- const paramPath = path.normalize(req.params[0]).replace(/^(\.\.(\/|\\|$))+/, '')
456
+ const paramPath = path.normalize(req.params.path).replace(/^(\.\.(\/|\\|$))+/, '')
457
457
  log.debug(`GET /cache/${paramPath}`, req.query)
458
458
  const fpath = path.resolve(this.videoCachePath, paramPath)
459
459
  if (!fs.existsSync(fpath)) {
package/src/session.ts CHANGED
@@ -15,6 +15,7 @@ import puppeteer, {
15
15
  CDPSession,
16
16
  CookieParam,
17
17
  ElementHandle,
18
+ ImageFormat,
18
19
  KeyInput,
19
20
  Metrics,
20
21
  Page,
@@ -176,7 +177,7 @@ export interface SessionParams {
176
177
  debuggingAddress: string
177
178
  randomAudioPeriod: number
178
179
  maxVideoDecoders: number
179
- maxVideoDecodersAt: number
180
+ maxVideoDecodersRange: string
180
181
  incognito: boolean
181
182
  serverPort: number
182
183
  serverSecret: string
@@ -260,7 +261,7 @@ export class Session extends EventEmitter {
260
261
  private readonly debuggingAddress: string
261
262
  private readonly randomAudioPeriod: number
262
263
  private readonly maxVideoDecoders: number
263
- private readonly maxVideoDecodersAt: number
264
+ private readonly maxVideoDecodersRange: string
264
265
  private readonly incognito: boolean
265
266
  private readonly serverPort: number
266
267
  private readonly serverSecret: string
@@ -390,7 +391,7 @@ export class Session extends EventEmitter {
390
391
  debuggingAddress,
391
392
  randomAudioPeriod,
392
393
  maxVideoDecoders,
393
- maxVideoDecodersAt,
394
+ maxVideoDecodersRange,
394
395
  incognito,
395
396
  serverPort,
396
397
  serverSecret,
@@ -464,7 +465,7 @@ export class Session extends EventEmitter {
464
465
  this.userAgent = userAgent
465
466
  this.randomAudioPeriod = randomAudioPeriod
466
467
  this.maxVideoDecoders = maxVideoDecoders
467
- this.maxVideoDecodersAt = maxVideoDecodersAt
468
+ this.maxVideoDecodersRange = maxVideoDecodersRange
468
469
  this.incognito = incognito
469
470
  this.serverPort = serverPort
470
471
  this.serverSecret = serverSecret
@@ -596,7 +597,7 @@ export class Session extends EventEmitter {
596
597
  env.CHROME_LOG_FILE = path.resolve(pageLogDir, `chrome-${this.id}.log`)
597
598
  }
598
599
 
599
- if (this.maxVideoDecoders !== -1 && this.id >= this.maxVideoDecodersAt) {
600
+ if (this.maxVideoDecoders !== -1 && enabledForSession(this.id, this.maxVideoDecodersRange)) {
600
601
  fieldTrials = `WebRTC-MaxVideoDecoders/${this.maxVideoDecoders}/` + fieldTrials
601
602
  }
602
603
  if (fieldTrials.length) {
@@ -1921,7 +1922,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1921
1922
  if (!page) {
1922
1923
  throw new Error(`Page ${index} not found`)
1923
1924
  }
1924
- const filePath = `/tmp/screenshot-${index}.${format}`
1925
+ const filePath = `/tmp/screenshot-${index}.${format}` as `${string}.${ImageFormat}`
1925
1926
  await page.screenshot({
1926
1927
  path: filePath,
1927
1928
  fullPage: true,
package/src/stats.ts CHANGED
@@ -795,10 +795,12 @@ export class Stats extends events.EventEmitter {
795
795
  this.collectedStatsConfig.pages = 0
796
796
  this.collectedStatsConfig.startTime = this.startTimestamp
797
797
  // Reset collectedStats object.
798
- Object.values(this.collectedStats).forEach(stats => {
798
+ const prevByParticipantAndTrackStats = {} as Record<string, Set<string>>
799
+ Object.entries(this.collectedStats).forEach(([name, stats]) => {
799
800
  stats.all.reset()
800
801
  Object.values(stats.byHost).forEach(s => s.reset())
801
802
  Object.values(stats.byCodec).forEach(s => s.reset())
803
+ prevByParticipantAndTrackStats[name] = new Set(Object.keys(stats.byParticipantAndTrack))
802
804
  stats.byParticipantAndTrack = {}
803
805
  })
804
806
  for (const [sessionId, session] of this.sessions.entries()) {
@@ -812,6 +814,7 @@ export class Stats extends events.EventEmitter {
812
814
  //log.log(name, obj)
813
815
  try {
814
816
  const collectedStats = this.collectedStats[name]
817
+ const prevByParticipantAndTrack = prevByParticipantAndTrackStats[name]
815
818
  if (typeof obj === 'number' && isFinite(obj)) {
816
819
  collectedStats.all.push(obj)
817
820
  } else {
@@ -828,7 +831,9 @@ export class Stats extends events.EventEmitter {
828
831
  stats.push(value)
829
832
  // Push participant and track values.
830
833
  if (enabledForSession(sessionId, this.enableDetailedStats) && participantName) {
831
- collectedStats.byParticipantAndTrack[`${participantName}:${trackId || ''}`] = value
834
+ const label = `${participantName}:${trackId || ''}`
835
+ collectedStats.byParticipantAndTrack[label] = value
836
+ prevByParticipantAndTrack.delete(label)
832
837
  }
833
838
  } else if (typeof value === 'string') {
834
839
  // Codec stats.
@@ -869,6 +874,7 @@ export class Stats extends events.EventEmitter {
869
874
  return
870
875
  }
871
876
  const collectedStats = this.collectedStats[name]
877
+ const prevByParticipantAndTrack = prevByParticipantAndTrackStats[name]
872
878
  collectedStats.all.push(stats.all)
873
879
  Object.entries(stats.byHost).forEach(([host, values]) => {
874
880
  if (!collectedStats.byHost[host]) {
@@ -884,6 +890,7 @@ export class Stats extends events.EventEmitter {
884
890
  })
885
891
  Object.entries(stats.byParticipantAndTrack).forEach(([label, value]) => {
886
892
  collectedStats.byParticipantAndTrack[label] = value
893
+ prevByParticipantAndTrack.delete(label)
887
894
  })
888
895
  })
889
896
  }
@@ -930,7 +937,7 @@ export class Stats extends events.EventEmitter {
930
937
  await Promise.allSettled([
931
938
  this.writeStats(),
932
939
  this.writeDetailedStats(),
933
- this.sendToPushGateway(),
940
+ this.sendToPushGateway(prevByParticipantAndTrackStats),
934
941
  this.writeAlertRulesReport(),
935
942
  ])
936
943
  }
@@ -1062,7 +1069,7 @@ export class Stats extends events.EventEmitter {
1062
1069
  /**
1063
1070
  * sendToPushGateway
1064
1071
  */
1065
- async sendToPushGateway(): Promise<void> {
1072
+ async sendToPushGateway(removedByParticipantAndTrackStats: Record<string, Set<string>>): Promise<void> {
1066
1073
  if (!this.gateway || !this.running) {
1067
1074
  return
1068
1075
  }
@@ -1109,6 +1116,20 @@ export class Stats extends events.EventEmitter {
1109
1116
  })
1110
1117
  }
1111
1118
 
1119
+ // Remove metrics for removed participants and tracks.
1120
+ const removedByParticipantAndTrack = removedByParticipantAndTrackStats[name]
1121
+ if (removedByParticipantAndTrack) {
1122
+ for (const label of removedByParticipantAndTrack) {
1123
+ const [participantName, trackId] = label.split(':', 2)
1124
+ metric.value?.remove({
1125
+ participantName,
1126
+ trackId,
1127
+ datetime,
1128
+ ...this.customMetricsLabels,
1129
+ })
1130
+ }
1131
+ }
1132
+
1112
1133
  // Set alerts metrics.
1113
1134
  if (this.alertRules && this.alertRules[name]) {
1114
1135
  const rule = this.alertRules[name]
package/src/utils.ts CHANGED
@@ -20,7 +20,7 @@ import os, { networkInterfaces } from 'os'
20
20
  import path, { dirname } from 'path'
21
21
  import pidtree from 'pidtree'
22
22
  import pidusage from 'pidusage'
23
- import puppeteer, { Page } from 'puppeteer-core'
23
+ import puppeteer, { ImageFormat, Page } from 'puppeteer-core'
24
24
 
25
25
  import { Session } from './session'
26
26
 
@@ -255,7 +255,7 @@ export function startRandomActivateAudio(
255
255
  sessions: Map<number, Session>,
256
256
  randomAudioPeriod: number,
257
257
  randomAudioProbability: number,
258
- randomAudioRange: number,
258
+ randomAudioRange: string,
259
259
  ): void {
260
260
  if (randomActivateAudioRunning) return
261
261
  randomActivateAudioRunning = true
@@ -272,13 +272,13 @@ export function stopRandomActivateAudio(): void {
272
272
  * @param sessions The sessions Map
273
273
  * @param randomAudioPeriod If set, the function will be called in loop
274
274
  * @param randomAudioProbability The activation probability
275
- * @param randomAudioRange The number of pages to include into the automation
275
+ * @param randomAudioRange The page indexes to include into the automation
276
276
  */
277
277
  export async function randomActivateAudio(
278
278
  sessions: Map<number, Session>,
279
279
  randomAudioPeriod: number,
280
280
  randomAudioProbability: number,
281
- randomAudioRange: number,
281
+ randomAudioRange: string,
282
282
  ): Promise<void> {
283
283
  if (!randomAudioPeriod || !randomActivateAudioRunning) {
284
284
  return
@@ -287,13 +287,9 @@ export async function randomActivateAudio(
287
287
  let pages: (Page | null)[] = []
288
288
  for (const session of sessions.values()) {
289
289
  const sessionPages = [...session.pages.values()]
290
- if (randomAudioRange) {
291
- if (session.id > randomAudioRange) {
292
- break
293
- }
294
- sessionPages.splice(randomAudioRange - session.id)
290
+ if (enabledForSession(session.id, randomAudioRange)) {
291
+ pages = pages.concat(sessionPages)
295
292
  }
296
- pages = pages.concat(sessionPages)
297
293
  }
298
294
  // Remove pages with no audio tracks.
299
295
  for (const [i, page] of pages.entries()) {
@@ -380,6 +376,8 @@ export interface DownloadData {
380
376
  end: number
381
377
  /** Total returned size. */
382
378
  total: number
379
+ /** Content type. */
380
+ contentType: string
383
381
  }
384
382
 
385
383
  /**
@@ -452,12 +450,13 @@ export async function downloadUrl(
452
450
  } else {
453
451
  /* log.debug(`downloadUrl ${response.data.length} bytes, headers=${
454
452
  JSON.stringify(response.headers)}`); */
453
+ const contentType = response.headers['content-type']
455
454
  let start = 0
456
455
  let end = 0
457
456
  let total = 0
458
457
  if (response.headers['content-range']) {
459
- log.debug(`downloadUrl ${response.data.length} bytes, content-range=${response.headers['content-range']}`)
460
458
  const contentRange = response.headers['content-range'].split('/')
459
+ log.debug(`downloadUrl ${response.data.length} bytes, contentType=${contentType}, contentRange=${contentRange}`)
461
460
  const rangeParts = contentRange[0].split('-')
462
461
  total = parseInt(contentRange[1])
463
462
  if (rangeParts.length === 2) {
@@ -475,6 +474,7 @@ export async function downloadUrl(
475
474
  start,
476
475
  end,
477
476
  total,
477
+ contentType,
478
478
  }
479
479
  }
480
480
  }
@@ -613,7 +613,7 @@ SIGNALS.forEach(event =>
613
613
  export async function checkChromeExecutable(): Promise<string> {
614
614
  // eslint-disable-next-line @typescript-eslint/no-require-imports
615
615
  const { loadConfig } = require('./config')
616
- const config = loadConfig()
616
+ const config = await loadConfig()
617
617
  const cacheDir = path.join(os.homedir(), '.webrtcperf/chrome')
618
618
 
619
619
  const fixSemVer = (v: string) => v.split('.').slice(0, 3).join('.')
@@ -1037,7 +1037,8 @@ export async function pageScreenshot(
1037
1037
  if (!element) {
1038
1038
  throw new Error(`pageScreenshot selector "${selector}" not found`)
1039
1039
  }
1040
- await element.screenshot({ path: filePath })
1040
+ const path = filePath as `${string}.${ImageFormat}`
1041
+ await element.screenshot({ path })
1041
1042
  } catch (err) {
1042
1043
  log.error(`pageScreenshot error: ${(err as Error).message}`)
1043
1044
  } finally {
package/src/vmaf.ts CHANGED
@@ -587,8 +587,19 @@ ${splitFilter(['ref_vmaf', 'ref_psnr', preview ? 'ref_preview' : ''])};\
587
587
  }
588
588
 
589
589
  async function writeGraph(vmafLogPath: string) {
590
+ const {
591
+ CategoryScale,
592
+ Chart,
593
+ LinearScale,
594
+ LineController,
595
+ LineElement,
596
+ PointElement,
597
+ Legend,
598
+ Title,
599
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
600
+ } = require('chart.js')
590
601
  // eslint-disable-next-line @typescript-eslint/no-require-imports
591
- const { ChartJSNodeCanvas } = require('chartjs-node-canvas')
602
+ const { Canvas } = require('skia-canvas')
592
603
 
593
604
  const vmafLog = JSON.parse(await fs.promises.readFile(vmafLogPath, 'utf-8')) as {
594
605
  frames: {
@@ -625,13 +636,9 @@ async function writeGraph(vmafLogPath: string) {
625
636
  )
626
637
  .map(d => ({ x: d.x, y: d.y / d.count }))
627
638
 
628
- const chartJSNodeCanvas = new ChartJSNodeCanvas({
629
- width: 1280,
630
- height: 720,
631
- backgroundColour: 'white',
632
- })
633
-
634
- const buffer = await chartJSNodeCanvas.renderToBuffer({
639
+ Chart.register([CategoryScale, LineController, LineElement, LinearScale, PointElement, Legend, Title])
640
+ const canvas = new Canvas(1280, 720)
641
+ const chart = new Chart(canvas, {
635
642
  type: 'line',
636
643
  data: {
637
644
  labels: data.map(d => d.x),
@@ -652,7 +659,7 @@ async function writeGraph(vmafLogPath: string) {
652
659
  plugins: {
653
660
  title: {
654
661
  display: true,
655
- text: path.basename(vmafLogPath).replace('.vmaf.json', '').replace(/_/g, ' '),
662
+ text: path.basename(path.dirname(vmafLogPath)).replace(/_/g, ' '),
656
663
  },
657
664
  },
658
665
  scales: {
@@ -663,8 +670,8 @@ async function writeGraph(vmafLogPath: string) {
663
670
  },
664
671
  },
665
672
  })
666
-
667
- await fs.promises.writeFile(fpath, buffer)
673
+ await canvas.saveAs(fpath, { format: 'png', matte: 'white' })
674
+ chart.destroy()
668
675
  }
669
676
 
670
677
  interface Crop {