@vpalmisano/webrtcperf 4.4.11 → 4.4.14

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.4.11",
3
+ "version": "4.4.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/vpalmisano/webrtcperf.git"
@@ -90,7 +90,7 @@
90
90
  "yaml": "^2.8.1"
91
91
  },
92
92
  "devDependencies": {
93
- "@eslint/js": "^9.35.0",
93
+ "@eslint/js": "^9.36.0",
94
94
  "@types/basic-auth": "^1.1.3",
95
95
  "@types/compression": "^1.8.1",
96
96
  "@types/convict": "^6.1.6",
@@ -109,7 +109,7 @@
109
109
  "@typescript-eslint/parser": "^8.44.0",
110
110
  "@vpalmisano/typedoc-cookie-consent": "^0.0.4",
111
111
  "@vpalmisano/typedoc-plugin-ga": "^1.0.6",
112
- "eslint": "^9.35.0",
112
+ "eslint": "^9.36.0",
113
113
  "eslint-config-prettier": "^10.1.8",
114
114
  "eslint-import-resolver-typescript": "^4.4.4",
115
115
  "eslint-plugin-import": "^2.32.0",
@@ -130,7 +130,8 @@
130
130
  },
131
131
  "optionalDependencies": {
132
132
  "chart.js": "^4.5.0",
133
- "skia-canvas": "^2.0.2",
133
+ "chartjs-chart-error-bars": "^4.4.4",
134
+ "skia-canvas": "^3.0.7",
134
135
  "zeromq": "^6.5.0"
135
136
  }
136
137
  }
package/src/index.ts CHANGED
@@ -9,3 +9,4 @@ export * from './stats'
9
9
  export * from './utils'
10
10
  export * from './vmaf'
11
11
  export * from './scenarios'
12
+ export * from './plot'
package/src/plot.ts ADDED
@@ -0,0 +1,163 @@
1
+ import fs from 'fs'
2
+
3
+ import { logger } from './utils'
4
+ import json5 from 'json5'
5
+
6
+ const log = logger('webrtcperf:plot')
7
+
8
+ export type PlotOptions = {
9
+ type?: string
10
+ title?: string
11
+ xLabel?: string
12
+ yLabel?: string
13
+ yMin?: number
14
+ yMax?: number
15
+ labels?: (string | number)[]
16
+ filePath?: string
17
+ }
18
+
19
+ export type PlotData = {
20
+ label: string
21
+ data: { x: number | string; y: number; yMin?: number; yMax?: number }[]
22
+ }
23
+
24
+ const SERIES_COLORS = [
25
+ 'rgba(33, 150, 243, 1)',
26
+ 'rgba(244, 67, 54, 1)',
27
+ 'rgba(76, 175, 80, 1)',
28
+ 'rgba(255, 193, 7, 1)',
29
+ 'rgba(156, 39, 176, 1)',
30
+ ]
31
+
32
+ export function plotConfig(options: PlotOptions, series: PlotData[]) {
33
+ log.debug('plotConfig')
34
+
35
+ return {
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ type: (options.type || 'line') as any,
38
+ options: {
39
+ plugins: {
40
+ title: options.title
41
+ ? {
42
+ display: true,
43
+ text: options.title,
44
+ }
45
+ : undefined,
46
+ zoom: {
47
+ pan: {
48
+ enabled: true,
49
+ mode: 'x',
50
+ modifierKey: 'ctrl',
51
+ },
52
+ zoom: {
53
+ drag: {
54
+ enabled: true,
55
+ },
56
+ mode: 'x',
57
+ },
58
+ },
59
+ },
60
+ scales: {
61
+ x: {
62
+ type: 'category',
63
+ title: options.xLabel
64
+ ? {
65
+ display: true,
66
+ text: options.xLabel,
67
+ }
68
+ : undefined,
69
+ },
70
+ y: {
71
+ type: 'linear',
72
+ title: options.yLabel
73
+ ? {
74
+ display: true,
75
+ text: options.yLabel,
76
+ }
77
+ : undefined,
78
+ min: options.yMin,
79
+ max: options.yMax,
80
+ },
81
+ },
82
+ },
83
+ data: {
84
+ labels: options.labels,
85
+ datasets: series.map((s, i) => ({
86
+ fill: false,
87
+ backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length],
88
+ borderColor: SERIES_COLORS[i % SERIES_COLORS.length],
89
+ borderWidth: 1,
90
+ pointRadius: 0,
91
+ ...s,
92
+ })),
93
+ },
94
+ }
95
+ }
96
+
97
+ export async function plot(options: PlotOptions, series: PlotData[]) {
98
+ const {
99
+ CategoryScale,
100
+ Chart,
101
+ LinearScale,
102
+ LineController,
103
+ BarController,
104
+ LineElement,
105
+ BarElement,
106
+ PointElement,
107
+ Legend,
108
+ Title,
109
+ } = await import('chart.js')
110
+ const { BarWithErrorBar, BarWithErrorBarsController } = await import('chartjs-chart-error-bars')
111
+ Chart.register(
112
+ CategoryScale,
113
+ LineController,
114
+ LineElement,
115
+ BarController,
116
+ LinearScale,
117
+ BarElement,
118
+ PointElement,
119
+ Legend,
120
+ Title,
121
+ BarWithErrorBar,
122
+ BarWithErrorBarsController,
123
+ )
124
+ const { Canvas } = await import('skia-canvas')
125
+
126
+ log.debug('plot')
127
+ const config = plotConfig(options, series)
128
+ const canvas = new Canvas(1280, 720)
129
+ const chart = new Chart(canvas as unknown as HTMLCanvasElement, config)
130
+ await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' })
131
+ chart.destroy()
132
+ }
133
+
134
+ export async function plotHtml(options: PlotOptions, series: PlotData[]) {
135
+ log.debug('plotHtml')
136
+ const config = plotConfig(options, series)
137
+
138
+ const data = `
139
+ <!DOCTYPE html>
140
+ <html lang="en">
141
+ <head>
142
+ <meta charset="UTF-8">
143
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
+ <title>${options.title || 'Plot'}</title>
145
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
146
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars"></script>
147
+ <script src="https://cdn.jsdelivr.net/npm/hammerjs"></script>
148
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>
149
+ </head>
150
+ <body>
151
+ <div>
152
+ <canvas id="chart"></canvas>
153
+ </div>
154
+ <script>
155
+ const ctx = document.getElementById('chart');
156
+ const chart = new Chart(ctx, ${json5.stringify(config)});
157
+ chart.options.onClick = e => e.chart.resetZoom();
158
+ addEventListener('resize', () => chart.resize());
159
+ </script>
160
+ </body>
161
+ </html>`
162
+ await fs.promises.writeFile(options.filePath || 'plot.html', data)
163
+ }
package/src/scenarios.ts CHANGED
@@ -5,6 +5,8 @@ import { ThrottleConfig, ThrottleRule } from '@vpalmisano/throttler'
5
5
  import { Config } from './config'
6
6
  import { Auth, google } from 'googleapis'
7
7
  import { logger } from './utils'
8
+ import { sprintf } from 'sprintf-js'
9
+ import { PlotData, plotHtml } from './plot'
8
10
 
9
11
  const log = logger('webrtcperf:scenarios')
10
12
 
@@ -154,28 +156,32 @@ export async function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadshee
154
156
 
155
157
  export type ThrottleDirection = 'up' | 'down' | 'bidi'
156
158
 
157
- function formatBitrate(bitrate: number | undefined, prefix = ' ') {
159
+ export function formatBitrate(bitrate: number | undefined, prefix = ' ', pad = true) {
158
160
  if (bitrate === undefined) return ''
159
161
  let suffix = 'Kbps'
160
- if (bitrate >= 10000) {
162
+ if (bitrate >= 1000) {
161
163
  bitrate /= 1000
162
164
  suffix = 'Mbps'
163
165
  }
164
- return `${prefix}${bitrate.toFixed(0)}${suffix}`.padStart(8, ' ')
166
+ return `${prefix}${sprintf(`%${pad ? '5' : ''}.4g`, bitrate)}${suffix}`
165
167
  }
166
168
 
167
- function formatLoss(loss: number | undefined, prefix = ' ') {
168
- return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(2, ' ')}%` : ''
169
+ export function formatLoss(loss: number | undefined, prefix = ' ', pad = true) {
170
+ return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(pad ? 2 : 0, ' ')}%` : ''
169
171
  }
170
172
 
171
- function formatDelay(delay: number | undefined, prefix = ' ') {
172
- return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(3, ' ')}ms` : ''
173
+ export function formatDelay(delay: number | undefined, prefix = ' ', pad = true) {
174
+ return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(pad ? 3 : 0, ' ')}ms` : ''
173
175
  }
174
176
 
175
- export function formatThrottleRule(throttleRule: ThrottleRule & { direction: ThrottleDirection }, human = false) {
177
+ export function formatThrottleRule(
178
+ throttleRule: ThrottleRule & { direction: ThrottleDirection },
179
+ human = false,
180
+ pad = true,
181
+ ) {
176
182
  const { rate, loss, delay, direction } = throttleRule
177
183
  return human
178
- ? `${direction.padEnd(4, ' ')}${formatBitrate(rate)}${formatLoss(loss)}${formatDelay(delay)}`
184
+ ? `${direction.padEnd(pad ? 4 : 0, ' ')}${formatBitrate(rate, ' ', pad)}${formatLoss(loss, ' ', pad)}${formatDelay(delay, ' ', pad)}`
179
185
  : `${direction}-r${rate}-l${loss}-d${delay}`
180
186
  }
181
187
 
@@ -189,6 +195,50 @@ export function parseThrottleRule(throttleDesc: string) {
189
195
  return { direction, rate, loss, delay }
190
196
  }
191
197
 
198
+ export async function plotStatsSummary(stats: StatsSummary[]) {
199
+ const labels = new Set<string>()
200
+ const data = {} as Record<string, Record<string, FastStats>>
201
+ stats.forEach(s => {
202
+ const { id, scenario, videoRecvBitratePerPixel } = s
203
+ if (!videoRecvBitratePerPixel.length) return null
204
+ if (!data[id]) {
205
+ data[id] = {}
206
+ }
207
+ const scenarioFormatted = formatThrottleRule(parseThrottleRule(scenario), true, true)
208
+ if (!data[id][scenarioFormatted]) {
209
+ data[id][scenarioFormatted] = new FastStats()
210
+ }
211
+ labels.add(scenarioFormatted)
212
+ data[id][scenarioFormatted].push(videoRecvBitratePerPixel.percentile(95))
213
+ })
214
+ const series: PlotData[] = []
215
+ Object.entries(data)
216
+ .sort((a, b) => a[0].localeCompare(b[0]))
217
+ .forEach(([id, data]) => {
218
+ const plotData: PlotData = { label: id, data: [] }
219
+ Object.entries(data)
220
+ .sort((a, b) => a[0].localeCompare(b[0]))
221
+ .forEach(([scenario, stats]) => {
222
+ plotData.data.push({
223
+ x: scenario,
224
+ y: stats.percentile(50),
225
+ yMin: stats.percentile(5),
226
+ yMax: stats.percentile(95),
227
+ })
228
+ })
229
+ series.push(plotData)
230
+ })
231
+ await plotHtml(
232
+ {
233
+ type: 'barWithErrorBars',
234
+ xLabel: 'Scenario',
235
+ yLabel: 'Video Receive Bitrate per Pixel',
236
+ labels: Array.from(labels).sort((a, b) => a.localeCompare(b)),
237
+ },
238
+ series,
239
+ )
240
+ }
241
+
192
242
  /**
193
243
  * It generates a test configuration with a scenario including 2 participants.
194
244
  * The first participant sends video and the second receives it.