@vpalmisano/webrtcperf 4.1.5 → 4.1.9
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 +14 -9
- package/app.min.js +1 -1
- package/build/src/config.d.ts +3 -3
- package/build/src/config.js +36 -27
- package/build/src/config.js.map +1 -1
- package/build/src/server.js +4 -4
- package/build/src/server.js.map +1 -1
- package/build/src/session.d.ts +3 -3
- package/build/src/session.js +6 -6
- package/build/src/session.js.map +1 -1
- package/build/src/stats.d.ts +1 -1
- package/build/src/stats.js +24 -4
- package/build/src/stats.js.map +1 -1
- package/build/src/utils.d.ts +3 -3
- package/build/src/utils.js +5 -8
- package/build/src/utils.js.map +1 -1
- package/build/src/vmaf.js +19 -12
- package/build/src/vmaf.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -21
- package/src/config.ts +36 -27
- package/src/server.ts +4 -4
- package/src/session.ts +10 -9
- package/src/stats.ts +25 -4
- package/src/utils.ts +8 -11
- package/src/vmaf.ts +33 -14
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
180
|
+
maxVideoDecodersRange: string
|
|
180
181
|
incognito: boolean
|
|
181
182
|
serverPort: number
|
|
182
183
|
serverSecret: string
|
|
@@ -245,7 +246,7 @@ export class Session extends EventEmitter {
|
|
|
245
246
|
private readonly responseModifiers: Record<
|
|
246
247
|
string,
|
|
247
248
|
{
|
|
248
|
-
search?: RegExp
|
|
249
|
+
search?: string | RegExp
|
|
249
250
|
replace?: string
|
|
250
251
|
file?: string
|
|
251
252
|
headers?: Record<string, 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
|
|
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
|
-
|
|
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.
|
|
468
|
+
this.maxVideoDecodersRange = maxVideoDecodersRange
|
|
468
469
|
this.incognito = incognito
|
|
469
470
|
this.serverPort = serverPort
|
|
470
471
|
this.serverSecret = serverSecret
|
|
@@ -511,8 +512,8 @@ export class Session extends EventEmitter {
|
|
|
511
512
|
`responseModifiers replacements should be an array of { search, replace, body, headers } objects: ${replacements}`,
|
|
512
513
|
)
|
|
513
514
|
}
|
|
514
|
-
this.responseModifiers[url] = replacements.map(({ search, replace, file, headers }) => ({
|
|
515
|
-
search:
|
|
515
|
+
this.responseModifiers[url] = replacements.map(({ search, regexp, replace, file, headers }) => ({
|
|
516
|
+
search: regexp ? new RegExp(regexp, 'g') : search,
|
|
516
517
|
replace,
|
|
517
518
|
file,
|
|
518
519
|
headers,
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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()) {
|
|
@@ -1037,7 +1033,8 @@ export async function pageScreenshot(
|
|
|
1037
1033
|
if (!element) {
|
|
1038
1034
|
throw new Error(`pageScreenshot selector "${selector}" not found`)
|
|
1039
1035
|
}
|
|
1040
|
-
|
|
1036
|
+
const path = filePath as `${string}.${ImageFormat}`
|
|
1037
|
+
await element.screenshot({ path })
|
|
1041
1038
|
} catch (err) {
|
|
1042
1039
|
log.error(`pageScreenshot error: ${(err as Error).message}`)
|
|
1043
1040
|
} finally {
|
package/src/vmaf.ts
CHANGED
|
@@ -464,7 +464,12 @@ export async function runVmaf(
|
|
|
464
464
|
const sender = path.basename(referencePath).replace('.ivf', '')
|
|
465
465
|
const receiver = path.basename(degradedPath).replace('.ivf', '').split('_recv-by_')[1]
|
|
466
466
|
|
|
467
|
-
const {
|
|
467
|
+
const {
|
|
468
|
+
width: refWidth,
|
|
469
|
+
height: refHeight,
|
|
470
|
+
frameRate: refFrameRate,
|
|
471
|
+
frames: refFrames,
|
|
472
|
+
} = await parseIvf(referencePath, false)
|
|
468
473
|
const {
|
|
469
474
|
width: degWidth,
|
|
470
475
|
height: degHeight,
|
|
@@ -517,13 +522,20 @@ export async function runVmaf(
|
|
|
517
522
|
-i ${referencePath} \
|
|
518
523
|
`
|
|
519
524
|
|
|
525
|
+
let cropWidth = 0
|
|
526
|
+
const refFactor = refWidth / refHeight
|
|
527
|
+
const degFactor = degWidth / degHeight
|
|
528
|
+
if (refFactor > degFactor) {
|
|
529
|
+
cropWidth = Math.round(degFactor * refHeight)
|
|
530
|
+
}
|
|
531
|
+
|
|
520
532
|
const filter = `\
|
|
521
533
|
[0:v]\
|
|
522
|
-
scale=w=-1:h=${
|
|
534
|
+
scale=w=-1:h=${refHeight}:flags=bicubic,${cropWidth ? `crop=w=${cropWidth}:x=(iw-${cropWidth})/2,` : ''}\
|
|
523
535
|
${cropFilter(crop.deg, 0, ',')}\
|
|
524
536
|
${splitFilter(['deg_vmaf', 'deg_psnr', preview ? 'deg_preview' : ''])};\
|
|
525
537
|
[1:v]\
|
|
526
|
-
scale=w=-1:h=${
|
|
538
|
+
scale=w=-1:h=${refHeight}:flags=bicubic,\
|
|
527
539
|
${cropFilter(crop.ref, 0, ',')}\
|
|
528
540
|
${splitFilter(['ref_vmaf', 'ref_psnr', preview ? 'ref_preview' : ''])};\
|
|
529
541
|
[deg_vmaf][ref_vmaf]libvmaf=model='path=/usr/share/model/vmaf_v0.6.1.json':log_fmt=json:log_path=${vmafLogPath}:n_subsample=1:n_threads=${cpus}:shortest=1[vmaf];\
|
|
@@ -575,8 +587,19 @@ ${splitFilter(['ref_vmaf', 'ref_psnr', preview ? 'ref_preview' : ''])};\
|
|
|
575
587
|
}
|
|
576
588
|
|
|
577
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')
|
|
578
601
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
579
|
-
const {
|
|
602
|
+
const { Canvas } = require('skia-canvas')
|
|
580
603
|
|
|
581
604
|
const vmafLog = JSON.parse(await fs.promises.readFile(vmafLogPath, 'utf-8')) as {
|
|
582
605
|
frames: {
|
|
@@ -613,13 +636,9 @@ async function writeGraph(vmafLogPath: string) {
|
|
|
613
636
|
)
|
|
614
637
|
.map(d => ({ x: d.x, y: d.y / d.count }))
|
|
615
638
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
backgroundColour: 'white',
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
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, {
|
|
623
642
|
type: 'line',
|
|
624
643
|
data: {
|
|
625
644
|
labels: data.map(d => d.x),
|
|
@@ -640,7 +659,7 @@ async function writeGraph(vmafLogPath: string) {
|
|
|
640
659
|
plugins: {
|
|
641
660
|
title: {
|
|
642
661
|
display: true,
|
|
643
|
-
text: path.basename(
|
|
662
|
+
text: path.basename(path.dirname(vmafLogPath)).replace(/_/g, ' '),
|
|
644
663
|
},
|
|
645
664
|
},
|
|
646
665
|
scales: {
|
|
@@ -651,8 +670,8 @@ async function writeGraph(vmafLogPath: string) {
|
|
|
651
670
|
},
|
|
652
671
|
},
|
|
653
672
|
})
|
|
654
|
-
|
|
655
|
-
|
|
673
|
+
await canvas.saveAs(fpath, { format: 'png', matte: 'white' })
|
|
674
|
+
chart.destroy()
|
|
656
675
|
}
|
|
657
676
|
|
|
658
677
|
interface Crop {
|