@rtcstats/rtcstats-shared 1.0.0 → 2.0.0
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 +7 -0
- package/compression.js +3 -19
- package/dump.js +119 -0
- package/index.d.ts +5 -0
- package/index.js +2 -0
- package/package.json +5 -2
- package/timeseries.js +86 -0
- package/utils.js +4 -6
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
This is part of the [monorepo](https://github.com/rtcstats/rtcstats) for rtcstats-js
|
|
2
|
+
(clientside monitoring), rtcstats-server (serverside dump collection) and the updated
|
|
3
|
+
dump-importer (supporting rtcstats and webrtc-internals formats).
|
|
4
|
+
|
|
5
|
+
It is part of a bigger offering that includes [rtcstats.com](https://rtcstats.com),
|
|
6
|
+
an online service for debugging and troubleshooting WebRTC statistics.
|
|
7
|
+
|
|
1
8
|
## rtcstats shared code
|
|
2
9
|
|
|
3
10
|
Internal package with shared code used by rtcstats-js and rtcstats-server.
|
package/compression.js
CHANGED
|
@@ -79,9 +79,8 @@ export function statsCompression(baseStatsInput, newStatsInput, statsIdMap) {
|
|
|
79
79
|
delta[compressStatsProperty('timestamp')] = timestamp;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// Remove mostly useless things like certificate
|
|
82
|
+
// Remove mostly useless things like certificate stats.
|
|
83
83
|
removeCertificateStats(delta);
|
|
84
|
-
removeCodecStats(delta);
|
|
85
84
|
removeObsoleteProperties(delta);
|
|
86
85
|
|
|
87
86
|
// Replace the semi-structured ids with numerically increasing ones.
|
|
@@ -229,23 +228,6 @@ function removeCertificateStats(stats) {
|
|
|
229
228
|
});
|
|
230
229
|
}
|
|
231
230
|
|
|
232
|
-
/**
|
|
233
|
-
* Removes codec stats and references to them.
|
|
234
|
-
* @protected
|
|
235
|
-
* @param stats {stats} - JSON stats object.
|
|
236
|
-
*/
|
|
237
|
-
function removeCodecStats(stats) {
|
|
238
|
-
Object.keys(stats).forEach((id) => {
|
|
239
|
-
const report = stats[id];
|
|
240
|
-
if (report.type === 'codec') {
|
|
241
|
-
delete stats[id];
|
|
242
|
-
}
|
|
243
|
-
if (report.codecId !== undefined) {
|
|
244
|
-
delete report.codecId;
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
231
|
/**
|
|
250
232
|
* Removes obsolete properties.
|
|
251
233
|
* @protected
|
|
@@ -396,6 +378,8 @@ const methodTable = {
|
|
|
396
378
|
'MediaStreamTrack.onended': 72,
|
|
397
379
|
'MediaStreamTrack.onmute': 73,
|
|
398
380
|
'MediaStreamTrack.onunmute': 74,
|
|
381
|
+
// HTMLMediaElement
|
|
382
|
+
'HTMLMediaElement.resize': 80,
|
|
399
383
|
};
|
|
400
384
|
const reverseMethodTable = Object.keys(methodTable).reduce((table, method) => {
|
|
401
385
|
table[methodTable[method]] = method;
|
package/dump.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decompressMethod,
|
|
3
|
+
descriptionDecompression,
|
|
4
|
+
statsDecompression,
|
|
5
|
+
} from './compression.js';
|
|
6
|
+
|
|
7
|
+
export async function detectRTCStatsDump(blob) {
|
|
8
|
+
const magic = await blob.slice(0, 13).text();
|
|
9
|
+
return magic.startsWith('RTCStatsDump\n');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function detectWebRTCInternalsDump(blob) {
|
|
13
|
+
return (await blob.text()).startsWith('{');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readRTCStatsDump(blob) {
|
|
17
|
+
const textBlob = await blob.text();
|
|
18
|
+
const firstLine = await textBlob.slice(0, 13);
|
|
19
|
+
if (firstLine !== 'RTCStatsDump\n') {
|
|
20
|
+
console.error('Not an RTCStatsDump');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const lines = (await textBlob.slice(13)).split('\n');
|
|
24
|
+
|
|
25
|
+
// The second line must be a JSON object with metadata.
|
|
26
|
+
let data;
|
|
27
|
+
try {
|
|
28
|
+
data = JSON.parse(lines.shift());
|
|
29
|
+
} catch(e) {
|
|
30
|
+
console.error('Second line is not JSON data');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
34
|
+
console.error('Second line must be an object');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
data.peerConnections = {};
|
|
38
|
+
data.eventSizes = {};
|
|
39
|
+
|
|
40
|
+
const baseStats = {};
|
|
41
|
+
let lastTime = 0;
|
|
42
|
+
for (let line of lines) {
|
|
43
|
+
if (!line.length) {
|
|
44
|
+
continue; // Ignore empty lines.
|
|
45
|
+
}
|
|
46
|
+
let jsonData;
|
|
47
|
+
try {
|
|
48
|
+
jsonData = JSON.parse(line);
|
|
49
|
+
} catch(e) {
|
|
50
|
+
console.error('Parsing line as JSON failed');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(jsonData)) {
|
|
54
|
+
continue; // Ignore non-array lines.
|
|
55
|
+
}
|
|
56
|
+
let [method, connection_id, value, ...extra] = jsonData;
|
|
57
|
+
method = decompressMethod(method);
|
|
58
|
+
|
|
59
|
+
lastTime = extra.pop() + lastTime;
|
|
60
|
+
const time = new Date(lastTime);
|
|
61
|
+
|
|
62
|
+
if (method === 'getStats') { // delta-compressed stats
|
|
63
|
+
value = statsDecompression(baseStats[connection_id], value);
|
|
64
|
+
baseStats[connection_id] = JSON.parse(JSON.stringify(value));
|
|
65
|
+
} else if (method === 'setLocalDescription') {
|
|
66
|
+
let createCall;
|
|
67
|
+
for (let previousIndex = data.peerConnections[connection_id].length - 1; previousIndex >= 0; previousIndex--) {
|
|
68
|
+
if ((value.type === 'offer' && data.peerConnections[connection_id][previousIndex].type === 'createOfferOnSuccess') ||
|
|
69
|
+
(value.type === 'answer' && data.peerConnections[connection_id][previousIndex].type === 'createAnswerOnSuccess')) {
|
|
70
|
+
createCall = data.peerConnections[connection_id][previousIndex];
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (createCall) {
|
|
75
|
+
value = descriptionDecompression(createCall.value, value);
|
|
76
|
+
}
|
|
77
|
+
} else if (['createOfferOnSuccess', 'createAnswerOnSuccess'].includes(method)) {
|
|
78
|
+
let sldCall;
|
|
79
|
+
for (let previousIndex = data.peerConnections[connection_id].length - 1; previousIndex >= 0; previousIndex--) {
|
|
80
|
+
if ((value.type === 'offer' && data.peerConnections[connection_id][previousIndex].type === 'setLocalDescription') ||
|
|
81
|
+
(value.type === 'answer' && data.peerConnections[connection_id][previousIndex].type === 'setLocalDescription')) {
|
|
82
|
+
sldCall = data.peerConnections[connection_id][previousIndex];
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (sldCall) {
|
|
87
|
+
value = descriptionDecompression(sldCall.value, value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// TODO: more explicit handling via create and close?
|
|
92
|
+
if (!data.peerConnections[connection_id]) {
|
|
93
|
+
data.peerConnections[connection_id] = [];
|
|
94
|
+
baseStats[connection_id] = {};
|
|
95
|
+
}
|
|
96
|
+
data.peerConnections[connection_id].push({
|
|
97
|
+
time, // deprecated, prefer timestamp.
|
|
98
|
+
timestamp: lastTime,
|
|
99
|
+
type: method,
|
|
100
|
+
value,
|
|
101
|
+
extra,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!data.eventSizes[connection_id]) {
|
|
105
|
+
data.eventSizes[connection_id] = [];
|
|
106
|
+
}
|
|
107
|
+
data.eventSizes[connection_id].push({
|
|
108
|
+
x: lastTime,
|
|
109
|
+
y: line.length,
|
|
110
|
+
method,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function readWebRTCInternalsDump(blob) {
|
|
117
|
+
const textBlob = await blob.text();
|
|
118
|
+
return JSON.parse(textBlob);
|
|
119
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Minimal .d.ts
|
|
2
|
+
export async function detectWebRTCInternalsDump(blob: Blob): boolean;
|
|
3
|
+
export async function readWebRTCInternalsDump(blob: Blob): object;
|
|
4
|
+
export async function detectRTCStatsDump(blob: Blob): boolean;
|
|
5
|
+
export async function readRTCStatsDump(blob: Blob): object;
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtcstats/rtcstats-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"description": "gather WebRTC API traces and statistics",
|
|
6
6
|
"type": "module",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"lint": "eslint *.js test/",
|
|
13
|
-
"coverage": "c8 --reporter html mocha test",
|
|
13
|
+
"coverage": "c8 --reporter html --reporter lcov mocha test",
|
|
14
14
|
"test": "npm run lint && npm run coverage"
|
|
15
15
|
},
|
|
16
16
|
"author": "rtcstats.com",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"sdp": "^3.2.1"
|
|
20
23
|
}
|
|
21
24
|
}
|
package/timeseries.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a timeseries from webrtc-internals.
|
|
3
|
+
* Returns an object with stats by id and an object of property=> [[ts, value], ...].
|
|
4
|
+
*/
|
|
5
|
+
export function createInternalsTimeSeries(connection) {
|
|
6
|
+
const series = {};
|
|
7
|
+
for (const reportname in connection.stats) {
|
|
8
|
+
// special casing of computed stats, in particular [a-b]
|
|
9
|
+
let statsId;
|
|
10
|
+
let statsProperty;
|
|
11
|
+
if (reportname.indexOf('[') !== -1) {
|
|
12
|
+
const t = reportname.split('[');
|
|
13
|
+
statsProperty = '[' + t.pop();
|
|
14
|
+
statsId = t.join('');
|
|
15
|
+
statsId = statsId.substr(0, statsId.length - 1);
|
|
16
|
+
} else {
|
|
17
|
+
const t = reportname.split('-');
|
|
18
|
+
statsProperty = t.pop();
|
|
19
|
+
statsId = t.join('-');
|
|
20
|
+
}
|
|
21
|
+
if (statsProperty === 'type') continue;
|
|
22
|
+
const stats = connection.stats[reportname];
|
|
23
|
+
|
|
24
|
+
if (!series.hasOwnProperty(statsId)) {
|
|
25
|
+
series[statsId] = {
|
|
26
|
+
type: stats.statsType,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!connection.stats[statsId + '-timestamp']) {
|
|
30
|
+
// Individual timestamps were added in crbug.com/1462567 in M117.
|
|
31
|
+
// This version is not supported anymore.
|
|
32
|
+
console.error('webrtc-internals dump missing timestamps for stats added in M117.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timestamps = JSON.parse(connection.stats[statsId + '-timestamp'].values);
|
|
36
|
+
series[statsId][statsProperty] = JSON.parse(stats.values).map((currentValue, index) => {
|
|
37
|
+
return [timestamps[index], currentValue];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return series;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a timeseries from rtcstats.
|
|
45
|
+
* Returns an object with stats by id and an object of property=> [[ts, value], ...].
|
|
46
|
+
*/
|
|
47
|
+
export function createRtcStatsTimeSeries(trace) {
|
|
48
|
+
const series = {};
|
|
49
|
+
for (const traceEvent of trace) {
|
|
50
|
+
if (traceEvent.type !== 'getStats') {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const stats = traceEvent.value;
|
|
54
|
+
for (const id in stats) {
|
|
55
|
+
const report = stats[id];
|
|
56
|
+
if (!series[id]) {
|
|
57
|
+
series[id] = {};
|
|
58
|
+
series[id].type = stats[id].type;
|
|
59
|
+
}
|
|
60
|
+
for (const statsProperty in report) {
|
|
61
|
+
if (['timestamp', 'type', 'id'].includes(statsProperty)) continue;
|
|
62
|
+
const timeSeries = series[id];
|
|
63
|
+
if (!timeSeries[statsProperty]) {
|
|
64
|
+
timeSeries[statsProperty] = [];
|
|
65
|
+
}
|
|
66
|
+
timeSeries[statsProperty].push([report.timestamp, report[statsProperty]]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return series;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Inserts null values for gaps in timeseries which creates a gap in Highcharts plots.
|
|
74
|
+
// TODO: figure out how to show the markers for this point.
|
|
75
|
+
// Possibly using https://api.highcharts.com/highcharts/plotOptions.series.zones
|
|
76
|
+
export function insertNullForGapsIntoTimeSeries(timeSeries, gapSizeMs = 5000) {
|
|
77
|
+
const newSeries = [timeSeries[0]];;
|
|
78
|
+
for (let i = 1; i < timeSeries.length; i++) {
|
|
79
|
+
const delta = timeSeries[i][0] - timeSeries[i - 1][0];
|
|
80
|
+
if (delta > gapSizeMs) {
|
|
81
|
+
newSeries.push([timeSeries[i][0], null]);
|
|
82
|
+
}
|
|
83
|
+
newSeries.push([timeSeries[i][0], timeSeries[i][1]]);
|
|
84
|
+
}
|
|
85
|
+
return newSeries;
|
|
86
|
+
}
|
package/utils.js
CHANGED
|
@@ -10,11 +10,9 @@ export function map2obj(m) {
|
|
|
10
10
|
if (!m.entries) {
|
|
11
11
|
return m;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
17
|
-
return o;
|
|
13
|
+
return [...m.entries()].reduce((o, [k, v]) => {
|
|
14
|
+
o[k] = v; return o;
|
|
15
|
+
}, {});
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
/**
|
|
@@ -27,7 +25,7 @@ export function map2obj(m) {
|
|
|
27
25
|
* @returns {String[]} - serialized representation.
|
|
28
26
|
*/
|
|
29
27
|
export function dumpTrackWithStreams(track, ...streams) {
|
|
30
|
-
return [track.kind, track.id, ...streams.map(s => s.id)];
|
|
28
|
+
return [track.kind, track.id, track.label, ...streams.map(s => s.id)];
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
/**
|