@rtcstats/rtcstats-shared 1.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 ADDED
@@ -0,0 +1,3 @@
1
+ ## rtcstats shared code
2
+
3
+ Internal package with shared code used by rtcstats-js and rtcstats-server.
@@ -0,0 +1,101 @@
1
+ // Obfuscate ip addresses which should not be stored long-term.
2
+ import SDPUtils from 'sdp';
3
+
4
+ // Obfuscate an IP address, keeping address family intact.
5
+ // Based on libWebRTCs IPAddress::ToSensitiveString
6
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/rtc_base/ip_address.cc;l=149;drc=0dc30698370bcde67dda4f48b7ca19bf4c1dbc17
7
+ function obfuscateIpOrAddress(ipOrAddress) {
8
+ if (['::1', '127.0.0.1'].includes(ipOrAddress)) {
9
+ return ipOrAddress;
10
+ }
11
+ if (ipOrAddress.indexOf(':') !== -1) { // IPv6
12
+ const parts = ipOrAddress.split(':');
13
+ return parts.slice(0, 3).join(':') + 'x:x:x:x:x:x';
14
+ }
15
+ const parts = ipOrAddress.split('.');
16
+ if (parts.length === 4) { // IPv4
17
+ parts[3] = 'x';
18
+ return parts.join('.');
19
+ } else { // A hostname.
20
+ return ipOrAddress;
21
+ }
22
+ }
23
+
24
+ // Obfuscate the IP in ice candidates. Does NOT obfuscate the IP of the
25
+ // TURN server to allow selecting/grouping sessions by TURN server.
26
+ function obfuscateCandidate(candidate) {
27
+ const cand = SDPUtils.parseCandidate(candidate);
28
+ if (cand.type !== 'relay') {
29
+ cand.address = obfuscateIpOrAddress(cand.address);
30
+ delete cand.ip;
31
+ }
32
+ if (cand.relatedAddress) {
33
+ cand.relatedAddress = obfuscateIpOrAddress(cand.relatedAddress);
34
+ }
35
+ return SDPUtils.writeCandidate(cand);
36
+ }
37
+
38
+ // Obfuscate the IP in SDP, candidate lines, c= and a=rtcp lines.
39
+ function obfuscateSDP(sdp) {
40
+ return SDPUtils.splitLines(sdp).map(line => {
41
+ // obfuscate a=candidate, c= and a=rtcp
42
+ if (line.startsWith('a=candidate:')) {
43
+ return 'a=' + obfuscateCandidate(line);
44
+ } else if (line.startsWith('c=')) {
45
+ return 'c=IN IP4 0.0.0.0';
46
+ } else if (line.startsWith('a=rtcp:')) {
47
+ return 'a=rtcp:9 IN IP4 0.0.0.0';
48
+ }
49
+ return line;
50
+ }).join('\r\n').trim() + '\r\n';
51
+ }
52
+
53
+ function obfuscateStats(stats) {
54
+ Object.keys(stats).forEach((id) => {
55
+ const report = stats[id];
56
+ if (!report) {
57
+ return;
58
+ }
59
+ if (report.address && report.candidateType !== 'relay') {
60
+ report.address = obfuscateIpOrAddress(report.address);
61
+ delete report.ip;
62
+ }
63
+ if (report.relatedAddress) {
64
+ report.relatedAddress = obfuscateIpOrAddress(report.relatedAddress);
65
+ }
66
+ });
67
+ }
68
+
69
+ export function obfuscateAddress(method, data) {
70
+ switch(method) {
71
+ case 'addIceCandidate':
72
+ case 'onicecandidate':
73
+ if (data[2] && data[2].candidate) {
74
+ data[2].candidate = obfuscateCandidate(data[2].candidate);
75
+ }
76
+ break;
77
+ case 'setLocalDescription':
78
+ case 'setLocalDescriptionOnSuccess':
79
+ case 'setRemoteDescription':
80
+ case 'createOfferOnSuccess':
81
+ case 'createAnswerOnSuccess':
82
+ if (data[2] && data[2].sdp) {
83
+ data[2].sdp = obfuscateSDP(data[2].sdp);
84
+ }
85
+ break;
86
+ case 'getStats':
87
+ if (data[2]) {
88
+ obfuscateStats(data[2]);
89
+ }
90
+ break;
91
+ case 'publicIP':
92
+ if (Array.isArray(data[2])) {
93
+ data[2] = data[2].map(obfuscateIpOrAddress);
94
+ } else {
95
+ data[2] = obfuscateIpOrAddress(data[2]);
96
+ }
97
+ break;
98
+ default:
99
+ break;
100
+ }
101
+ };
package/compression.js ADDED
@@ -0,0 +1,754 @@
1
+ import {map2obj} from './utils.js';
2
+
3
+ /**
4
+ * Apply compression to the stats report. Reduces size a lot.
5
+ * @param {object|RTCStatsReport} baseStatsInput - baseline statistics
6
+ * against which the delta will be calculated against.
7
+ * @param {object|RTCStatsReport} newStatsInput - current statistics
8
+ * from which the delta will be calculated.
9
+ * @param {object} statsIdMap statsIdMap - initially empty mapping of
10
+ * full stats id to compressed stats id. Will be modified.
11
+ *
12
+ * @returns {object} compressed statistics.
13
+ */
14
+ export function statsCompression(baseStatsInput, newStatsInput, statsIdMap) {
15
+ const baseStats = map2obj(baseStatsInput);
16
+ const delta = JSON.parse(JSON.stringify(map2obj(newStatsInput)));
17
+
18
+ const removedObjectIds = [];
19
+ // Detect any removed values that were ever transmitted.
20
+ // Those get flagged at the end.
21
+ Object.keys(baseStats).forEach((id) => {
22
+ if (statsIdMap[id] !== undefined && delta[id] === undefined) {
23
+ removedObjectIds.push(id);
24
+ }
25
+ });
26
+
27
+ // Core delta compression.
28
+ Object.keys(delta).forEach((id) => {
29
+ const report = delta[id];
30
+ delete report.id;
31
+ if (!baseStats[id]) {
32
+ return;
33
+ }
34
+ Object.keys(report).forEach((name) => {
35
+ if (report[name] === baseStats[id][name]) {
36
+ delete delta[id][name];
37
+ } else if (Array.isArray(report[name])) {
38
+ // Arrays get serialized if anything changed.
39
+ if (JSON.stringify(report[name]) === JSON.stringify(baseStats[id][name])) {
40
+ delete delta[id][name];
41
+ }
42
+ } else if (typeof(report[name]) === 'object') {
43
+ Object.keys(report[name]).forEach(key => {
44
+ if (baseStats[id][name] && baseStats[id][name][key] === report[name][key]) {
45
+ delete delta[id][name][key];
46
+ }
47
+ });
48
+ if (Object.keys(report[name]).length === 0) {
49
+ delete report[name];
50
+ }
51
+ }
52
+ // TODO: does this ever happen since we have `timestamp`?
53
+ if (Object.keys(report).length === 0) {
54
+ delete delta[id];
55
+ }
56
+ });
57
+ });
58
+
59
+ // Collapse timestamps to latest timestamp (if equal)
60
+ // and move to top-level.
61
+ // TODO: can we make this relative to load time?
62
+ let timestamp = -Infinity;
63
+ Object.keys(delta).forEach((id) => {
64
+ const report = delta[id];
65
+ if (report.timestamp > timestamp) {
66
+ timestamp = report.timestamp;
67
+ }
68
+ });
69
+ Object.keys(delta).forEach((id) => {
70
+ const report = delta[id];
71
+ if (report.timestamp === timestamp) {
72
+ delete report.timestamp;
73
+ if (Object.keys(report).length === 0) {
74
+ delete delta[id];
75
+ }
76
+ }
77
+ });
78
+ if (timestamp !== -Infinity) {
79
+ delta[compressStatsProperty('timestamp')] = timestamp;
80
+ }
81
+
82
+ // Remove mostly useless things like certificate and codec stats.
83
+ removeCertificateStats(delta);
84
+ removeCodecStats(delta);
85
+ removeObsoleteProperties(delta);
86
+
87
+ // Replace the semi-structured ids with numerically increasing ones.
88
+ // Also replace the references so this operation does not need to be
89
+ // reversed on the other end.
90
+ Object.keys(delta).forEach((id) => {
91
+ if (!statsIdMap.hasOwnProperty(id) && typeof delta[id] === 'object') {
92
+ statsIdMap[id] = (Object.keys(statsIdMap).length).toString();
93
+ }
94
+ });
95
+ Object.keys(delta).forEach((id) => {
96
+ if (typeof delta[id] !== 'object') return;
97
+ const newId = statsIdMap[id];
98
+ if (id === newId) return;
99
+ delta[newId] = delta[id];
100
+ Object.keys(delta[id]).forEach(property => {
101
+ if (property.endsWith('Id')) {
102
+ delta[newId][property] = statsIdMap[delta[id][property]];
103
+ }
104
+ });
105
+ delete delta[id];
106
+ });
107
+
108
+ // Compress property names using a static table.
109
+ // Done last so other parts of the algorithm do not have to deal with
110
+ // the internal property names.
111
+ Object.keys(delta).forEach((id) => {
112
+ const report = delta[id];
113
+ Object.keys(report).forEach((name) => {
114
+ const newName = compressStatsProperty(name);
115
+ if (newName === name) {
116
+ if (name === 'type') {
117
+ // Compressing `type` is useful for the first time, after
118
+ // that the effect is gone due to delta compression.
119
+ report[name] = compressStatsType(report[name]);
120
+ }
121
+ return;
122
+ }
123
+ report[newName] = report[name];
124
+ delete report[name];
125
+ });
126
+ });
127
+
128
+ // Set removed object ids to null. Don't remove them from the idMap so the
129
+ // key stays unique.
130
+ removedObjectIds.forEach((id) => {
131
+ delta[statsIdMap[id] || id] = null;
132
+ });
133
+
134
+ // TODO: consider truncating floating point numbers to three decimals.
135
+ return delta;
136
+ }
137
+
138
+ /**
139
+ * Reverse compression of a stats report.
140
+ * @param {object} baseStatsInput - baseline statistics
141
+ * from which the statistics will be restored.
142
+ * @param {object} delta - compressed stats delta
143
+ * from which the delta will be restored.
144
+ *
145
+ * @returns {object} statistics.
146
+ */
147
+ export function statsDecompression(baseStatsInput, delta) {
148
+ const baseStats = map2obj(baseStatsInput);
149
+ const newStats = JSON.parse(JSON.stringify(map2obj(delta)));
150
+
151
+ // Decompress property names using a static table.
152
+ Object.keys(newStats).forEach((id) => {
153
+ const report = newStats[id];
154
+ if (report === null) {
155
+ return;
156
+ }
157
+ Object.keys(report).forEach((name) => {
158
+ const newName = decompressStatsProperty(name);
159
+ if (newName === name) {
160
+ if (name === 'type') {
161
+ report[name] = decompressStatsType(report[name]);
162
+ }
163
+ return;
164
+ }
165
+ report[newName] = report[name];
166
+ delete report[name];
167
+ });
168
+ });
169
+ const timestamp = newStats[compressStatsProperty('timestamp')];
170
+ delete newStats[compressStatsProperty('timestamp')];
171
+
172
+ // Core delta decompression.
173
+ Object.keys(baseStats).forEach(id => {
174
+ if (newStats[id] === null) {
175
+ // This id was removed and is no longer valid.
176
+ delete newStats[id];
177
+ return;
178
+ }
179
+ if (!newStats[id]) {
180
+ newStats[id] = baseStats[id];
181
+ } else {
182
+ const report = baseStats[id];
183
+ Object.keys(report).forEach(name => {
184
+ if (!newStats[id][name]) {
185
+ newStats[id][name] = report[name];
186
+ } else if (typeof(report[name]) === 'object') {
187
+ const newObject = newStats[id][name];
188
+ newStats[id][name] = report[name];
189
+ Object.keys(newObject).forEach(key => {
190
+ newStats[id][name][key] = newObject[key];
191
+ });
192
+ } // Else: take the new value.
193
+ });
194
+ }
195
+ if (timestamp !== undefined) {
196
+ newStats[id].timestamp = timestamp;
197
+ }
198
+ });
199
+
200
+ // Uncollapse timestamps from top-level
201
+ if (timestamp !== undefined) {
202
+ Object.keys(newStats).forEach((id) => {
203
+ const report = newStats[id];
204
+ if (!report) return;
205
+ if (report.timestamp === undefined) {
206
+ report.timestamp = timestamp;
207
+ }
208
+ });
209
+ }
210
+
211
+ return newStats;
212
+ }
213
+
214
+ /**
215
+ * Removes certificate stats and references to them.
216
+ * @protected
217
+ * @param stats {stats} - JSON stats object.
218
+ */
219
+ function removeCertificateStats(stats) {
220
+ Object.keys(stats).forEach((id) => {
221
+ const report = stats[id];
222
+ if (report.type === 'certificate') {
223
+ delete stats[id];
224
+ }
225
+ if (report.type === 'transport') {
226
+ delete report.localCertificateId;
227
+ delete report.remoteCertificateId;
228
+ }
229
+ });
230
+ }
231
+
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
+ /**
250
+ * Removes obsolete properties.
251
+ * @protected
252
+ * @param stats {stats} - JSON stats object.
253
+ */
254
+ function removeObsoleteProperties(stats) {
255
+ Object.keys(stats).forEach((id) => {
256
+ const report = stats[id];
257
+ ['isRemote', 'ip', 'mediaType', 'writable'].forEach(property => {
258
+ delete report[property];
259
+ });
260
+ });
261
+ }
262
+
263
+ /**
264
+ * Helper function to split SDP into sections.
265
+ * Similar to SDPUtils.splitSections but trims.
266
+ * @protected
267
+ * @param {string} sdp - SDP as string.
268
+ *
269
+ * @returns {string[]} - different sections of the SDP.
270
+ */
271
+ // Similar to SDPUtils.splitSections but trims.
272
+ function splitSections(sdp) {
273
+ return sdp.split('\nm=')
274
+ .map((part, index) => (index > 0 ?
275
+ 'm=' + part : part).trim());
276
+ }
277
+
278
+ /**
279
+ * Compresses the new description by splitting the SDP into sections and
280
+ * replacing sections that are identical with m= (or v= for the first section)
281
+ * from the base description SDP.
282
+ *
283
+ * @param {RTCSessionDescription} baseDescription - old description (potentially null)
284
+ * @param {RTCSessionDescription} newDescription - new description
285
+ *
286
+ * @returns {RTCSessionDescriptionInit} description with compressed SDP.
287
+ */
288
+ export function descriptionCompression(baseDescription, newDescription) {
289
+ if (!baseDescription || baseDescription.type !== newDescription.type ||
290
+ !newDescription.sdp || !baseDescription.sdp) {
291
+ return newDescription;
292
+ }
293
+ const baseSections = splitSections(baseDescription.sdp);
294
+ const newSections = splitSections(newDescription.sdp);
295
+ const sdp = newSections.map((newSection, index) => {
296
+ return (newSection === baseSections[index] ?
297
+ (index === 0 ? 'v=' : 'm=') : newSection);
298
+ }).join('\r\n') + '\r\n';
299
+ return {
300
+ type: newDescription.type,
301
+ sdp,
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Uncompresses the new description by splitting the SDP into sections and
307
+ * replacing sections that are equall to m= (or v= for the first section) with
308
+ * the section from the base description SDP.
309
+ *
310
+ * @returns {string} Uncompressed SDP.
311
+ * @param {RTCSessionDescription} baseDescription - old description (potentially null)
312
+ * @param {RTCSessionDescription} newDescription - new description
313
+ *
314
+ * @returns {RTCSessionDescriptionInit} description with uncompressed SDP.
315
+ */
316
+ export function descriptionDecompression(baseDescription, newDescription) {
317
+ if (!baseDescription || baseDescription.type !== newDescription.type ||
318
+ !newDescription.sdp || !baseDescription.sdp) {
319
+ return newDescription;
320
+ }
321
+ const baseSections = splitSections(baseDescription.sdp);
322
+ const newSections = splitSections(newDescription.sdp);
323
+ const sdp = newSections.map((newSection, index) => {
324
+ return (newSection === (index === 0 ? 'v=' : 'm=') ? baseSections[index] : newSection);
325
+ }).join('\r\n').trim() + '\r\n';
326
+ return {
327
+ type: newDescription.type,
328
+ sdp,
329
+ };
330
+ }
331
+
332
+ // Table-based lookup for compression of methods.
333
+ // Numbers must not be reassigned.
334
+ const methodTable = {
335
+ // Main peerconnection APIs.
336
+ // These are very common so ordered specifically such that
337
+ // they get ѕingle-digit ids.
338
+ getStats: 'g',
339
+ createOffer: 2,
340
+ createOfferOnSuccess: 3,
341
+ createOfferOnFailure: 12,
342
+ createAnswer: 4,
343
+ createAnswerOnSuccess: 5,
344
+ createAnswerOnFailure: 13,
345
+ setLocalDescription: 6,
346
+ setLocalDescriptionOnSuccess: 7,
347
+ setLocalDescriptionOnFailure: 14,
348
+ setRemoteDescription: 8,
349
+ setRemoteDescriptionOnSuccess: 9,
350
+ setRemoteDescriptionOnFailure: 15,
351
+ addIceCandidate: 10,
352
+ addIceCandidateOnSuccess: 11,
353
+ addIceCandidateOnFailure: 16,
354
+ // peerconnection events.
355
+ onicecandidate: 20,
356
+ onicecandidateerror: 21,
357
+ ontrack: 22,
358
+ onsignalingstatechange: 23,
359
+ oniceconnectionstatechange: 24,
360
+ onconnectionstatechange: 25,
361
+ onicegatheringstatechange: 26,
362
+ onnegotiationneeded: 27,
363
+ ondatachannel: 28,
364
+ // Media APIs.
365
+ addTrack: 30,
366
+ addTrackOnSuccess: 31,
367
+ addTransceiver: 32,
368
+ addTransceiverOnSuccess: 33,
369
+ removeTrack: 34,
370
+ // Misc APIs.
371
+ createDataChannel: 35,
372
+ close: 36,
373
+ restartIce: 37,
374
+ setConfiguration: 38,
375
+ // Constructor (and constraints)
376
+ create: 39,
377
+ constraints: 40,
378
+ // Transceiver APIs.
379
+ setCodecPreferences: 41,
380
+ setHeaderExtensionsToNegotiate: 42,
381
+ // Sender APIs.
382
+ setParameters: 50,
383
+ replaceTrack: 51,
384
+ // getUserMedia and friends.
385
+ 'navigator.mediaDevices.getUserMedia': 60,
386
+ 'navigator.mediaDevices.getUserMediaOnSuccess': 61,
387
+ 'navigator.mediaDevices.getUserMediaOnFailure': 62,
388
+ 'navigator.mediaDevices.getDisplayMedia': 63,
389
+ 'navigator.mediaDevices.getDisplayMediaOnSuccess': 64,
390
+ 'navigator.mediaDevices.getUserMediaOnFailure': 65,
391
+ 'navigator.mediaDevices.enumerateDevices': 66,
392
+ 'navigator.mediaDevices.ondevicechange': 67,
393
+ // MediaStreamTrack methods and events.
394
+ 'MediaStreamTrack.stop': 70,
395
+ 'MediaStreamTrack.applyConstraints': 71,
396
+ 'MediaStreamTrack.onended': 72,
397
+ 'MediaStreamTrack.onmute': 73,
398
+ 'MediaStreamTrack.onunmute': 74,
399
+ };
400
+ const reverseMethodTable = Object.keys(methodTable).reduce((table, method) => {
401
+ table[methodTable[method]] = method;
402
+ return table;
403
+ }, {});
404
+
405
+ /**
406
+ * Replace a rtcstats method with a numeric identifier.
407
+ * Unknown methods are returned as-is.
408
+ * @protected
409
+ * @param {string} method - rtcstats method.
410
+ *
411
+ * @returns {string|number} compressed method.
412
+ */
413
+ export function compressMethod(method) {
414
+ return methodTable[method] || method;
415
+ }
416
+
417
+ /**
418
+ * Resolve a compressed method to the original method name.
419
+ * Unknown method ids are returned as-is.
420
+ * @protected
421
+ * @param {string|number} methodKey - compressed method.
422
+ *
423
+ * @returns {string} rtcstats method.
424
+ */
425
+ export function decompressMethod(methodKey) {
426
+ return reverseMethodTable[methodKey] || methodKey;
427
+ }
428
+
429
+ // Table-based lookup for compression of stats types.
430
+ // Numbers must not be reassigned.
431
+ // https://w3c.github.io/webrtc-stats/#rtcstatstype-str*
432
+ const statsTypeTable = {
433
+ 'inbound-rtp': 1,
434
+ 'outbound-rtp': 2,
435
+ 'remote-inbound-rtp': 3,
436
+ 'remote-outbound-rtp': 4,
437
+ 'transport': 5,
438
+ 'candidate-pair': 6,
439
+ 'local-candidate': 7,
440
+ 'remote-candidate': 8,
441
+ 'data-channel': 9,
442
+ 'peer-connection': 10,
443
+ 'certificate': 11,
444
+ 'media-source': 12,
445
+ 'media-playout': 13,
446
+ 'codec': 14,
447
+ };
448
+ const reverseStatsTypeTable = Object.keys(statsTypeTable).reduce((table, type) => {
449
+ table[statsTypeTable[type]] = type;
450
+ return table;
451
+ }, {});
452
+
453
+ /**
454
+ * Replace a stats `type` with a numeric identifier.
455
+ * Unknown types are returned as-is.
456
+ * @protected
457
+ * @param {string} type - stats type.
458
+ *
459
+ * @returns {string|number} compressed stats type.
460
+ */
461
+ export function compressStatsType(type) {
462
+ return statsTypeTable[type] || type;
463
+ }
464
+
465
+ /**
466
+ * Resolve a compressed stats type to the original type.
467
+ * Unknown types are returned as-is.
468
+ * @protected
469
+ * @param type {string|number} - compressed stats type.
470
+ *
471
+ * @returns {string} original stats type.
472
+ */
473
+ export function decompressStatsType(typeKey) {
474
+ return reverseStatsTypeTable[typeKey] || typeKey;
475
+ }
476
+
477
+ // Table-based lookup for compression of stats properties.
478
+ // Numbers must not be reassigned.
479
+ // https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats
480
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/peerconnection/rtc_stats_report.idl
481
+ // is the most complete reference.
482
+ // TODO: sort these to minimize symbol length for most-used.
483
+ const statsPropertyTable = {
484
+ // https://w3c.github.io/webrtc-pc/#rtcstats-dictionary
485
+ timestamp: 't',
486
+ // type: 2,
487
+ // id: 3,
488
+ // https://www.w3.org/TR/webrtc-stats/#codec-dict*
489
+ payloadType: 4,
490
+ transportId: 5,
491
+ mimeType: 6,
492
+ clockRate: 7,
493
+ channels: 8,
494
+ sdpFmtpLine: 9,
495
+ // https://w3c.github.io/webrtc-stats/#streamstats-dict*
496
+ ssrc: 10,
497
+ kind: 11,
498
+ // transportId duplicate from 4
499
+ codecId: 12,
500
+ // Non-standard and obsolete stats.
501
+ // mediaType: 13,
502
+ // https://w3c.github.io/webrtc-stats/#receivedrtpstats-dict*
503
+ packetsReceived: 14,
504
+ packetsLost: 15,
505
+ jitter: 16,
506
+ // https://w3c.github.io/webrtc-stats/#inboundrtpstats-dict*
507
+ trackIdentifier: 17,
508
+ mid: 18,
509
+ remoteId: 19,
510
+ framesDecoded: 20,
511
+ keyFramesDecoded: 21,
512
+ // Not implemented: unsigned long framesRendered;
513
+ framesDropped: 22,
514
+ frameWidth: 23,
515
+ frameHeight: 24,
516
+ framesPerSecond: 25,
517
+ qpSum: 26,
518
+ totalDecodeTime: 27,
519
+ totalInterFrameDelay: 28,
520
+ totalSquaredInterFrameDelay: 29,
521
+ pauseCount: 30,
522
+ totalPausesDuration: 31,
523
+ freezeCount: 32,
524
+ totalFreezesDuration: 33,
525
+ lastPacketReceivedTimestamp: 34,
526
+ headerBytesReceived: 35,
527
+ packetsDiscarded: 36,
528
+ fecPacketsReceived: 37,
529
+ fecPacketsDiscarded: 38,
530
+ fecBytesReceived: 39,
531
+ fecSsrc: 40,
532
+ bytesReceived: 41,
533
+ nackCount: 42,
534
+ firCount: 43,
535
+ pliCount: 44,
536
+ totalProcessingDelay: 45,
537
+ estimatedPlayoutTimestamp: 46,
538
+ jitterBufferDelay: 47,
539
+ jitterBufferTargetDelay: 48,
540
+ jitterBufferEmittedCount: 49,
541
+ jitterBufferMinimumDelay: 50,
542
+ totalSamplesReceived: 51,
543
+ concealedSamples: 52,
544
+ silentConcealedSamples: 53,
545
+ concealmentEvents: 54,
546
+ insertedSamplesForDeceleration: 55,
547
+ removedSamplesForAcceleration: 56,
548
+ audioLevel: 57,
549
+ totalAudioEnergy: 58,
550
+ totalSamplesDuration: 59,
551
+ framesReceived: 60,
552
+ decoderImplementation: 61,
553
+ playoutId: 62,
554
+ powerEfficientDecoder: 63,
555
+ framesAssembledFromMultiplePackets: 64,
556
+ totalAssemblyTime: 65,
557
+ // https://w3c.github.io/webrtc-provisional-stats/#RTCInboundRtpStreamStats-dict*
558
+ contentType: 66,
559
+ // https://github.com/w3c/webrtc-provisional-stats/issues/40
560
+ googTimingFrameInfo: 67,
561
+ retransmittedPacketsReceived: 68,
562
+ retransmittedBytesReceived: 69,
563
+ rtxSsrc: 70,
564
+ totalCorruptionProbability: 71,
565
+ totalSquaredCorruptionProbability: 72,
566
+ corruptionMeasurements: 73,
567
+ // https://w3c.github.io/webrtc-stats/#remoteinboundrtpstats-dict*
568
+ localId: 74,
569
+ roundTripTime: 75,
570
+ totalRoundTripTime: 76,
571
+ fractionLost: 77,
572
+ roundTripTimeMeasurements: 78,
573
+ // https://w3c.github.io/webrtc-stats/#sentrtpstats-dict*
574
+ packetsSent: 79,
575
+ bytesSent: 80,
576
+ // https://w3c.github.io/webrtc-stats/#outboundrtpstats-dict*
577
+ // mid duplicate from 17
578
+ mediaSourceId: 81,
579
+ // remoteId duplicate from 18
580
+ rid: 82,
581
+ encodingIndex: 83,
582
+ headerBytesSent: 84,
583
+ retransmittedPacketsSent: 85,
584
+ retransmittedBytesSent: 86,
585
+ // rtxSsrc duplicate from 69
586
+ targetBitrate: 87,
587
+ totalEncodedBytesTarget: 88,
588
+ // frameWidth duplicate from 22
589
+ // frameHeight duplicate from 23
590
+ // framesPerSecond duplicate from 24
591
+ framesSent: 89,
592
+ hugeFramesSent: 90,
593
+ framesEncoded: 91,
594
+ keyFramesEncoded: 92,
595
+ // qpSum duplicate from 25
596
+ totalEncodeTime: 93,
597
+ totalPacketSendDelay: 94,
598
+ qualityLimitationReason: 95,
599
+ qualityLimitationDurations: 96,
600
+ qualityLimitationResolutionChanges: 97,
601
+ // nackCount duplicate from 41
602
+ // firCount duplicate from 42
603
+ // pliCount duplicate from 43
604
+ encoderImplementation: 98,
605
+ powerEfficientEncoder: 99,
606
+ active: 100,
607
+ scalabilityMode: 101,
608
+ // contentType duplicate from 65
609
+ // https://w3c.github.io/webrtc-stats/#remoteoutboundrtpstats-dict*
610
+ // localId duplicate from 73
611
+ remoteTimestamp: 102,
612
+ reportsSent: 103,
613
+ // roundTripTime duplicate from 74
614
+ // totalRoundTripTime duplicate from 75
615
+ // roundTripTimeMeasurements duplicate from 77
616
+ // https://w3c.github.io/webrtc-stats/#mediasourcestats-dict*
617
+ // trackIdentifier duplicate from 16
618
+ // kind duplicate from 10
619
+ // https://w3c.github.io/webrtc-stats/#audiosourcestats-dict*
620
+ // audioLevel duplicate from 56
621
+ // totalAudioEnergy duplicate from 57
622
+ // totalSamplesDuration duplicate from 58
623
+ echoReturnLoss: 104,
624
+ echoReturnLossEnhancement: 105,
625
+ // Not implemented: double droppedSamplesDuration;
626
+ // Not implemented: unsigned long droppedSamplesEvents;
627
+ // Not implemented: double totalCaptureDelay;
628
+ // Not implemented: unsigned long long totalSamplesCaptured;
629
+ // https://w3c.github.io/webrtc-stats/#videosourcestats-dict*
630
+ width: 106,
631
+ height: 107,
632
+ frames: 108,
633
+ // framesPerSecond duplicate from 24
634
+ // https://w3c.github.io/webrtc-stats/#playoutstats-dict*
635
+ // kind duplicate from 10
636
+ synthesizedSamplesDuration: 109,
637
+ synthesizedSamplesEvents: 110,
638
+ // totalSamplesDuration duplicate from 58
639
+ totalPlayoutDelay: 111,
640
+ totalSamplesCount: 112,
641
+ // https://w3c.github.io/webrtc-stats/#pcstats-dict*
642
+ dataChannelsOpened: 113,
643
+ dataChannelsClosed: 114,
644
+ // https://w3c.github.io/webrtc-stats/#dcstats-dict*
645
+ label: 115,
646
+ protocol: 116,
647
+ dataChannelIdentifier: 117,
648
+ state: 118,
649
+ messagesSent: 119,
650
+ // bytesSent duplicate from 79
651
+ messagesReceived: 120,
652
+ // bytesReceived duplicate from 40
653
+ // https://w3c.github.io/webrtc-stats/#transportstats-dict*
654
+ // packetsSent duplicate from 78
655
+ // packetsReceived duplicate from 13
656
+ // bytesSent duplicate from 79
657
+ // bytesReceived duplicate from 40
658
+ iceRole: 121,
659
+ iceLocalUsernameFragment: 122,
660
+ dtlsState: 123,
661
+ iceState: 124,
662
+ selectedCandidatePairId: 125,
663
+ localCertificateId: 126,
664
+ remoteCertificateId: 127,
665
+ tlsVersion: 128,
666
+ dtlsCipher: 129,
667
+ dtlsRole: 130,
668
+ srtpCipher: 131,
669
+ selectedCandidatePairChanges: 132,
670
+ // https://w3c.github.io/webrtc-provisional-stats/#RTCTransportStats-dict*
671
+ rtcpTransportStatsId: 133,
672
+ // https://w3c.github.io/webrtc-stats/#icecandidate-dict*
673
+ // transportId duplicate from 4
674
+ address: 134,
675
+ port: 135,
676
+ // protocol duplicate from 115
677
+ candidateType: 136,
678
+ priority: 137,
679
+ url: 138,
680
+ relayProtocol: 139,
681
+ foundation: 140,
682
+ relatedAddress: 141,
683
+ relatedPort: 142,
684
+ usernameFragment: 143,
685
+ tcpType: 144,
686
+ // https://w3c.github.io/webrtc-provisional-stats/#RTCIceCandidateStats-stat*
687
+ networkType: 145,
688
+ // Non-standard and obsolete stats.
689
+ // - Removed because `type` reveals same information ('local-candidate' or
690
+ // 'remote-candidate').
691
+ // isRemote: 146,
692
+ // - Removed because it was renamed `address`.
693
+ // ip: 147,
694
+ // https://w3c.github.io/webrtc-stats/#candidatepair-dict*
695
+ // transportId duplicate from 4
696
+ localCandidateId: 148,
697
+ remoteCandidateId: 149,
698
+ // state duplicate from 117
699
+ nominated: 150,
700
+ // packetsSent duplicate from 78
701
+ // packetsReceived duplicate from 13
702
+ // bytesSent duplicate from 79
703
+ // bytesReceived duplicate from 40
704
+ lastPacketSentTimestamp: 151,
705
+ // lastPacketReceivedTimestamp duplicate from 33
706
+ // totalRoundTripTime duplicate from 75
707
+ currentRoundTripTime: 152,
708
+ availableOutgoingBitrate: 153,
709
+ availableIncomingBitrate: 154,
710
+ requestsReceived: 155,
711
+ requestsSent: 156,
712
+ responsesReceived: 157,
713
+ responsesSent: 158,
714
+ consentRequestsSent: 159,
715
+ packetsDiscardedOnSend: 160,
716
+ bytesDiscardedOnSend: 161,
717
+ // Non-standard and obsolete stats.
718
+ // writable: 162,
719
+ // priority duplicate from 136
720
+ // https://w3c.github.io/webrtc-stats/#certificatestats-dict*
721
+ fingerprint: 163,
722
+ fingerprintAlgorithm: 164,
723
+ base64Certificate: 165,
724
+ issuerCertificateId: 166,
725
+ };
726
+ const reverseStatsPropertyTable = Object.keys(statsPropertyTable).reduce((table, property) => {
727
+ table[statsPropertyTable[property]] = property;
728
+ return table;
729
+ }, {});
730
+
731
+ /**
732
+ * Replace a stats property name with a numeric key for known properties.
733
+ * Unknown property names are returned as-is.
734
+ * @protected
735
+ * @param property {string} - stats property name to be compressed.
736
+ *
737
+ * @returns {number|string} compressed property key.
738
+ */
739
+ export function compressStatsProperty(property) {
740
+ return statsPropertyTable[property] || property;
741
+ }
742
+
743
+ /**
744
+ * Resolve a numeric stats property key to the original name.
745
+ * Unknown keys are returned as-is.
746
+ * @protected
747
+ * @param property {number|string} - stats property name to be decompressed.
748
+ *
749
+ * @returns {string} property name.
750
+ */
751
+ export function decompressStatsProperty(property) {
752
+ return reverseStatsPropertyTable[property] || property;
753
+ }
754
+
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './compression.js';
2
+ export * from './utils.js';
3
+ // Not exported since the dump-importer would pull the sdp dependency.
4
+ // export * from './address-obfuscator.js';
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@rtcstats/rtcstats-shared",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "description": "gather WebRTC API traces and statistics",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rtcstats/rtcstats.git"
10
+ },
11
+ "scripts": {
12
+ "lint": "eslint *.js test/",
13
+ "coverage": "c8 --reporter html mocha test",
14
+ "test": "npm run lint && npm run coverage"
15
+ },
16
+ "author": "rtcstats.com",
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ }
21
+ }
package/utils.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Transforms a maplike to a JS object. Mostly for getStats + JSON.parse(JSON.stringify())
3
+ *
4
+ * @protected
5
+ * @param {Map|Object} m - Map or Javascript object.
6
+ *
7
+ * @returns {Object} object with the entries of the map.
8
+ */
9
+ export function map2obj(m) {
10
+ if (!m.entries) {
11
+ return m;
12
+ }
13
+ const o = {};
14
+ m.forEach((v, k) => {
15
+ o[k] = v;
16
+ });
17
+ return o;
18
+ }
19
+
20
+ /**
21
+ * Creates a representation of a track and its associated streams for serialization.
22
+ *
23
+ * @protected
24
+ * @param {MediaStreamTrack} track - the MediaStreamTrack.
25
+ * @param {...MediaStream} streams - the MediaStreams the track belongs to.
26
+ *
27
+ * @returns {String[]} - serialized representation.
28
+ */
29
+ export function dumpTrackWithStreams(track, ...streams) {
30
+ return [track.kind, track.id, ...streams.map(s => s.id)];
31
+ }
32
+
33
+ /**
34
+ * Sanitizes a RTCConfiguration by hiding the TURN server credentials and
35
+ * making the certificates serializable.
36
+ *
37
+ * @protected
38
+ * @param {RTCConfiguration} config - the RTCConfiguration.
39
+ *
40
+ * @returns {Object} - serializable RTCConfiguration.
41
+ */
42
+ export function copyAndSanitizeConfig(config) {
43
+ if (!config) {
44
+ return undefined;
45
+ }
46
+ const sanitizedConfig = JSON.parse(JSON.stringify(config)); // deepcopy
47
+ // Remove TURN server credentials.
48
+ if (sanitizedConfig && sanitizedConfig.iceServers) {
49
+ sanitizedConfig.iceServers.forEach((server) => {
50
+ delete server.credential;
51
+ });
52
+ }
53
+ // Serialize certificates.
54
+ if (config.certificates) {
55
+ sanitizedConfig.certificates = config.certificates.map(cert => ({
56
+ expires: cert.expires,
57
+ fingerprints: cert.getFingerprints(),
58
+ }));
59
+ }
60
+ return sanitizedConfig;
61
+ }
62
+