@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 +3 -0
- package/address-obfuscator.js +101 -0
- package/compression.js +754 -0
- package/index.js +4 -0
- package/package.json +21 -0
- package/utils.js +62 -0
package/README.md
ADDED
|
@@ -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
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
|
+
|