@rtcstats/rtcstats-js 2.0.2 → 2.2.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 CHANGED
@@ -39,6 +39,78 @@ trace.connect('ws://localhost:8080' + window.location.pathname);
39
39
  const pc = new RTCPeerConnection();
40
40
  ```
41
41
 
42
+ ### Nondefault options
43
+
44
+ ### Counting reloads
45
+
46
+ rtcstats-js can count the number of times a user has reloaded the page by looking at the sessionStorage.
47
+ If enabled with
48
+ ```
49
+ const trace = new WebSocketTrace({countReloads: true});
50
+ ```
51
+ this will read the key `rtcstatsReloadCount` from session storage, increment it and write it back.
52
+
53
+ Reloading the page in the middle of a call is a last resort often used by users when something goes
54
+ really wrong. This feature does not count actual page reloads which would require more DOM event
55
+ listeners but the number of times the trace function has been instantiated which should be once
56
+ per page load.
57
+
58
+ ### Tracing custom events
59
+ Sometimes you may want to send your own events to RTCStats to have them all in one dump file.
60
+ This can be accomplished with the `trace` function as well. The method signature for this is
61
+ ```
62
+ trace(method name, e.g. `:userRating`,
63
+ peerconnection object to associate with or null if not associated,
64
+ javascript object with data or string)
65
+ ```
66
+
67
+ * The first argument is the method name MUST start with a colon (`:`) to avoid conflicts with any method names that rtcstats-js and rtcstats-server use internally.
68
+ * If the event is to be associated with a particular RTCPeerConnection, pass that connection as the second argument.
69
+ If it is not, e.g. for an end-of-call user rating, pass `null`.
70
+ * The third argument can be a Javascript string or object which will be encoded using JSON when sent to the server. If there is no additional data, pass `null`.
71
+ * The trace function will internally add a timestamp to the event.
72
+ * rtcstats-server will not process such events by default.
73
+ * A dump-importer should still display a generic rendering of the event.
74
+
75
+ ### Using a JWT to connect to rtcstats-server
76
+ See [the server README](https://github.com/rtcstats/rtcstats/blob/main/packages/rtcstats-server/README.md) for how to
77
+ generate JWT token with information about the user, session and conference.
78
+
79
+ If the server is configured to require an authorization token, the websocket will
80
+ be closed with a 1008 policy-violation error which can be seen when configuring the logging callback:
81
+ ```
82
+ const trace = new WebSocketTrace({log: console.warn.bind(console)});
83
+ ```
84
+
85
+ ### Bundling
86
+ To bundle rtcstats-js including its dependencies, use
87
+ ```
88
+ npx webpack --entry ./packages/rtcstats-js/rtcstats.js --output-path ./packages/rtcstats-js/dist --output-filename rtcstats.bundle.js --mode production --output-library rtcstats
89
+ ```
90
+ then include the resulting bundle which is exported as the global `rtcstats` object:
91
+ ```
92
+ <script type="text/javascript" src="rtcstats.bundle.js"></script>
93
+ <script type="text/javascript">
94
+ const trace = rtcstats.wrapRTCStatsWithDefaultOptions();
95
+ // Do something.
96
+ </script>
97
+ ```
98
+
99
+ ### Ending a session
100
+ RTCStats sessions create a single dump file for every websocket connection. For long-running pages it may be required
101
+ to close the websocket when a "session" is done and reconnect (possibly with a new JWT):
102
+ ```
103
+ trace.connect('ws://localhost:8080/?rtcstats-token=token-1');
104
+ // Do something
105
+ trace.close()
106
+
107
+ // Reconnect.
108
+ trace.connect('ws://localhost:8080/?rtcstats-token=token-2');
109
+ ```
110
+ Note that RTCStats is not keeping track of whether all RTCPeerConnections have been closed and track from getUserMedia/getDisplayMedia
111
+ have been stopped.
112
+
113
+ ### See also
42
114
  See also the [end-to-end example](/example/) in `example/` directory and
43
115
  the (internal) API docs [here](docs/index.md).
44
116
 
package/media.js CHANGED
@@ -34,7 +34,7 @@ function wrapTrackProperty(track, property, trace) {
34
34
  * Also wraps these methods on MediaStreamTrack
35
35
  * * stop
36
36
  * * applyConstraints
37
- * The `ended` event is wrapped as are the setters fro `enabled`
37
+ * The `ended` event is wrapped as are the setters for `enabled`
38
38
  * and `contentHint`.
39
39
  *
40
40
  * @protected
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@rtcstats/rtcstats-js",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "gather WebRTC API traces and statistics",
5
5
  "main": "rtcstats.js",
6
6
  "type": "module",
7
7
  "devDependencies": {
8
8
  "@esm-bundle/chai": "^4.3.4-fix.0",
9
9
  "@puppeteer/browsers": "^2.10.9",
10
- "@web/test-runner": "^0.20.2"
10
+ "@web/test-runner": "^0.20.2",
11
+ "jsdoc-to-markdown": "^9.1.3"
11
12
  },
12
13
  "scripts": {
13
14
  "lint": "eslint *.js test/",
package/peerconnection.js CHANGED
@@ -1,4 +1,9 @@
1
- import {statsCompression, descriptionCompression, compressMethod} from '@rtcstats/rtcstats-shared';
1
+ import {
2
+ statsCompression,
3
+ descriptionCompression,
4
+ compressMethod,
5
+ computePressureTable,
6
+ } from '@rtcstats/rtcstats-shared';
2
7
  import {map2obj, dumpTrackWithStreams, copyAndSanitizeConfig} from '@rtcstats/rtcstats-shared';
3
8
 
4
9
  /**
@@ -42,27 +47,27 @@ function wrapRTCRtpSender(trace, window) {
42
47
  const nativeMethod = window.RTCRtpSender.prototype[method];
43
48
  if (!nativeMethod) return;
44
49
 
45
- window.RTCRtpSender.prototype[method] = function(...args) {
46
- const serializedArgs = JSON.parse(JSON.stringify(args));
50
+ window.RTCRtpSender.prototype[method] = function(parameters, ...args) {
51
+ const serializedArgs = JSON.parse(JSON.stringify([parameters, ...args]));
47
52
  delete serializedArgs[0].transactionId;
48
53
  trace(method, this.__rtcStatsId,
49
54
  serializedArgs,
50
55
  this.__rtcStatsSenderId);
51
- return nativeMethod.apply(this, args);
56
+ return nativeMethod.apply(this, [parameters, ...args]);
52
57
  };
53
58
  });
54
59
  ['replaceTrack'].forEach(method => {
55
60
  const nativeMethod = window.RTCRtpSender.prototype[method];
56
61
  if (!nativeMethod) return;
57
- window.RTCRtpSender.prototype[method] = function(...args) {
62
+ window.RTCRtpSender.prototype[method] = function(track, ...args) {
58
63
  const serializedArgs = [
59
64
  this.track === null ? null : dumpTrackWithStreams(this.track),
60
- args[0] === null ? null : dumpTrackWithStreams(args[0]),
65
+ track === null ? null : dumpTrackWithStreams(track),
61
66
  ];
62
67
  trace(method, this.__rtcStatsId,
63
68
  serializedArgs,
64
69
  this.__rtcStatsSenderId);
65
- return nativeMethod.apply(this, args);
70
+ return nativeMethod.apply(this, [track, ...args]);
66
71
  };
67
72
  });
68
73
  }
@@ -84,6 +89,14 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
84
89
  // Prevent double-wrapping.
85
90
  return;
86
91
  }
92
+ let lastComputePressureRecord;
93
+ if (window.PressureObserver) {
94
+ const observer = new PressureObserver((records) => {
95
+ const lastRecord = records[records.length - 1]; // Usually only one record.
96
+ lastComputePressureRecord = [Date.now(), lastRecord];
97
+ });
98
+ observer.observe('cpu', {sampleInterval: getStatsInterval});
99
+ }
87
100
  wrapRTCRtpTransceiver(trace, window);
88
101
  wrapRTCRtpSender(trace, window);
89
102
  const OrigPeerConnection = window.RTCPeerConnection;
@@ -108,6 +121,15 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
108
121
  }
109
122
 
110
123
  pc.addEventListener('icecandidate', (e) => {
124
+ // Browser intentionally don't serialize .url and .relayProtocol in .toJSON
125
+ // but we want them for rtcstats.
126
+ if (e.candidate && (e.candidate.url || e.candidate.relayProtocol)) {
127
+ const serializedCandidate = e.candidate.toJSON();
128
+ serializedCandidate.url = e.candidate.url;
129
+ serializedCandidate.relayProtocol = e.candidate.relayProtocol;
130
+ trace('onicecandidate', pcId, serializedCandidate);
131
+ return;
132
+ }
111
133
  trace('onicecandidate', pcId, e.candidate);
112
134
  });
113
135
  pc.addEventListener('icecandidateerror', (e) => {
@@ -176,6 +198,14 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
176
198
  if (pc.signalingState === 'closed') {
177
199
  return;
178
200
  }
201
+ if (lastComputePressureRecord) {
202
+ const [timestamp, record] = lastComputePressureRecord;
203
+ stats['rtcStatsComputePressure'] = {
204
+ type: 'compute-pressure',
205
+ timestamp,
206
+ cpuState: computePressureTable[record.state] || record.state,
207
+ };
208
+ }
179
209
  const baseStats = JSON.parse(JSON.stringify(stats)); // our new prevStats.
180
210
  const compressedStats = statsCompression(prevStats, stats, statsIdMap);
181
211
  if (reason) {
@@ -201,9 +231,9 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
201
231
  ['createDataChannel'].forEach(method => {
202
232
  const nativeMethod = OrigPeerConnection.prototype[method];
203
233
  if (!nativeMethod) return;
204
- OrigPeerConnection.prototype[method] = function(...args) {
205
- trace(method, this.__rtcStatsId, args);
206
- return nativeMethod.apply(this, args);
234
+ OrigPeerConnection.prototype[method] = function(label, ...args) {
235
+ trace(method, this.__rtcStatsId, [label, ...args]);
236
+ return nativeMethod.apply(this, [label, ...args]);
207
237
  };
208
238
  });
209
239
 
@@ -219,11 +249,10 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
219
249
  ['addTrack'].forEach(method => {
220
250
  const nativeMethod = OrigPeerConnection.prototype[method];
221
251
  if (!nativeMethod) return;
222
- OrigPeerConnection.prototype[method] = function(...args) {
223
- const track = args[0];
224
- const streams = args.slice(1);
252
+ OrigPeerConnection.prototype[method] = function(track, ...args) {
253
+ const streams = args;
225
254
  trace(method, this.__rtcStatsId, dumpTrackWithStreams(track, ...streams));
226
- const sender = nativeMethod.apply(this, args);
255
+ const sender = nativeMethod.apply(this, [track, ...args]);
227
256
  sender.__rtcStatsId = this.__rtcStatsId;
228
257
  const transceiver = this.getTransceivers().find(t => t.sender === sender);
229
258
  if (transceiver) {
@@ -238,18 +267,18 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
238
267
  ['addTransceiver'].forEach(method => {
239
268
  const nativeMethod = OrigPeerConnection.prototype[method];
240
269
  if (!nativeMethod) return;
241
- OrigPeerConnection.prototype[method] = function(...args) {
270
+ OrigPeerConnection.prototype[method] = function(trackOrKind, ...args) {
242
271
  const serializedArgs = [
243
- typeof(args[0]) === 'string' ? args[0] : dumpTrackWithStreams(args[0]), // trackOrKind,
272
+ typeof(trackOrKind) === 'string' ? trackOrKind : dumpTrackWithStreams(trackOrKind),
244
273
  ];
245
- if (args[1]) {
246
- serializedArgs.push(JSON.parse(JSON.stringify(args[1])));
247
- if (args[1].streams) {
248
- serializedArgs[serializedArgs.length - 1].streams = args[1].streams.map(s => s.id);
274
+ if (args[0]) {
275
+ serializedArgs.push(JSON.parse(JSON.stringify(args[0])));
276
+ if (args[0].streams) {
277
+ serializedArgs[serializedArgs.length - 1].streams = args[0].streams.map(s => s.id);
249
278
  }
250
279
  }
251
280
  trace(method, this.__rtcStatsId, serializedArgs);
252
- const transceiver = nativeMethod.apply(this, args);
281
+ const transceiver = nativeMethod.apply(this, [trackOrKind, ...args]);
253
282
  transceiver.__rtcStatsId = this.__rtcStatsId;
254
283
  transceiver.sender.__rtcStatsId = this.__rtcStatsId;
255
284
  transceiver.sender.__rtcStatsSenderId = transceiver.receiver.track.id;
@@ -261,9 +290,9 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
261
290
  ['removeTrack'].forEach(method => {
262
291
  const nativeMethod = OrigPeerConnection.prototype[method];
263
292
  if (!nativeMethod) return;
264
- OrigPeerConnection.prototype[method] = function(...args) {
265
- trace(method, this.__rtcStatsId, args[0].__rtcStatsSenderId);
266
- return nativeMethod.apply(this, args);
293
+ OrigPeerConnection.prototype[method] = function(sender, ...args) {
294
+ trace(method, this.__rtcStatsId, sender.__rtcStatsSenderId);
295
+ return nativeMethod.apply(this, [sender, ...args]);
267
296
  };
268
297
  });
269
298
 
@@ -294,39 +323,33 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
294
323
  };
295
324
  });
296
325
 
297
- ['setLocalDescription', 'setRemoteDescription'].forEach(method => {
326
+ ['setLocalDescription'].forEach(method => {
298
327
  const nativeMethod = OrigPeerConnection.prototype[method];
299
328
  if (!nativeMethod) return;
300
329
  OrigPeerConnection.prototype[method] = function(...args) {
301
330
  const trackingId = compressMethod(method) + '-' + (counters[method]++);
302
331
  let implicitBaseDescription;
303
- if (method === 'setLocalDescription') {
304
- if (args[0]) {
305
- let explicitBaseDescription;
306
- if (args[0].type === 'offer') {
307
- explicitBaseDescription = this.__rtcStatsLastCreatedOffer;
308
- } else if (args[0].type === 'answer') {
309
- explicitBaseDescription = this.__rtcStatsLastCreatedAnswer;
310
- }
311
- delete this.__rtcStatsLastCreatedOffer;
312
- delete this.__rtcStatsLastCreatedAnswer;
313
- trace(method, this.__rtcStatsId,
314
- descriptionCompression(this.localDescription || explicitBaseDescription, args[0]),
315
- trackingId);
316
- } else {
317
- // Save previous localDescription for delta.
318
- implicitBaseDescription = JSON.parse(JSON.stringify(this.localDescription));
319
- trace(method, this.__rtcStatsId, null, trackingId);
332
+ if (args[0]) {
333
+ let explicitBaseDescription;
334
+ if (args[0].type === 'offer') {
335
+ explicitBaseDescription = this.__rtcStatsLastCreatedOffer;
336
+ } else if (args[0].type === 'answer') {
337
+ explicitBaseDescription = this.__rtcStatsLastCreatedAnswer;
320
338
  }
321
- } else if (method === 'setRemoteDescription') {
339
+ delete this.__rtcStatsLastCreatedOffer;
340
+ delete this.__rtcStatsLastCreatedAnswer;
322
341
  trace(method, this.__rtcStatsId,
323
- descriptionCompression(this.remoteDescription, args[0]),
342
+ descriptionCompression(this.localDescription || explicitBaseDescription, args[0]),
324
343
  trackingId);
344
+ } else {
345
+ // Save previous localDescription for delta.
346
+ implicitBaseDescription = JSON.parse(JSON.stringify(this.localDescription));
347
+ trace(method, this.__rtcStatsId, null, trackingId);
325
348
  }
326
349
 
327
350
  return nativeMethod.apply(this, args)
328
351
  .then(() => {
329
- if (method === 'setLocalDescription' && args.length === 0) {
352
+ if (args.length === 0) {
330
353
  trace(method + 'OnSuccess', this.__rtcStatsId,
331
354
  descriptionCompression(implicitBaseDescription, this.localDescription),
332
355
  trackingId);
@@ -342,6 +365,27 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
342
365
  };
343
366
  });
344
367
 
368
+ ['setRemoteDescription'].forEach(method => {
369
+ const nativeMethod = OrigPeerConnection.prototype[method];
370
+ if (!nativeMethod) return;
371
+ OrigPeerConnection.prototype[method] = function(description, ...args) {
372
+ const trackingId = compressMethod(method) + '-' + (counters[method]++);
373
+ trace(method, this.__rtcStatsId,
374
+ descriptionCompression(this.remoteDescription, description),
375
+ trackingId);
376
+
377
+ return nativeMethod.apply(this, [description, ...args])
378
+ .then(() => {
379
+ trace(method + 'OnSuccess', this.__rtcStatsId, undefined,
380
+ trackingId);
381
+ }, (err) => {
382
+ trace(method + 'OnFailure', this.__rtcStatsId, err.toString(),
383
+ trackingId);
384
+ throw err;
385
+ });
386
+ };
387
+ });
388
+
345
389
  ['addIceCandidate'].forEach(method => {
346
390
  const nativeMethod = OrigPeerConnection.prototype[method];
347
391
  if (!nativeMethod) return;
package/rtcstats.js CHANGED
@@ -8,7 +8,7 @@ import {WebSocketTrace} from './trace-websocket.js';
8
8
  * @returns {function} RTCStats trace function.
9
9
  */
10
10
  export function wrapRTCStatsWithDefaultOptions(config = {getStatsInterval: 1000}) {
11
- const trace = new WebSocketTrace();
11
+ const trace = new WebSocketTrace(config);
12
12
 
13
13
  // Wrap RTCPeerConnection-related APIs and events
14
14
  wrapRTCPeerConnection(trace, window, config);
@@ -1,11 +1,27 @@
1
1
  import {compressMethod} from '@rtcstats/rtcstats-shared';
2
2
 
3
3
  const PROTOCOL_VERSION = '5.0';
4
+ const RELOAD_COUNT_KEY = 'rtcstatsReloadCount';
4
5
 
5
- export function WebSocketTrace() {
6
+ export function WebSocketTrace(config = {}) {
6
7
  let buffer = [];
7
8
  let connection;
8
9
  let lastTime = 0;
10
+ let connectionStartTime = 0;
11
+ const createTime = Date.now();
12
+
13
+ // This counts the number of times the trace itself has been initialized.
14
+ // Typically this is done once per session and counting (re)loads based
15
+ // on that does not require listening for onload etc.
16
+ let reloadCount = undefined;
17
+ if (window.sessionStorage && config.countReloads) {
18
+ reloadCount = window.sessionStorage.getItem(RELOAD_COUNT_KEY);
19
+ if (reloadCount === null || isNaN(reloadCount)) {
20
+ reloadCount = -1;
21
+ }
22
+ reloadCount = parseInt(reloadCount, 10) + 1;
23
+ window.sessionStorage.setItem(RELOAD_COUNT_KEY, reloadCount);
24
+ }
9
25
  const trace = function(...args) {
10
26
  const now = Date.now();
11
27
  args.push(now - lastTime);
@@ -32,40 +48,64 @@ export function WebSocketTrace() {
32
48
  buffer.push(args);
33
49
  }
34
50
  };
35
- trace('create', null, {
36
- hardwareConcurrency: navigator.hardwareConcurrency,
37
- userAgentData: navigator.userAgentData,
38
- deviceMemory: navigator.deviceMemory,
39
- screen: {
40
- width: window.screen.availWidth,
41
- height: window.screen.availHeight,
42
- devicePixelRatio: window.devicePixelRatio,
43
- },
44
- window: {
45
- width: window.innerWidth,
46
- height: window.innerHeight,
47
- },
48
- });
49
51
 
50
52
  trace.close = () => {
51
- connection.close();
53
+ if (window.sessionStorage && config.countReloads) {
54
+ // A clean disconnect clears the reload count.
55
+ window.sessionStorage.removeItem(RELOAD_COUNT_KEY);
56
+ }
57
+ if (connection) {
58
+ connection.close();
59
+ }
52
60
  };
53
61
  trace.connect = (wsURL) => {
54
62
  if (connection) {
55
63
  connection.close();
56
64
  }
65
+ connectionStartTime = Date.now();
57
66
  connection = new WebSocket(wsURL, 'rtcstats#' + PROTOCOL_VERSION);
58
67
  connection.addEventListener('error', (e) => {
59
68
  // console.error('WS ERROR', e);
60
69
  });
61
70
 
62
- connection.addEventListener('close', () => {
71
+ connection.addEventListener('close', (e) => {
72
+ if (e.code === 1008 && config.log) {
73
+ config.log('rtcstats websocket connection closed with error=1008. ' +
74
+ 'Typically this means authorization is required and failed.');
75
+ }
63
76
  // reconnect?
64
77
  });
65
78
 
66
79
  connection.addEventListener('open', () => {
80
+ // Note: open is called while the socket is still authenticating.
81
+ // This can lead to messages being send and dropped when the token
82
+ // is not valid.
83
+
84
+ // Note: this does not use trace so avoids the buffer.
85
+ connection.send(JSON.stringify(['create', null, {
86
+ hardwareConcurrency: navigator.hardwareConcurrency,
87
+ userAgentData: navigator.userAgentData,
88
+ deviceMemory: navigator.deviceMemory,
89
+ screen: {
90
+ width: window.screen.availWidth,
91
+ height: window.screen.availHeight,
92
+ devicePixelRatio: window.devicePixelRatio,
93
+ },
94
+ window: {
95
+ width: window.innerWidth,
96
+ height: window.innerHeight,
97
+ },
98
+ reloadCount,
99
+ }, createTime]));
100
+ const connectionTime = Date.now() - connectionStartTime;
67
101
  setTimeout(function flush() {
68
102
  if (!buffer.length) {
103
+ trace('websocket', null, {
104
+ connectionTime,
105
+ });
106
+ return;
107
+ }
108
+ if (connection.readyState !== WebSocket.OPEN) {
69
109
  return;
70
110
  }
71
111
  connection.send(JSON.stringify(buffer.shift()));