@observertc/client-monitor-js 3.4.1-e6caa47.0 → 3.5.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 +394 -72
- package/lib/ClientMonitor.d.ts +5 -0
- package/lib/ClientMonitor.d.ts.map +1 -1
- package/lib/ClientMonitor.js +37 -1
- package/lib/collectors/MediasoupStatsCollector.d.ts.map +1 -1
- package/lib/collectors/MediasoupStatsCollector.js +10 -3
- package/lib/detectors/CongestionDetector.d.ts.map +1 -1
- package/lib/detectors/CongestionDetector.js +0 -1
- package/lib/entries/OutboundTrackStats.d.ts.map +1 -1
- package/lib/entries/OutboundTrackStats.js +1 -4
- package/lib/entries/PeerConnectionEntryManifest.d.ts +3 -2
- package/lib/entries/PeerConnectionEntryManifest.d.ts.map +1 -1
- package/lib/entries/PeerConnectionEntryManifest.js +14 -9
- package/lib/entries/StatsEntryInterfaces.d.ts +2 -0
- package/lib/entries/StatsEntryInterfaces.d.ts.map +1 -1
- package/lib/entries/StatsStorage.d.ts +7 -6
- package/lib/entries/StatsStorage.d.ts.map +1 -1
- package/lib/entries/StatsStorage.js +40 -220
- package/lib/utils/callEvents.d.ts +2 -0
- package/lib/utils/callEvents.d.ts.map +1 -1
- package/lib/utils/callEvents.js +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,11 +7,22 @@ Table of Contents:
|
|
|
7
7
|
- [Quick Start](#quick-start)
|
|
8
8
|
- [Integrations](#integrations)
|
|
9
9
|
- [Mediasoup](#mediasoup)
|
|
10
|
+
- [Collected Metrics](#collected-metrics)
|
|
11
|
+
- [Calculated updates](#calculated-updates)
|
|
12
|
+
- [PeerConnection Entry](#peerconnection-entry)
|
|
13
|
+
- [MediaStreamTrack Entry](#mediastreamtrack-entry)
|
|
14
|
+
- [InboundRTP Entry](#inboundrtp-entry)
|
|
15
|
+
- [OutboundRTP Entry](#outboundrtp-entry)
|
|
10
16
|
- [Detectors and Alerts](#detectors-and-alerts)
|
|
11
17
|
- [Audio Desync Detector](#audio-desync-detector)
|
|
12
18
|
- [CPU Performance Detector](#cpu-performance-detector)
|
|
13
|
-
- [Calculated updates](#calculated-updates)
|
|
14
19
|
- [Configurations](#configurations)
|
|
20
|
+
- [Events](#events)
|
|
21
|
+
- [CLIENT_JOINED Event](#client_joined-event)
|
|
22
|
+
- [CLIENT_LEFT Event](#client_left-event)
|
|
23
|
+
- [Custom Call Event](#custom-call-event)
|
|
24
|
+
- [Extension Stats Event](#extension-stats-event)
|
|
25
|
+
- [Sampling](#sampling)
|
|
15
26
|
- [NPM package](#npm-package)
|
|
16
27
|
- [API docs](#api-docs)
|
|
17
28
|
- [Schemas](#schemas)
|
|
@@ -25,6 +36,10 @@ Install it from [npm](https://www.npmjs.com/package/@observertc/client-monitor-j
|
|
|
25
36
|
```
|
|
26
37
|
npm i @observertc/client-monitor-js
|
|
27
38
|
```
|
|
39
|
+
or
|
|
40
|
+
```
|
|
41
|
+
yarn add @observertc/client-monitor-js
|
|
42
|
+
```
|
|
28
43
|
|
|
29
44
|
Add `@observertc/client-monitor-js` to your WebRTC app.
|
|
30
45
|
|
|
@@ -35,18 +50,17 @@ const config = {
|
|
|
35
50
|
collectingPeriodInMs: 5000,
|
|
36
51
|
};
|
|
37
52
|
const monitor = createClientMonitor(config);
|
|
38
|
-
const
|
|
53
|
+
const collector = monitor.collectors.addRTCPeerConnection(peerConnection);
|
|
39
54
|
|
|
40
55
|
monitor.on("stats-collected", () => {
|
|
41
|
-
const
|
|
42
|
-
for (const inboundRtp of storage.inboundRtps()) {
|
|
56
|
+
for (const inboundRtp of monitor.inboundRtps) {
|
|
43
57
|
const trackId = inboundRtp.getTrackId();
|
|
44
58
|
const remoteOutboundRtp = inboundRtp.getRemoteOutboundRtp();
|
|
45
59
|
console.log(trackId, inboundRtp.stats, remoteOutboundRtp.stats);
|
|
46
60
|
}
|
|
47
61
|
});
|
|
48
62
|
// if you want to stop collecting from the peerConnection, then:
|
|
49
|
-
|
|
63
|
+
collector.close();
|
|
50
64
|
```
|
|
51
65
|
|
|
52
66
|
The above example do as follows:
|
|
@@ -69,123 +83,314 @@ const config = {
|
|
|
69
83
|
collectingPeriodInMs: 5000,
|
|
70
84
|
};
|
|
71
85
|
const monitor = createClientMonitor(config);
|
|
72
|
-
const
|
|
86
|
+
const collector = monitor.collectors.addMediasoupDevice(mediasoupDevice);
|
|
87
|
+
|
|
88
|
+
collector.onclose = () => {
|
|
89
|
+
console.log(`mediasoup collector ${collector.id} is closed`);
|
|
90
|
+
}
|
|
73
91
|
|
|
74
92
|
monitor.on("stats-collected", () => {
|
|
75
93
|
// do your stuff
|
|
76
94
|
|
|
77
95
|
// you can close detach mediasoup
|
|
78
96
|
// collector by calling the close
|
|
79
|
-
|
|
97
|
+
collector.close();
|
|
80
98
|
});
|
|
81
99
|
```
|
|
82
100
|
|
|
83
|
-
**Important Note**: The created collector is hooked
|
|
84
|
-
and can detect transports automatically when they are created after the device is added.
|
|
85
|
-
If you create transports before you add the device to the monitor,
|
|
86
|
-
transports you created before will not be monitored automatically, you need to add them
|
|
87
|
-
to the statscollector, like this:
|
|
101
|
+
**Important Note**: The created collector is hooked to the device's 'newtransport' event and can automatically detect transports created **after** the device has been added. If you create transports before adding the device to the monitor, those previously created transports will not be monitored automatically. You will need to manually add them to the stats collector like this:
|
|
88
102
|
|
|
89
103
|
```javascript
|
|
90
|
-
const myTransport =
|
|
104
|
+
const myTransport = collector.addTransport(myTransport); // your transport created before the device is added to the monitor
|
|
91
105
|
```
|
|
92
106
|
|
|
93
|
-
##
|
|
107
|
+
## Collected Metrics
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
Collecting WebRTC Metrics is either done periodically according to the `collectingPeriodInMs` configuration or manually by calling the `monitor.collect()` method. The collected metrics are stored in the `ClientMonitor` instance and assigned to Entries.
|
|
110
|
+
Each entry in clientMonitor represents a WebRTC component such as `RTCPeerConnection`, `MediaStreamTrack`, etc. Entries can relate to each other and from one entry you can navigate to a correspondent entry. For example, from an `InboundRTP` entry you can navigate to the correspondent `RemoteOutboundRtp` entry, and reverse. Additionally entries are exposing basic derivatives calculated from the collected metrics.
|
|
96
111
|
|
|
97
|
-
### Storage Updates
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
import { createClientMonitor } from 'client-monitor-js';
|
|
113
|
+
### Calculated Updates
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
Calculated updates allow you to observe metrics derived from polled WebRTC stats captured by the library. These calculated updates provide a richer, more nuanced understanding of your application's client-side behavior, offering valuable insights beyond what raw stats metrics can provide.
|
|
116
|
+
|
|
117
|
+
For example by accessing `storage` you can get the following calculated updates:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
106
120
|
|
|
107
|
-
const storage = monitor.storage;
|
|
108
121
|
monitor.on('stats-collected', () => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
const {
|
|
123
|
+
totalInboundPacketsLost,
|
|
124
|
+
totalInboundPacketsReceived,
|
|
125
|
+
totalOutboundPacketsLost,
|
|
126
|
+
totalOutboundPacketsReceived,
|
|
127
|
+
totalOutboundPacketsSent,
|
|
128
|
+
totalSentAudioBytes,
|
|
129
|
+
totalSentVideoBytes,
|
|
130
|
+
totalReceivedAudioBytes,
|
|
131
|
+
totalReceivedVideoBytes,
|
|
132
|
+
totalDataChannelBytesSent,
|
|
133
|
+
totalDataChannelBytesReceived,
|
|
134
|
+
|
|
135
|
+
deltaInboundPacketsLost,
|
|
136
|
+
deltaInboundPacketsReceived,
|
|
137
|
+
deltaOutboundPacketsLost,
|
|
138
|
+
deltaOutboundPacketsReceived,
|
|
139
|
+
deltaOutboundPacketsSent,
|
|
140
|
+
deltaSentAudioBytes,
|
|
141
|
+
deltaSentVideoBytes,
|
|
142
|
+
deltaReceivedAudioBytes,
|
|
143
|
+
deltaReceivedVideoBytes,
|
|
144
|
+
deltaDataChannelBytesSent,
|
|
145
|
+
deltaDataChannelBytesReceived,
|
|
146
|
+
|
|
147
|
+
avgRttInS,
|
|
148
|
+
sendingAudioBitrate,
|
|
149
|
+
sendingVideoBitrate,
|
|
150
|
+
receivingAudioBitrate,
|
|
151
|
+
receivingVideoBitrate,
|
|
152
|
+
} = monitor.storage
|
|
153
|
+
|
|
154
|
+
console.log(`Total inbound packets lost: ${totalInboundPacketsLost}`);
|
|
155
|
+
console.log(`Total inbound packets received: ${totalInboundPacketsReceived}`);
|
|
156
|
+
console.log(`Total outbound packets lost: ${totalOutboundPacketsLost}`);
|
|
157
|
+
console.log(`Total outbound packets received: ${totalOutboundPacketsReceived}`);
|
|
158
|
+
console.log(`Total outbound packets sent: ${totalOutboundPacketsSent}`);
|
|
159
|
+
console.log(`Total sent audio bytes: ${totalSentAudioBytes}`);
|
|
160
|
+
console.log(`Total sent video bytes: ${totalSentVideoBytes}`);
|
|
161
|
+
console.log(`Total received audio bytes: ${totalReceivedAudioBytes}`);
|
|
162
|
+
console.log(`Total received video bytes: ${totalReceivedVideoBytes}`);
|
|
163
|
+
console.log(`Total data channel bytes sent: ${totalDataChannelBytesSent}`);
|
|
164
|
+
console.log(`Total data channel bytes received: ${totalDataChannelBytesReceived}`);
|
|
165
|
+
|
|
166
|
+
console.log(`Lost Inbound packets since last collecting: ${deltaInboundPacketsLost}`);
|
|
167
|
+
console.log(`Received Inbound packets since last collecting: ${deltaInboundPacketsReceived}`);
|
|
168
|
+
console.log(`Lost Outbound packets since last collecting: ${deltaOutboundPacketsLost}`);
|
|
169
|
+
console.log(`Received Outbound packets since last collecting: ${deltaOutboundPacketsReceived}`);
|
|
170
|
+
console.log(`Sent Outbound packets since last collecting: ${deltaOutboundPacketsSent}`);
|
|
171
|
+
console.log(`Sent audio bytes since last collecting: ${deltaSentAudioBytes}`);
|
|
172
|
+
console.log(`Sent video bytes since last collecting: ${deltaSentVideoBytes}`);
|
|
173
|
+
console.log(`Received audio bytes since last collecting: ${deltaReceivedAudioBytes}`);
|
|
174
|
+
console.log(`Received video bytes since last collecting: ${deltaReceivedVideoBytes}`);
|
|
175
|
+
console.log(`Data channel bytes sent since last collecting: ${deltaDataChannelBytesSent}`);
|
|
176
|
+
console.log(`Data channel bytes received since last collecting: ${deltaDataChannelBytesReceived}`);
|
|
177
|
+
|
|
178
|
+
console.log(`Average RTT: ${avgRttInS}`);
|
|
179
|
+
console.log(`Sending audio bitrate: ${sendingAudioBitrate}`);
|
|
180
|
+
console.log(`Sending video bitrate: ${sendingVideoBitrate}`);
|
|
181
|
+
console.log(`Receiving audio bitrate: ${receivingAudioBitrate}`);
|
|
182
|
+
console.log(`Receiving video bitrate: ${receivingVideoBitrate}`);
|
|
183
|
+
|
|
184
|
+
});
|
|
114
185
|
|
|
115
186
|
```
|
|
116
187
|
|
|
117
|
-
|
|
188
|
+
As mentioned above, the `ClientMonitor` instance stores entries, and each entry has a `stats` property that contains the raw stats collected from the WebRTC API. The `ClientMonitor` also stores calculated updates for each entry, which are derived from the raw stats. These calculated updates are accessible from the `ClientMonitor` instance and are updated every time the `stats-collected` event is emitted.
|
|
189
|
+
|
|
190
|
+
### PeerConnection Entry
|
|
118
191
|
|
|
192
|
+
**Accessing Stats**:
|
|
119
193
|
```javascript
|
|
120
|
-
|
|
194
|
+
// from monitor
|
|
195
|
+
monitor.peerConnections.forEach(pc => console.log(`PeerConnection: ${pc.statsId}`, pc.stats));
|
|
121
196
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
197
|
+
// or from storage
|
|
198
|
+
[...monitor.storage.peerConnections()].forEach(pc => console.log(`PeerConnection: ${pc.statsId}`, pc.stats));
|
|
199
|
+
```
|
|
126
200
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log(
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
**Accessing Calculated Updates**:
|
|
202
|
+
```javascript
|
|
203
|
+
|
|
204
|
+
monitor.on('stats-collected', () => {
|
|
205
|
+
for (const pc of monitor.peerConnections) {
|
|
206
|
+
console.log(`Between this and last collecting, the following stats were calculated for PeerConnection`, [
|
|
207
|
+
`Received bytes through data channel ${pc.deltaDataChannelBytesReceived}`,
|
|
208
|
+
`Sent bytes through data channel ${pc.deltaDataChannelBytesSent}`,
|
|
209
|
+
`Received bytes through inbound rtps ${pc.deltaInboundPacketsReceived}`,
|
|
210
|
+
`Sent bytes through outbound rtps ${pc.deltaOutboundPacketsSent}`,
|
|
211
|
+
`Lost packets on inbound rtps ${pc.deltaInboundPacketsReceived}`,
|
|
212
|
+
`Lost packets on outbound rtps ${pc.deltaOutboundPacketsSent}`,
|
|
213
|
+
`Outbound audio bitrate${pc.sendingAudioBitrate}`,
|
|
214
|
+
`Outbound video bitrate${pc.sendingVideoBitrate}`,
|
|
215
|
+
`Inbound audio bitrate${pc.receivingAudioBitrate}`,
|
|
216
|
+
`Inbound video bitrate${pc.receivingVideoBitrate}`,
|
|
217
|
+
`Received bytes on all audio tracks ${pc.deltaReceivedAudioBytes}`,
|
|
218
|
+
`Received bytes on all video tracks ${pc.deltaReceivedVideoBytes}`,
|
|
219
|
+
`Sent bytes on all audio tracks ${pc.deltaSentAudioBytes}`,
|
|
220
|
+
`Sent bytes on all video tracks ${pc.deltaSentVideoBytes}`,
|
|
221
|
+
].join('\n'));
|
|
135
222
|
}
|
|
136
223
|
});
|
|
137
224
|
```
|
|
138
225
|
|
|
139
|
-
|
|
226
|
+
**Accessing related entries**:
|
|
140
227
|
|
|
141
228
|
```javascript
|
|
142
|
-
|
|
229
|
+
const pc = monitor.getPeerConnectionStats(peerConnectionId);
|
|
230
|
+
if (pc) {
|
|
231
|
+
[...pc.inboundRtps()].forEach((inboundRtp) => void 0);
|
|
232
|
+
[...pc.outboundRtps()].forEach((outboundRtp) => void 0);
|
|
233
|
+
|
|
234
|
+
[...pc.localCandidates()].forEach((localICECandidate) => void 0);
|
|
235
|
+
[...pc.remoteCandidates()].forEach((remoteICECandidate) => void 0);
|
|
236
|
+
[...pc.iceCandidatePairs()].forEach((iceCandidatePair) => void 0);
|
|
237
|
+
|
|
238
|
+
[...pc.sctpTransports()].forEach((sctpTransport) => void 0);
|
|
239
|
+
[...pc.transceivers()].forEach((transceiver) => void 0);
|
|
240
|
+
[...pc.senders()].forEach((sender) => void 0);
|
|
241
|
+
[...pc.receivers()].forEach((receiver) => void 0);
|
|
242
|
+
[...pc.transports()].forEach((transport) => void 0);
|
|
243
|
+
[...pc.certificates()].forEach((certificate) => void 0);
|
|
244
|
+
[...pc.iceServers()].forEach((iceServer) => void 0);
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
```
|
|
143
248
|
|
|
144
|
-
|
|
145
|
-
const monitor = createClientMonitor({
|
|
146
|
-
collectingPeriodInMs: 2000,
|
|
147
|
-
});
|
|
249
|
+
### MediaStreamTrack Entry
|
|
148
250
|
|
|
149
|
-
const storage = monitor.storage;
|
|
150
|
-
monitor.on("stats-collected", () => {
|
|
151
|
-
for (const inboundRtp of storage.inboundRtps()) {
|
|
152
|
-
console.log("mean opinion score for inbound-rtp", inboundRtp.score);
|
|
153
251
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
252
|
+
**Collected tracks**:
|
|
253
|
+
```javascript
|
|
254
|
+
monitor.on('stats-collected', () => {
|
|
255
|
+
for (const track of monitor.tracks) {
|
|
256
|
+
console.log(`Track ${track.trackId} is ${track.kind}`);
|
|
158
257
|
}
|
|
159
258
|
});
|
|
160
259
|
```
|
|
161
260
|
|
|
162
|
-
|
|
261
|
+
**Accessing Calculated Updates**:
|
|
262
|
+
```javascript
|
|
263
|
+
monitor.on('stats-collected', () => {
|
|
264
|
+
for (const track of monitor.tracks) {
|
|
265
|
+
if (track?.direction === 'outbound') {
|
|
266
|
+
console.log(`Stats belongs to Track ${track.trackId} `);
|
|
267
|
+
console.log(`Lost packets reported by remote endpoint: ${track.remoteLostPackets}`);
|
|
268
|
+
console.log(`Received packets reported by remote endpoint: ${track.remoteReceivedPackets}`);
|
|
269
|
+
console.log(`Sent packets reported by local endpoint: ${track.sentPackets}`);
|
|
270
|
+
console.log(`Sending bitrate ${track.sendingBitrate}`);
|
|
271
|
+
console.log(`Associated sfu stream id: ${track.sfuStreamId}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (trackStats?.direction === 'inbound') {
|
|
275
|
+
console.log(`Stats belongs to Track ${track.trackId} `);
|
|
276
|
+
console.log(`Lost packets reported by local endpoint: ${track.lostPackets}`);
|
|
277
|
+
console.log(`Received packets reported by local endpoint: ${track.receivedPackets}`);
|
|
278
|
+
console.log(`Received bitrate ${track.receivingBitrate}`);
|
|
279
|
+
console.log(`Associated sfu stream id: ${track.sfuStreamId}`);
|
|
280
|
+
console.log(`Associated sfu sink id: ${track.sfuSinkId}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
```
|
|
163
285
|
|
|
286
|
+
**Accessing related entries**
|
|
164
287
|
```javascript
|
|
165
|
-
|
|
288
|
+
monitor.on('stats-collected', () => {
|
|
289
|
+
for (const track of monitor.tracks) {
|
|
290
|
+
console.log(`Track ${track.trackId} is ${track.kind}`);
|
|
291
|
+
if (track.direction === 'outbound') {
|
|
292
|
+
[...track.outboundRtps()].forEach(outboundRtp => {
|
|
293
|
+
console.log(`Outbound RTP ${outboundRtp.getSsrc()} has ${outboundRtp.sentPackets} sent packets`);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (track.direction === 'inbound') {
|
|
298
|
+
[...track.inboundRtps()].forEach(inboundRtp => {
|
|
299
|
+
console.log(`Inbound RTP ${inboundRtp.getSsrc()} has ${inboundRtp.receivedPackets} received packets`);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
```
|
|
166
305
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
306
|
+
### InboundRTP Entry
|
|
307
|
+
|
|
308
|
+
**Accessing Stats**:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
monitor.on('stats-collected', () => {
|
|
312
|
+
for (const inboundRtp of monitor.inboundRtps) {
|
|
313
|
+
console.log(`InboundRtp ${inboundRtp.statsId} collected stats`, inboundRtp.stats);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
**Accessing Calculated Updates**:
|
|
318
|
+
```javascript
|
|
319
|
+
monitor.on('stats-collected', () => {
|
|
320
|
+
for (const inboundRtp of monitor.inboundRtps) {
|
|
321
|
+
console.log(`InboundRtp ${inboundRtp.getSsrc()} `);
|
|
322
|
+
console.log(`Received packets since last collecting: ${inboundRtp.receivedBytes}`);
|
|
323
|
+
console.log(`Received bitrate since last collecting: ${inboundRtp.receivingBitrate}`);
|
|
324
|
+
console.log(`Received packets since last collecting: ${inboundRtp.receivedPackets}`);
|
|
325
|
+
console.log(`Received frames since last collecting: ${inboundRtp.receivedFrames}`);
|
|
326
|
+
console.log(`Lost packets since last collecting: ${inboundRtp.lostPackets}`);
|
|
327
|
+
console.log(`Average jitter buffer delay since last collecting: ${inboundRtp.avgJitterBufferDelayInMs}`);
|
|
328
|
+
console.log(`Average RTT since last collecting: ${inboundRtp.avgRttInS}s`);
|
|
329
|
+
|
|
330
|
+
console.log(`Associated sfu stream id: ${inboundRtp.sfuStreamId}`);
|
|
331
|
+
console.log(`Associated sfu sink id: ${inboundRtp.sfuSinkId}`);
|
|
332
|
+
|
|
333
|
+
}
|
|
170
334
|
});
|
|
335
|
+
```
|
|
171
336
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
337
|
+
**Accessing related entries**:
|
|
338
|
+
```javascript
|
|
339
|
+
monitor.on('stats-collected', () => {
|
|
340
|
+
for (const inboundRtp of monitor.inboundRtps) {
|
|
341
|
+
|
|
342
|
+
console.log(`inbound RTP associated with SSRC ${inboundRtp.getSsrc()} uses codec ${inboundRtp.getCodec()?.stats.mimeType}`);
|
|
343
|
+
console.log(`inbound RTP associated with SSRC ${inboundRtp.getSsrc()} remote endpoint sent ${inboundRtp.getRemoteOutboundRtp()?.stats.packetsSent} packets`);
|
|
176
344
|
|
|
177
|
-
console.log(
|
|
178
|
-
console.log("sent packets since last stats-collected", outboundRtp.sentPackets);
|
|
345
|
+
inboundRtp.kind ==='audio' && console.log(`inbound RTP associated with SSRC ${inboundRtp.getSsrc()} has played out ${inboundRtp.getAudioPlayout()?.stats.totalSamplesCount} audio samples`);
|
|
179
346
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
347
|
+
const audioPlayout = inboundRtp.getAudioPlayout(); // The AudioPlayoutEntry associated with the inboundRtp
|
|
348
|
+
const peerConnection = inboundRtp.getPeerConnection(); // The PeerConnectionEntry associated with the inboundRtp
|
|
349
|
+
const trackId = inboundRtp.getTrackId(); // The trackId associated with the inboundRtp
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### OutboundRTP Entry
|
|
355
|
+
|
|
356
|
+
**Accessing Stats**:
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
monitor.on('stats-collected', () => {
|
|
360
|
+
for (const outboundRtp of monitor.outboundRtps) {
|
|
361
|
+
console.log(`OutboundRtp ${outboundRtp.statsId} collected stats`, outboundRtp.stats);
|
|
185
362
|
}
|
|
186
363
|
});
|
|
187
364
|
```
|
|
188
365
|
|
|
366
|
+
**Accessing Calculated Updates**:
|
|
367
|
+
```javascript
|
|
368
|
+
monitor.on('stats-collected', () => {
|
|
369
|
+
for (const outboundRtp of monitor.outboundRtps) {
|
|
370
|
+
console.log(`OutboundRtp ${outboundRtp.getSsrc()} `);
|
|
371
|
+
console.log(`Sent packets since last collecting: ${outboundRtp.sentPackets}`);
|
|
372
|
+
console.log(`Sending bitrate since last collecting: ${outboundRtp.sendingBitrate}`);
|
|
373
|
+
console.log(`Sent bytes since last collecting: ${outboundRtp.sentBytes}`);
|
|
374
|
+
console.log(`Associated sfu stream id: ${outboundRtp.sfuStreamId}`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Accessing related entries**:
|
|
380
|
+
```javascript
|
|
381
|
+
monitor.on('stats-collected', () => {
|
|
382
|
+
for (const outboundRtp of monitor.outboundRtps) {
|
|
383
|
+
console.log(`outbound RTP associated with SSRC ${outboundRtp.getSsrc()} uses codec ${outboundRtp.getCodec()?.stats.mimeType}`);
|
|
384
|
+
console.log(`outbound RTP associated with SSRC ${outboundRtp.getSsrc()} remote endpoint received ${outboundRtp.getRemoteInboundRtp()?.stats.packetsReceived} packets`);
|
|
385
|
+
|
|
386
|
+
const remoteInboundRtp = outboundRtp.getRemoteInboundRtp(); // The RemoteInboundRtpEntry associated with the outboundRtp
|
|
387
|
+
const peerConnection = outboundRtp.getPeerConnection(); // The PeerConnectionEntry associated with the outboundRtp
|
|
388
|
+
const trackId = outboundRtp.getTrackId(); // The trackId associated with the outboundRtp
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
|
|
189
394
|
## Detectors and Alerts
|
|
190
395
|
|
|
191
396
|
Detectors and alerts provide events of tracking and responding to anomalies or performance issues based on the polled stats. Detectors are components that continuously monitor for specific conditions in the polled stats, and set an alert if certain thresholds are hit. You can subscribe to alerts of an instantiated client-monitor-js and configure detectors via initial configurations.
|
|
@@ -281,6 +486,7 @@ const config = {
|
|
|
281
486
|
* By setting this, the observer makes samples after n number or collected stats.
|
|
282
487
|
*
|
|
283
488
|
* For example if the value is 10, the observer makes a sample after 10 collected stats (in every 10 collectingPeriodInMs).
|
|
489
|
+
* if the value is less or equal than 0
|
|
284
490
|
*
|
|
285
491
|
* DEFAULT: 1
|
|
286
492
|
*/
|
|
@@ -288,6 +494,122 @@ const config = {
|
|
|
288
494
|
};
|
|
289
495
|
```
|
|
290
496
|
|
|
497
|
+
## Events
|
|
498
|
+
|
|
499
|
+
In the context of our monitoring library, events play a crucial role in enabling real-time insights and interactions. These events are emitted by the monitor to signal various occurrences, such as the creation of a peer connection, media track, or ICE connections. Below, we detail the events detected by the monitor.
|
|
500
|
+
* `CLIENT_JOINED`: A client has joined
|
|
501
|
+
* `CLIENT_LEFT`: A client has left
|
|
502
|
+
* `PEER_CONNECTION_OPENED`: A peer connection is opened
|
|
503
|
+
* `PEER_CONNECTION_CLOSED`: A peer connection is closed
|
|
504
|
+
* `MEDIA_TRACK_ADDED`: A media track is added
|
|
505
|
+
* `MEDIA_TRACK_REMOVED`: A media track is removed
|
|
506
|
+
* `MEDIA_TRACK_MUTED`: A media track is muted
|
|
507
|
+
* `MEDIA_TRACK_UNMUTED`: A media track is unmuted
|
|
508
|
+
* `ICE_GATHERING_STATE_CHANGED`: The ICE gathering state has changed
|
|
509
|
+
* `PEER_CONNECTION_STATE_CHANGED`: The peer connection state has changed
|
|
510
|
+
* `ICE_CONNECTION_STATE_CHANGED`: The ICE connection state has changed
|
|
511
|
+
* `DATA_CHANNEL_OPEN`: A data channel is opened
|
|
512
|
+
* `DATA_CHANNEL_CLOSED`: A data channel is closed
|
|
513
|
+
* `DATA_CHANNEL_ERROR`: A data channel error occurred
|
|
514
|
+
|
|
515
|
+
For Mediasoup integration the following events are detected:
|
|
516
|
+
* `PRODUCER_ADDED`: A producer is added
|
|
517
|
+
* `PRODUCER_REMOVED`: A producer is removed
|
|
518
|
+
* `PRODUCER_PAUSED`: A producer is paused
|
|
519
|
+
* `PRODUCER_RESUMED`: A producer is resumed
|
|
520
|
+
* `CONSUMER_ADDED`: A consumer is added
|
|
521
|
+
* `CONSUMER_REMOVED`: A consumer is removed
|
|
522
|
+
* `CONSUMER_PAUSED`: A consumer is paused
|
|
523
|
+
* `CONSUMER_RESUMED`: A consumer is resumed
|
|
524
|
+
* `DATA_PRODUCER_ADDED`: A data producer is added
|
|
525
|
+
* `DATA_PRODUCER_REMOVED`: A data producer is removed
|
|
526
|
+
* `DATA_CONSUMER_ADDED`: A data consumer is added
|
|
527
|
+
* `DATA_CONSUMER_REMOVED`: A data consumer is removed
|
|
528
|
+
|
|
529
|
+
### CLIENT_JOINED Event
|
|
530
|
+
|
|
531
|
+
A client event is automatically generated when the first sample is created, and
|
|
532
|
+
the joined timestamp is set to the time of the monitor creation. However, you can
|
|
533
|
+
manually set the event by calling the `monitor.join()` method.
|
|
534
|
+
|
|
535
|
+
**Important Note**: The `monitor.join()` method is called only once, and in case of manual setup it should be called before the first sample is created.
|
|
536
|
+
|
|
537
|
+
```javascript
|
|
538
|
+
monitor.join({
|
|
539
|
+
timestamp: Date.now(),
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### CLIENT_LEFT Event
|
|
544
|
+
|
|
545
|
+
A client event is automatically generated when the monitor is closed, and the left timestamp is set to the time of the monitor closing. However, you can manually set the event by calling the `monitor.leave()` method.
|
|
546
|
+
|
|
547
|
+
**Important Note**: The `monitor.leave()` method is called only once, and in case of manual setup it should be called before the monitor is closed.
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
monitor.leave({
|
|
551
|
+
timestamp: Date.now(),
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
### Custom Call Event
|
|
557
|
+
|
|
558
|
+
You can create a custom event by calling the `monitor.addCustomCallEvent()` method. The method takes an object with the following properties:
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
monitor.addCustomCallEvent({
|
|
562
|
+
name: 'MY CUSTOM EVENT', // mandatory
|
|
563
|
+
|
|
564
|
+
message: 'my custom message', // optional;
|
|
565
|
+
attachments: JSON.stringify({
|
|
566
|
+
value: 'my custom value',
|
|
567
|
+
}), // optional
|
|
568
|
+
timestamp: Date.now(), // optional
|
|
569
|
+
value: `simple string value`, // optional
|
|
570
|
+
peerConnectionId: 'peer-connection-id', // optional;
|
|
571
|
+
mediaTrackId: 'media-track-id', // optional;
|
|
572
|
+
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Extension Stats Event
|
|
577
|
+
|
|
578
|
+
You can create an extension state event by calling the `monitor.addExtensionStats()` method. The method takes an object with the following properties:
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
monitor.addExtensionStats({
|
|
582
|
+
type: 'CLIENT_CPU_STATS',
|
|
583
|
+
payload: {
|
|
584
|
+
cpuUsage: 0.5,
|
|
585
|
+
memoryUsage: 0.3,
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
As a rule of thumb for when to use `addCustomCallEvent` and `addExtensionStateEvent`:
|
|
591
|
+
- Use `addCustomCallEvent` for events that are related to the call itself, such as user clicked a button.
|
|
592
|
+
- Use `addExtensionStats` for events that are related to the client's environment, such as CPU usage and collected periodically.
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
## Sampling
|
|
596
|
+
|
|
597
|
+
The monitor generates samples by invoking the `monitor.sample()` method. The monitor will automatically call the `sample()` method unless `samplingTick` is set to a value less than or equal to 0. The ClientMonitor creates a `ClientSample`, a compound object that contains all observed stats and created events. The ClientSample object is emitted, and an event listener can listen to it by subscribing to the `sample-created` event:
|
|
598
|
+
|
|
599
|
+
```javascript
|
|
600
|
+
monitor.on('sample-created', ({
|
|
601
|
+
clientSample,
|
|
602
|
+
elapsedSinceLastSampleInMs,
|
|
603
|
+
}) => {
|
|
604
|
+
console.log('The created client sample is:', clientSample);
|
|
605
|
+
console.log('Elapsed time in milliseconds since the last sample was created:', elapsedSinceLastSampleInMs);
|
|
606
|
+
})
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
The ClientSample can be forwarded to a backend service, where the samples can be further processed (e.g., saved into a database or used for anomaly detection). To facilitate this, we have developed the [observer-js](https://github.com/ObserveRTC/observer-js) library, which you can use to process the samples.
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
291
613
|
## NPM package
|
|
292
614
|
|
|
293
615
|
https://www.npmjs.com/package/@observertc/client-monitor-js
|
package/lib/ClientMonitor.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface ClientMonitorEvents {
|
|
|
42
42
|
}
|
|
43
43
|
export declare class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents> {
|
|
44
44
|
private _config;
|
|
45
|
+
readonly created: number;
|
|
45
46
|
readonly meta: ClientMetaData;
|
|
46
47
|
readonly storage: StatsStorage;
|
|
47
48
|
readonly collectors: {
|
|
@@ -98,6 +99,8 @@ export declare class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents
|
|
|
98
99
|
private readonly _detectors;
|
|
99
100
|
private readonly _sampler;
|
|
100
101
|
private _timer?;
|
|
102
|
+
private _joined;
|
|
103
|
+
private _left;
|
|
101
104
|
private _lastCollectedAt;
|
|
102
105
|
private _lastSampledAt;
|
|
103
106
|
private _closed;
|
|
@@ -107,6 +110,8 @@ export declare class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents
|
|
|
107
110
|
close(): void;
|
|
108
111
|
collect(): Promise<CollectedStats>;
|
|
109
112
|
sample(): ClientSample | undefined;
|
|
113
|
+
join(settings?: Pick<CustomCallEvent, 'attachments' | 'timestamp' | 'message'>): void;
|
|
114
|
+
leave(settings?: Pick<CustomCallEvent, 'attachments' | 'timestamp' | 'message'>): void;
|
|
110
115
|
setMarker(value?: string): void;
|
|
111
116
|
setMediaDevices(...devices: MediaDevice[]): void;
|
|
112
117
|
addUserMediaError(err: unknown): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientMonitor.d.ts","sourceRoot":"","sources":["../src/ClientMonitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAoB,MAAM,cAAc,CAAC;AAEhE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAIzD,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAEjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,EAAE,4BAA4B,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAI1E,MAAM,MAAM,mBAAmB,GAAG;IAE9B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,KAAK,CAAC;AAEtC,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,KAAK,CAAC;IACf,OAAO,EAAE,SAAS,CAAC;IACnB,iBAAiB,EAAE;QACf,6BAA6B,EAAE,MAAM,CAAC;QACtC,cAAc,EAAE,cAAc,CAAC;KAClC,CAAC;IACF,gBAAgB,EAAE;QACd,0BAA0B,EAAE,MAAM,CAAC;QACnC,YAAY,EAAE,YAAY,CAAC;KAC9B,CAAC;IAEF,kBAAkB,EAAE,UAAU,CAAC;IAC/B,oBAAoB,EAAE,UAAU,CAAC;IACjC,uBAAuB,EAAE,UAAU,CAAC;CACvC;AAED,qBAAa,aAAc,SAAQ,iBAAiB,CAAC,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"ClientMonitor.d.ts","sourceRoot":"","sources":["../src/ClientMonitor.ts"],"names":[],"mappings":"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"}
|