@observertc/client-monitor-js 3.11.1 → 4.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.
Files changed (237) hide show
  1. package/README.md +1821 -667
  2. package/dist/ClientMonitor.d.ts +114 -0
  3. package/dist/ClientMonitor.js +1 -0
  4. package/dist/ClientMonitorConfig.d.ts +66 -0
  5. package/dist/ClientMonitorConfig.js +1 -0
  6. package/dist/ClientMonitorEvents.d.ts +170 -0
  7. package/dist/ClientMonitorEvents.js +1 -0
  8. package/dist/adapters/Firefox94StatsAdapter.d.ts +5 -0
  9. package/dist/adapters/Firefox94StatsAdapter.js +1 -0
  10. package/dist/adapters/FirefoxTransportStatsAdapter.d.ts +10 -0
  11. package/dist/adapters/FirefoxTransportStatsAdapter.js +1 -0
  12. package/dist/adapters/StatsAdapter.d.ts +6 -0
  13. package/dist/adapters/StatsAdapter.js +1 -0
  14. package/dist/adapters/StatsAdapters.d.ts +9 -0
  15. package/dist/adapters/StatsAdapters.js +1 -0
  16. package/dist/collectors/MediasoupTransportStatsCollector.d.ts +20 -0
  17. package/dist/collectors/MediasoupTransportStatsCollector.js +1 -0
  18. package/dist/collectors/RtcPeerConnectionStatsCollector.d.ts +16 -0
  19. package/dist/collectors/RtcPeerConnectionStatsCollector.js +1 -0
  20. package/dist/collectors/StatsCollector.d.ts +5 -0
  21. package/dist/collectors/StatsCollector.js +1 -0
  22. package/dist/collectors/utils.d.ts +2 -0
  23. package/dist/collectors/utils.js +1 -0
  24. package/dist/detectors/AudioDesyncDetector.d.ts +13 -0
  25. package/dist/detectors/AudioDesyncDetector.js +1 -0
  26. package/dist/detectors/CongestionDetector.d.ts +15 -0
  27. package/dist/detectors/CongestionDetector.js +1 -0
  28. package/dist/detectors/CpuPerformanceDetector.d.ts +8 -0
  29. package/dist/detectors/CpuPerformanceDetector.js +1 -0
  30. package/dist/detectors/Detector.d.ts +4 -0
  31. package/dist/detectors/Detector.js +1 -0
  32. package/dist/detectors/Detectors.d.ts +10 -0
  33. package/dist/detectors/Detectors.js +1 -0
  34. package/dist/detectors/DryInboundTrackDetector.d.ts +12 -0
  35. package/dist/detectors/DryInboundTrackDetector.js +1 -0
  36. package/dist/detectors/DryOutboundTrackDetector.d.ts +12 -0
  37. package/dist/detectors/DryOutboundTrackDetector.js +1 -0
  38. package/dist/detectors/FreezedVideoTrackDetector.d.ts +11 -0
  39. package/dist/detectors/FreezedVideoTrackDetector.js +1 -0
  40. package/dist/detectors/IceTupleChangeDetector.d.ts +9 -0
  41. package/dist/detectors/IceTupleChangeDetector.js +1 -0
  42. package/dist/detectors/LongPcConnectionEstablishment.d.ts +10 -0
  43. package/dist/detectors/LongPcConnectionEstablishment.js +1 -0
  44. package/dist/detectors/PlayoutDiscrepancyDetector.d.ts +11 -0
  45. package/dist/detectors/PlayoutDiscrepancyDetector.js +1 -0
  46. package/dist/detectors/ResolutionChangeDetector.d.ts +1 -0
  47. package/dist/detectors/ResolutionChangeDetector.js +1 -0
  48. package/dist/detectors/SynthesizedSamplesDetector.d.ts +10 -0
  49. package/dist/detectors/SynthesizedSamplesDetector.js +1 -0
  50. package/dist/index.d.ts +26 -0
  51. package/dist/index.js +1 -0
  52. package/dist/monitors/CertificateMonitor.d.ts +19 -0
  53. package/dist/monitors/CertificateMonitor.js +1 -0
  54. package/dist/monitors/CodecMonitor.d.ts +22 -0
  55. package/dist/monitors/CodecMonitor.js +1 -0
  56. package/dist/monitors/DataChannelMonitor.d.ts +25 -0
  57. package/dist/monitors/DataChannelMonitor.js +1 -0
  58. package/dist/monitors/IceCandidateMonitor.d.ts +29 -0
  59. package/dist/monitors/IceCandidateMonitor.js +1 -0
  60. package/dist/monitors/IceCandidatePairMonitor.d.ts +44 -0
  61. package/dist/monitors/IceCandidatePairMonitor.js +1 -0
  62. package/dist/monitors/IceTransportMonitor.d.ts +38 -0
  63. package/dist/monitors/IceTransportMonitor.js +1 -0
  64. package/dist/monitors/InboundRtpMonitor.d.ts +106 -0
  65. package/dist/monitors/InboundRtpMonitor.js +1 -0
  66. package/dist/monitors/InboundTrackMonitor.d.ts +26 -0
  67. package/dist/monitors/InboundTrackMonitor.js +1 -0
  68. package/dist/monitors/MediaPlayoutMonitor.d.ts +26 -0
  69. package/dist/monitors/MediaPlayoutMonitor.js +1 -0
  70. package/dist/monitors/MediaSourceMonitor.d.ts +28 -0
  71. package/dist/monitors/MediaSourceMonitor.js +1 -0
  72. package/dist/monitors/OutboundRtpMonitor.d.ts +63 -0
  73. package/dist/monitors/OutboundRtpMonitor.js +1 -0
  74. package/dist/monitors/OutboundTrackMonitor.d.ts +29 -0
  75. package/dist/monitors/OutboundTrackMonitor.js +1 -0
  76. package/dist/monitors/PeerConnectionMonitor.d.ts +145 -0
  77. package/dist/monitors/PeerConnectionMonitor.js +1 -0
  78. package/dist/monitors/PeerConnectionTransportMonitor.d.ts +17 -0
  79. package/dist/monitors/PeerConnectionTransportMonitor.js +1 -0
  80. package/dist/monitors/RemoteInboundRtpMonitor.d.ts +31 -0
  81. package/dist/monitors/RemoteInboundRtpMonitor.js +1 -0
  82. package/dist/monitors/RemoteOutboundRtpMonitor.d.ts +30 -0
  83. package/dist/monitors/RemoteOutboundRtpMonitor.js +1 -0
  84. package/dist/monitors/TrackMonitor.d.ts +7 -0
  85. package/dist/monitors/TrackMonitor.js +1 -0
  86. package/dist/schema/ClientEventTypes.d.ts +211 -0
  87. package/dist/schema/ClientEventTypes.js +1 -0
  88. package/dist/schema/ClientMetaTypes.d.ts +33 -0
  89. package/dist/schema/ClientMetaTypes.js +1 -0
  90. package/dist/schema/ClientSample.d.ts +355 -0
  91. package/dist/schema/ClientSample.js +1 -0
  92. package/dist/schema/W3cStatsIdentifiers.d.ts +37 -0
  93. package/dist/schema/W3cStatsIdentifiers.js +1 -0
  94. package/dist/scores/CalculatedScore.d.ts +79 -0
  95. package/dist/scores/CalculatedScore.js +1 -0
  96. package/dist/scores/DefaultScoreCalculator.d.ts +47 -0
  97. package/dist/scores/DefaultScoreCalculator.js +1 -0
  98. package/dist/scores/ScoreCalculator.d.ts +9 -0
  99. package/dist/scores/ScoreCalculator.js +1 -0
  100. package/{lib/entries/UpdateFields.d.ts → dist/scores/mosScores.d.ts} +0 -1
  101. package/dist/scores/mosScores.js +1 -0
  102. package/dist/sources/ClientEventPayloadProvider.d.ts +67 -0
  103. package/dist/sources/ClientEventPayloadProvider.js +1 -0
  104. package/dist/sources/MediasoupDeviceBinding.d.ts +10 -0
  105. package/dist/sources/MediasoupDeviceBinding.js +1 -0
  106. package/dist/sources/MediasoupTransportBinding.d.ts +21 -0
  107. package/dist/sources/MediasoupTransportBinding.js +1 -0
  108. package/dist/sources/RtcPeerConnectionBinding.d.ts +36 -0
  109. package/dist/sources/RtcPeerConnectionBinding.js +1 -0
  110. package/dist/sources/Sources.d.ts +27 -0
  111. package/dist/sources/Sources.js +1 -0
  112. package/dist/sources/fetchUserAgentData.d.ts +2 -0
  113. package/dist/sources/fetchUserAgentData.js +1 -0
  114. package/dist/sources/inferSourceType.d.ts +2 -0
  115. package/dist/sources/inferSourceType.js +1 -0
  116. package/dist/sources/watchMediaDevice.d.ts +2 -0
  117. package/dist/sources/watchMediaDevice.js +1 -0
  118. package/dist/utils/Processor.d.ts +10 -0
  119. package/dist/utils/Processor.js +1 -0
  120. package/{lib → dist}/utils/common.d.ts +2 -1
  121. package/dist/utils/common.js +1 -0
  122. package/dist/utils/logger.d.ts +9 -0
  123. package/dist/utils/logger.js +1 -0
  124. package/{lib → dist}/utils/validators.d.ts +0 -1
  125. package/dist/utils/validators.js +1 -0
  126. package/package.json +45 -22
  127. package/lib/ClientMonitor.d.ts +0 -318
  128. package/lib/ClientMonitor.d.ts.map +0 -1
  129. package/lib/ClientMonitor.js +0 -978
  130. package/lib/Collectors.d.ts +0 -50
  131. package/lib/Collectors.d.ts.map +0 -1
  132. package/lib/Collectors.js +0 -182
  133. package/lib/Sampler.d.ts +0 -37
  134. package/lib/Sampler.d.ts.map +0 -1
  135. package/lib/Sampler.js +0 -516
  136. package/lib/collectors/Adapter.d.ts +0 -14
  137. package/lib/collectors/Adapter.d.ts.map +0 -1
  138. package/lib/collectors/Adapter.js +0 -57
  139. package/lib/collectors/MediasoupStatsCollector.d.ts +0 -23
  140. package/lib/collectors/MediasoupStatsCollector.d.ts.map +0 -1
  141. package/lib/collectors/MediasoupStatsCollector.js +0 -305
  142. package/lib/collectors/MediasoupSurrogates.d.ts +0 -83
  143. package/lib/collectors/MediasoupSurrogates.d.ts.map +0 -1
  144. package/lib/collectors/MediasoupSurrogates.js +0 -2
  145. package/lib/collectors/PeerConnectionStatsCollector.d.ts +0 -21
  146. package/lib/collectors/PeerConnectionStatsCollector.d.ts.map +0 -1
  147. package/lib/collectors/PeerConnectionStatsCollector.js +0 -117
  148. package/lib/collectors/StatsCollector.d.ts +0 -12
  149. package/lib/collectors/StatsCollector.d.ts.map +0 -1
  150. package/lib/collectors/StatsCollector.js +0 -2
  151. package/lib/collectors/StatsProvider.d.ts +0 -7
  152. package/lib/collectors/StatsProvider.d.ts.map +0 -1
  153. package/lib/collectors/StatsProvider.js +0 -11
  154. package/lib/collectors/browserAdapterMiddlewares.d.ts +0 -5
  155. package/lib/collectors/browserAdapterMiddlewares.d.ts.map +0 -1
  156. package/lib/collectors/browserAdapterMiddlewares.js +0 -75
  157. package/lib/collectors/utils.d.ts +0 -16
  158. package/lib/collectors/utils.d.ts.map +0 -1
  159. package/lib/collectors/utils.js +0 -73
  160. package/lib/detectors/AudioDesyncDetector.d.ts +0 -52
  161. package/lib/detectors/AudioDesyncDetector.d.ts.map +0 -1
  162. package/lib/detectors/AudioDesyncDetector.js +0 -77
  163. package/lib/detectors/CongestionDetector.d.ts +0 -49
  164. package/lib/detectors/CongestionDetector.d.ts.map +0 -1
  165. package/lib/detectors/CongestionDetector.js +0 -139
  166. package/lib/detectors/CpuPerformanceDetector.d.ts +0 -36
  167. package/lib/detectors/CpuPerformanceDetector.d.ts.map +0 -1
  168. package/lib/detectors/CpuPerformanceDetector.js +0 -71
  169. package/lib/detectors/Detector.d.ts +0 -9
  170. package/lib/detectors/Detector.d.ts.map +0 -1
  171. package/lib/detectors/Detector.js +0 -2
  172. package/lib/detectors/LayersChanged.d.ts +0 -37
  173. package/lib/detectors/LayersChanged.d.ts.map +0 -1
  174. package/lib/detectors/LayersChanged.js +0 -75
  175. package/lib/detectors/LongPcConnectionEstablishment.d.ts +0 -33
  176. package/lib/detectors/LongPcConnectionEstablishment.d.ts.map +0 -1
  177. package/lib/detectors/LongPcConnectionEstablishment.js +0 -72
  178. package/lib/detectors/StuckedInboundTrack.d.ts +0 -42
  179. package/lib/detectors/StuckedInboundTrack.d.ts.map +0 -1
  180. package/lib/detectors/StuckedInboundTrack.js +0 -81
  181. package/lib/detectors/StuckedOutboundTrack.d.ts +0 -42
  182. package/lib/detectors/StuckedOutboundTrack.d.ts.map +0 -1
  183. package/lib/detectors/StuckedOutboundTrack.js +0 -81
  184. package/lib/detectors/VideoFreezesDetector.d.ts +0 -40
  185. package/lib/detectors/VideoFreezesDetector.d.ts.map +0 -1
  186. package/lib/detectors/VideoFreezesDetector.js +0 -87
  187. package/lib/entries/InboundTrackStats.d.ts +0 -28
  188. package/lib/entries/InboundTrackStats.d.ts.map +0 -1
  189. package/lib/entries/InboundTrackStats.js +0 -103
  190. package/lib/entries/OutboundTrackStats.d.ts +0 -27
  191. package/lib/entries/OutboundTrackStats.d.ts.map +0 -1
  192. package/lib/entries/OutboundTrackStats.js +0 -76
  193. package/lib/entries/PeerConnectionEntryManifest.d.ts +0 -145
  194. package/lib/entries/PeerConnectionEntryManifest.d.ts.map +0 -1
  195. package/lib/entries/PeerConnectionEntryManifest.js +0 -1036
  196. package/lib/entries/StatsEntryInterfaces.d.ts +0 -350
  197. package/lib/entries/StatsEntryInterfaces.d.ts.map +0 -1
  198. package/lib/entries/StatsEntryInterfaces.js +0 -2
  199. package/lib/entries/StatsStorage.d.ts +0 -159
  200. package/lib/entries/StatsStorage.d.ts.map +0 -1
  201. package/lib/entries/StatsStorage.js +0 -332
  202. package/lib/entries/UpdateFields.d.ts.map +0 -1
  203. package/lib/entries/UpdateFields.js +0 -36
  204. package/lib/index.d.ts +0 -32
  205. package/lib/index.d.ts.map +0 -1
  206. package/lib/index.js +0 -66
  207. package/lib/schema/Samples.d.ts +0 -367
  208. package/lib/schema/Samples.d.ts.map +0 -1
  209. package/lib/schema/Samples.js +0 -4
  210. package/lib/schema/W3cStatsIdentifiers.d.ts +0 -362
  211. package/lib/schema/W3cStatsIdentifiers.d.ts.map +0 -1
  212. package/lib/schema/W3cStatsIdentifiers.js +0 -6
  213. package/lib/utils/CallEventType.d.ts +0 -22
  214. package/lib/utils/CallEventType.d.ts.map +0 -1
  215. package/lib/utils/CallEventType.js +0 -25
  216. package/lib/utils/IndexedMap.d.ts +0 -26
  217. package/lib/utils/IndexedMap.d.ts.map +0 -1
  218. package/lib/utils/IndexedMap.js +0 -219
  219. package/lib/utils/Processor.d.ts +0 -7
  220. package/lib/utils/Processor.d.ts.map +0 -1
  221. package/lib/utils/Processor.js +0 -38
  222. package/lib/utils/Stats.d.ts +0 -60
  223. package/lib/utils/Stats.d.ts.map +0 -1
  224. package/lib/utils/Stats.js +0 -204
  225. package/lib/utils/TypedEmitter.d.ts +0 -14
  226. package/lib/utils/TypedEmitter.d.ts.map +0 -1
  227. package/lib/utils/TypedEmitter.js +0 -38
  228. package/lib/utils/callEvents.d.ts +0 -26
  229. package/lib/utils/callEvents.d.ts.map +0 -1
  230. package/lib/utils/callEvents.js +0 -167
  231. package/lib/utils/common.d.ts.map +0 -1
  232. package/lib/utils/common.js +0 -14
  233. package/lib/utils/logger.d.ts +0 -27
  234. package/lib/utils/logger.d.ts.map +0 -1
  235. package/lib/utils/logger.js +0 -89
  236. package/lib/utils/validators.d.ts.map +0 -1
  237. package/lib/utils/validators.js +0 -24
package/README.md CHANGED
@@ -1,877 +1,2031 @@
1
- ## Javascript library to monitor WebRTC applications
2
-
3
- @observertc/client-monitor-js is a client side library to monitor [WebRTCStats](https://www.w3.org/TR/webrtc-stats/) and to integrate your app to observertc components.
4
-
5
- Table of Contents:
6
-
7
- - [Quick Start](#quick-start)
8
- - [Integrations](#integrations)
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)
16
- - [Detectors](#detectors)
17
- - [Congestion Detector](#congestion-detector)
18
- - [Audio Desync Detector](#audio-desync-detector)
19
- - [CPU Performance Detector](#cpu-performance-detector)
20
- - [Video Freeze Detector](#video-freeze-detector)
21
- - [Stucked Inbound Track Detector](#stucked-inbound-track-detector)
22
- - [Issues](#issues)
23
- - [Configurations](#configurations)
24
- - [Events](#events)
25
- - [CLIENT_JOINED Event](#client_joined-event)
26
- - [CLIENT_LEFT Event](#client_left-event)
27
- - [Custom Call Event](#custom-call-event)
28
- - [Extension Stats Event](#extension-stats-event)
29
- - [Sampling](#sampling)
30
- - [NPM package](#npm-package)
31
- - [API docs](#api-docs)
32
- - [Schemas](#schemas)
33
- - [Getting Involved](#getting-involved)
34
- - [License](#license)
1
+ # @observertc/client-monitor-js
2
+
3
+ **JavaScript library to monitor WebRTC applications**
4
+
5
+ @observertc/client-monitor-js is a client-side library to monitor [WebRTCStats](https://www.w3.org/TR/webrtc-stats/) and integrate your app with ObserveRTC components.
6
+
7
+ [![npm version](https://badge.fury.io/js/@observertc%2Fclient-monitor-js.svg)](https://badge.fury.io/js/@observertc%2Fclient-monitor-js)
8
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Installation](#installation)
13
+ 2. [Quick Start](#quick-start)
14
+ 3. [Integrations](#integrations)
15
+ 4. [Configuration](#configuration)
16
+ 5. [ClientMonitor](#clientmonitor)
17
+ 6. [Detectors](#detectors)
18
+ 7. [Score Calculation](#score-calculation)
19
+ 8. [Collecting and Adapting Stats](#collecting-and-adapting-stats)
20
+ 9. [Sampling](#sampling)
21
+ 10. [Events and Issues](#events-and-issues)
22
+ 11. [WebRTC Stats Monitors](#webrtc-stats-monitors)
23
+ 12. [Stats Adapters](#stats-adapters)
24
+ 13. [Derived Metrics](#derived-metrics)
25
+ 14. [Schema Reference](#schema-reference)
26
+ 15. [Examples](#examples)
27
+ 16. [Troubleshooting](#troubleshooting)
28
+ 17. [API Reference](#api-reference)
29
+ 18. [FAQ](#faq)
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install @observertc/client-monitor-js
35
+ ```
36
+
37
+ or
38
+
39
+ ```bash
40
+ yarn add @observertc/client-monitor-js
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```javascript
46
+ import { ClientMonitor } from "@observertc/client-monitor-js";
47
+
48
+ // Create a monitor with default configuration
49
+ const monitor = new ClientMonitor({
50
+ clientId: "my-client-id",
51
+ callId: "my-call-id",
52
+ collectingPeriodInMs: 2000,
53
+ samplingPeriodInMs: 4000,
54
+ });
55
+
56
+ // Add a peer connection to monitor
57
+ monitor.addSource(peerConnection);
58
+
59
+ // Listen for samples
60
+ monitor.on("sample-created", (sample) => {
61
+ console.log("Sample created:", sample);
62
+ // Send sample to your analytics backend
63
+ });
64
+
65
+ // Listen for issues
66
+ monitor.on("issue", (issue) => {
67
+ console.log("Issue detected:", issue);
68
+ });
69
+
70
+ // Close when done
71
+ monitor.close();
72
+ ```
73
+
74
+ ## Integrations
75
+
76
+ ### RTCPeerConnection Integration
77
+
78
+ Direct integration with native WebRTC PeerConnections:
79
+
80
+ ```javascript
81
+ import { ClientMonitor } from "@observertc/client-monitor-js";
82
+
83
+ const peerConnection = new RTCPeerConnection();
84
+ const monitor = new ClientMonitor();
85
+
86
+ // Add the peer connection for monitoring
87
+ monitor.addSource(peerConnection);
88
+ ```
89
+
90
+ ### Mediasoup Integration
91
+
92
+ ```javascript
93
+ import { ClientMonitor } from "@observertc/client-monitor-js";
94
+ import mediasoup from "mediasoup-client";
95
+
96
+ const device = new mediasoup.Device();
97
+ const monitor = new ClientMonitor();
98
+
99
+ // Monitor the mediasoup device
100
+ monitor.addSource(device);
101
+
102
+ // The monitor will automatically detect new transports created after adding the device
103
+ const transport = device.createSendTransport(/* ... */);
104
+
105
+ // For transports created before adding the device, add them manually:
106
+ monitor.addSource(transport);
107
+ ```
108
+
109
+ **Important**: When adding a mediasoup device, the monitor automatically hooks into the `newtransport` event to detect newly created transports. However, transports created before adding the device must be added manually.
110
+
111
+ ### Logger Integration
112
+
113
+ Customize logging behavior by providing your own logger:
114
+
115
+ ```javascript
116
+ import { setLogger, Logger } from "@observertc/client-monitor-js";
117
+
118
+ const customLogger: Logger = {
119
+ trace: (...args) => console.trace(...args),
120
+ debug: (...args) => console.debug(...args),
121
+ info: (...args) => console.info(...args),
122
+ warn: (...args) => console.warn(...args),
123
+ error: (...args) => console.error(...args),
124
+ };
125
+
126
+ setLogger(customLogger);
127
+ ```
128
+
129
+ ## Configuration
130
+
131
+ The `ClientMonitor` accepts a comprehensive configuration object. All configuration options are optional except when specifically noted:
132
+
133
+ ```javascript
134
+ import { ClientMonitor } from "@observertc/client-monitor-js";
135
+
136
+ const monitor = new ClientMonitor({
137
+ // Basic configuration (all optional)
138
+ clientId: "unique-client-id",
139
+ callId: "unique-call-id",
140
+ collectingPeriodInMs: 2000, // Default: 2000ms
141
+ samplingPeriodInMs: 4000, // Optional, no default
142
+
143
+ // Integration settings (optional with defaults)
144
+ integrateNavigatorMediaDevices: true, // Default: true
145
+ addClientJointEventOnCreated: true, // Default: true
146
+ addClientLeftEventOnClose: true, // Default: true
147
+ bufferingEventsForSamples: false, // Default: false
148
+
149
+ // Detector configurations (all optional with defaults)
150
+ audioDesyncDetector: {
151
+ disabled: false,
152
+ createIssue: true,
153
+ fractionalCorrectionAlertOnThreshold: 0.1,
154
+ fractionalCorrectionAlertOffThreshold: 0.05,
155
+ },
156
+
157
+ congestionDetector: {
158
+ disabled: false,
159
+ createIssue: true,
160
+ sensitivity: "medium", // 'low', 'medium', 'high'
161
+ },
162
+
163
+ cpuPerformanceDetector: {
164
+ disabled: false,
165
+ createIssue: true,
166
+ fpsVolatilityThresholds: {
167
+ lowWatermark: 0.1,
168
+ highWatermark: 0.3,
169
+ },
170
+ durationOfCollectingStatsThreshold: {
171
+ lowWatermark: 5000,
172
+ highWatermark: 10000,
173
+ },
174
+ },
175
+
176
+ dryInboundTrackDetector: {
177
+ disabled: false,
178
+ createIssue: true,
179
+ thresholdInMs: 5000,
180
+ },
181
+
182
+ dryOutboundTrackDetector: {
183
+ disabled: false,
184
+ createIssue: true,
185
+ thresholdInMs: 5000,
186
+ },
187
+
188
+ videoFreezesDetector: {
189
+ disabled: false,
190
+ createIssue: true,
191
+ },
192
+
193
+ playoutDiscrepancyDetector: {
194
+ disabled: false,
195
+ createIssue: true,
196
+ lowSkewThreshold: 2,
197
+ highSkewThreshold: 5,
198
+ },
199
+
200
+ syntheticSamplesDetector: {
201
+ disabled: false,
202
+ createIssue: true,
203
+ minSynthesizedSamplesDuration: 1000,
204
+ },
205
+
206
+ longPcConnectionEstablishmentDetector: {
207
+ disabled: false,
208
+ createIssue: true,
209
+ thresholdInMs: 5000,
210
+ },
211
+
212
+ // Application data (optional)
213
+ appData: {
214
+ userId: "user-123",
215
+ roomId: "room-456",
216
+ },
217
+ });
218
+ ```
219
+
220
+ **Important**: You can create a monitor with minimal configuration or even no configuration at all:
221
+
222
+ ```javascript
223
+ // Minimal configuration
224
+ const monitor = new ClientMonitor({
225
+ clientId: "my-client",
226
+ collectingPeriodInMs: 1000,
227
+ });
228
+
229
+ // No configuration (uses all defaults)
230
+ const monitor = new ClientMonitor();
231
+ ```
232
+
233
+ ## ClientMonitor
234
+
235
+ The `ClientMonitor` is the main class that orchestrates WebRTC monitoring, statistics collection, and anomaly detection.
236
+
237
+ ### Core Features
238
+
239
+ - **Multi-source monitoring**: Supports RTCPeerConnection, mediasoup devices and transports
240
+ - **Automatic stats collection**: Periodically collects WebRTC statistics
241
+ - **Real-time anomaly detection**: Built-in detectors for common issues
242
+ - **Performance scoring**: Calculates quality scores for connections and tracks
243
+ - **Event generation**: Emits events for WebRTC state changes and issues
244
+ - **Sampling**: Creates periodic snapshots of the client state
245
+
246
+ ### Public Methods
247
+
248
+ #### Core Methods
249
+
250
+ - **`addSource(source: RTCPeerConnection | MediasoupDevice | MediasoupTransport)`**: Adds a source for monitoring
251
+ - **`close()`**: Closes the monitor and stops all monitoring activities
252
+ - **`collect()`**: Manually collects stats from all monitored sources
253
+ - **`createSample()`**: Creates a client sample with current state
254
+
255
+ #### Configuration Methods
256
+
257
+ - **`setCollectingPeriod(periodInMs: number)`**: Updates the stats collection interval
258
+ - **`setSamplingPeriod(periodInMs: number)`**: Updates the sampling interval
259
+ - **`setScore(score: number, reasons?: Record<string, number>)`**: Manually sets the client score
260
+
261
+ #### Event Methods
262
+
263
+ - **`addEvent(event: ClientEvent)`**: Adds a custom client event
264
+ - **`addIssue(issue: ClientIssue)`**: Adds a custom client issue
265
+ - **`addMetaData(metaData: ClientMetaData)`**: Adds metadata
266
+ - **`addExtensionStats(stats: ExtensionStat)`**: Adds custom extension stats
267
+
268
+ #### Utility Methods
269
+
270
+ - **`getTrackMonitor(trackId: string)`**: Retrieves a track monitor by ID
271
+ - **`watchMediaDevices()`**: Integrates with navigator.mediaDevices
272
+ - **`fetchUserAgentData()`**: Fetches browser user agent information
273
+
274
+ ### Properties
275
+
276
+ - **`score`**: Current client performance score (0.0-5.0)
277
+ - **`scoreReasons`**: Detailed score calculation reasons
278
+ - **`closed`**: Whether the monitor is closed
279
+ - **`config`**: Current configuration
280
+ - **`detectors`**: Detector management instance
281
+ - **`peerConnections`**: Array of monitored peer connections
282
+ - **`tracks`**: Array of monitored tracks
283
+
284
+ ## Detectors
285
+
286
+ Detectors are specialized components that monitor for specific anomalies and issues in WebRTC connections. Each detector focuses on a particular aspect of the connection quality.
287
+
288
+ ### Built-in Detectors
289
+
290
+ #### AudioDesyncDetector
291
+
292
+ Detects audio synchronization issues by monitoring sample corrections.
293
+
294
+ **Triggers on:**
295
+
296
+ - Audio acceleration/deceleration corrections exceed thresholds
297
+ - Indicates audio-video sync problems
298
+
299
+ **Configuration:**
300
+
301
+ ```javascript
302
+ audioDesyncDetector: {
303
+ disabled: false,
304
+ createIssue: true,
305
+ fractionalCorrectionAlertOnThreshold: 0.1, // 10% correction rate triggers alert
306
+ fractionalCorrectionAlertOffThreshold: 0.05, // 5% correction rate clears alert
307
+ }
308
+ ```
309
+
310
+ #### CongestionDetector
311
+
312
+ Monitors network congestion by analyzing available bandwidth vs. usage.
313
+
314
+ **Triggers on:**
315
+
316
+ - Available bandwidth falls below sending/receiving bitrates
317
+ - Network congestion conditions
318
+
319
+ **Configuration:**
320
+
321
+ ```javascript
322
+ congestionDetector: {
323
+ disabled: false,
324
+ createIssue: true,
325
+ sensitivity: 'medium', // 'low', 'medium', 'high'
326
+ }
327
+ ```
328
+
329
+ #### CpuPerformanceDetector
330
+
331
+ Detects CPU performance issues affecting media processing.
332
+
333
+ **Triggers on:**
334
+
335
+ - FPS volatility exceeds thresholds
336
+ - Stats collection takes too long (indicating CPU stress)
337
+
338
+ **Configuration:**
339
+
340
+ ```javascript
341
+ cpuPerformanceDetector: {
342
+ disabled: false,
343
+ createIssue: true,
344
+ fpsVolatilityThresholds: {
345
+ lowWatermark: 0.1,
346
+ highWatermark: 0.3,
347
+ },
348
+ durationOfCollectingStatsThreshold: {
349
+ lowWatermark: 5000,
350
+ highWatermark: 10000,
351
+ },
352
+ }
353
+ ```
354
+
355
+ #### DryInboundTrackDetector
356
+
357
+ Detects inbound tracks that stop receiving data.
358
+
359
+ **Triggers on:**
360
+
361
+ - Inbound track receives no data for specified duration
362
+ - Track stalling or connection issues
363
+
364
+ **Configuration:**
365
+
366
+ ```javascript
367
+ dryInboundTrackDetector: {
368
+ disabled: false,
369
+ createIssue: true,
370
+ thresholdInMs: 5000,
371
+ }
372
+ ```
373
+
374
+ #### DryOutboundTrackDetector
375
+
376
+ Detects outbound tracks that stop sending data.
377
+
378
+ **Triggers on:**
379
+
380
+ - Outbound track sends no data for specified duration
381
+ - Local media issues or encoding problems
382
+
383
+ **Configuration:**
384
+
385
+ ```javascript
386
+ dryOutboundTrackDetector: {
387
+ disabled: false,
388
+ createIssue: true,
389
+ thresholdInMs: 5000,
390
+ }
391
+ ```
392
+
393
+ #### FreezedVideoTrackDetector
394
+
395
+ Detects frozen video tracks.
396
+
397
+ **Triggers on:**
398
+
399
+ - Video frames stop updating
400
+ - Video freeze conditions
401
+
402
+ **Configuration:**
403
+
404
+ ```javascript
405
+ videoFreezesDetector: {
406
+ disabled: false,
407
+ createIssue: true,
408
+ }
409
+ ```
410
+
411
+ #### PlayoutDiscrepancyDetector
412
+
413
+ Detects discrepancies between received and rendered frames.
414
+
415
+ **Triggers on:**
416
+
417
+ - Frame skew exceeds thresholds
418
+ - Video playout buffer issues
419
+
420
+ **Configuration:**
421
+
422
+ ```javascript
423
+ playoutDiscrepancyDetector: {
424
+ disabled: false,
425
+ createIssue: true,
426
+ lowSkewThreshold: 2,
427
+ highSkewThreshold: 5,
428
+ }
429
+ ```
430
+
431
+ #### SynthesizedSamplesDetector
432
+
433
+ Detects when audio playout synthesizes samples due to missing data.
434
+
435
+ **Triggers on:**
436
+
437
+ - Synthesized audio samples exceed duration threshold
438
+ - Audio gaps requiring interpolation
439
+
440
+ **Configuration:**
441
+
442
+ ```javascript
443
+ syntheticSamplesDetector: {
444
+ disabled: false,
445
+ createIssue: true,
446
+ minSynthesizedSamplesDuration: 1000,
447
+ }
448
+ ```
449
+
450
+ #### LongPcConnectionEstablishmentDetector
451
+
452
+ Detects slow peer connection establishment.
453
+
454
+ **Triggers on:**
455
+
456
+ - Peer connection takes too long to establish
457
+ - Connection setup issues
458
+
459
+ **Configuration:**
460
+
461
+ ```javascript
462
+ longPcConnectionEstablishmentDetector: {
463
+ disabled: false,
464
+ createIssue: true,
465
+ thresholdInMs: 5000,
466
+ }
467
+ ```
468
+
469
+ ### Custom Detectors
470
+
471
+ Create custom detectors by implementing the `Detector` interface:
472
+
473
+ ```javascript
474
+ import { Detector } from "@observertc/client-monitor-js";
475
+
476
+ class CustomDetector implements Detector {
477
+ public readonly name = 'custom-detector';
478
+
479
+ constructor(private monitor: any) {}
480
+
481
+ public update() {
482
+ // Custom detection logic
483
+ if (this.detectCustomCondition()) {
484
+ this.monitor.parent.emit('custom-issue', {
485
+ type: 'custom-issue',
486
+ payload: { reason: 'Custom condition detected' }
487
+ });
488
+ }
489
+ }
490
+
491
+ private detectCustomCondition(): boolean {
492
+ // Your detection logic here
493
+ return false;
494
+ }
495
+ }
496
+
497
+ // Add to monitor
498
+ const detector = new CustomDetector(someMonitor);
499
+ monitor.detectors.add(detector);
500
+
501
+ // Remove detector
502
+ monitor.detectors.remove(detector);
503
+ ```
504
+
505
+ ## Score Calculation
506
+
507
+ The scoring system provides quantitative quality assessment ranging from 0.0 (worst) to 5.0 (best). The library includes a `DefaultScoreCalculator` implementation and allows custom score calculators via the `ScoreCalculator` interface.
508
+
509
+ ### ScoreCalculator Interface
510
+
511
+ ```typescript
512
+ interface ScoreCalculator {
513
+ update(): void;
514
+ encodeClientScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
515
+ encodePeerConnectionScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
516
+ encodeInboundAudioScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
517
+ encodeInboundVideoScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
518
+ encodeOutboundAudioScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
519
+ encodeOutboundVideoScoreReasons?<T extends Record<string, number>>(reasons?: T): string;
520
+ }
521
+ ```
522
+
523
+ ### DefaultScoreCalculator Implementation
524
+
525
+ The default implementation calculates scores using a hierarchical weighted average approach:
526
+
527
+ #### Score Hierarchy
528
+
529
+ The client score is calculated as a weighted average of:
530
+
531
+ 1. **Peer Connection Stability Scores** (based on RTT and packet loss)
532
+ 2. **Track Quality Scores** (inbound/outbound audio/video tracks)
533
+
534
+ #### Client Score Calculation
535
+
536
+ ```
537
+ Client Score = Σ(PC_Score × PC_Weight) / Σ(PC_Weight)
538
+
539
+ Where PC_Score = Track_Score_Avg × PC_Stability_Score
540
+ ```
541
+
542
+ #### Peer Connection Stability Score
543
+
544
+ Based on Round Trip Time (RTT) and packet loss:
545
+
546
+ **RTT Penalties:**
547
+
548
+ - High RTT (150-300ms): -1.0 point
549
+ - Very High RTT (>300ms): -2.0 points
550
+
551
+ **Packet Loss Penalties:**
552
+
553
+ - 1-5% loss: -1.0 point
554
+ - 5-20% loss: -2.0 points
555
+ - > 20% loss: -5.0 points
556
+
557
+ #### Track Score Calculations
558
+
559
+ **Inbound Audio Track Score:**
560
+
561
+ - Based on normalized bitrate and packet loss
562
+ - Uses logarithmic bitrate normalization
563
+ - Exponential decay for packet loss impact
564
+
565
+ ```javascript
566
+ normalizedBitrate = log10(max(bitrate, MIN_AUDIO_BITRATE) / MIN_AUDIO_BITRATE) / NORMALIZATION_FACTOR;
567
+ lossPenalty = exp(-packetLoss / 2);
568
+ score = min(MAX_SCORE, 5 * normalizedBitrate * lossPenalty);
569
+ ```
570
+
571
+ **Inbound Video Track Score:**
572
+
573
+ - FPS volatility penalties
574
+ - Dropped frames penalties
575
+ - Frame corruption penalties
576
+
577
+ **Outbound Audio Track Score:**
578
+
579
+ - Similar to inbound, using sending bitrate
580
+ - Remote packet loss consideration
581
+
582
+ **Outbound Video Track Score:**
583
+
584
+ - Bitrate deviation from target penalties
585
+ - CPU limitation penalties
586
+ - Bitrate volatility penalties
587
+
588
+ ### Score Reasons
589
+
590
+ Each score calculation includes detailed reasons for penalties:
591
+
592
+ ```javascript
593
+ monitor.on("score", (event) => {
594
+ console.log("Client Score:", event.clientScore);
595
+ console.log("Score Reasons:", event.scoreReasons);
596
+ // Example reasons:
597
+ // {
598
+ // "high-rtt": 1.0,
599
+ // "high-packetloss": 2.0,
600
+ // "cpu-limitation": 2.0,
601
+ // "dropped-video-frames": 1.0
602
+ // }
603
+ });
604
+ ```
605
+
606
+ ### Custom Score Calculator
607
+
608
+ Implement your own scoring logic by implementing the `ScoreCalculator` interface:
609
+
610
+ ```javascript
611
+ import { ScoreCalculator } from "@observertc/client-monitor-js";
612
+
613
+ class CustomScoreCalculator {
614
+ constructor(clientMonitor) {
615
+ this.clientMonitor = clientMonitor;
616
+ }
617
+
618
+ update() {
619
+ // Calculate peer connection scores
620
+ for (const pcMonitor of this.clientMonitor.peerConnections) {
621
+ this.calculatePeerConnectionScore(pcMonitor);
622
+ }
623
+
624
+ // Calculate track scores
625
+ for (const track of this.clientMonitor.tracks) {
626
+ this.calculateTrackScore(track);
627
+ }
628
+
629
+ // Calculate final client score
630
+ this.calculateClientScore();
631
+ }
632
+
633
+ calculatePeerConnectionScore(pcMonitor) {
634
+ const rttMs = (pcMonitor.avgRttInSec ?? 0) * 1000;
635
+ const fractionLost = pcMonitor.inboundRtps.reduce((acc, rtp) => acc + (rtp.fractionLost ?? 0), 0);
636
+
637
+ let score = 5.0;
638
+ const reasons = {};
639
+
640
+ // Custom RTT penalties
641
+ if (rttMs > 200) {
642
+ score -= 1.5;
643
+ reasons["custom-high-rtt"] = 1.5;
644
+ }
645
+
646
+ // Custom packet loss penalties
647
+ if (fractionLost > 0.02) {
648
+ score -= 2.0;
649
+ reasons["custom-packet-loss"] = 2.0;
650
+ }
651
+
652
+ pcMonitor.calculatedStabilityScore.value = Math.max(0, score);
653
+ pcMonitor.calculatedStabilityScore.reasons = reasons;
654
+ }
655
+
656
+ calculateTrackScore(trackMonitor) {
657
+ let score = 5.0;
658
+ const reasons = {};
659
+
660
+ if (trackMonitor.direction === "inbound" && trackMonitor.kind === "video") {
661
+ // Custom video quality scoring
662
+ const fps = trackMonitor.ewmaFps ?? 0;
663
+ if (fps < 15) {
664
+ score -= 2.0;
665
+ reasons["low-fps"] = 2.0;
666
+ }
667
+ }
668
+
669
+ trackMonitor.calculatedScore.value = Math.max(0, score);
670
+ trackMonitor.calculatedScore.reasons = reasons;
671
+ }
672
+
673
+ calculateClientScore() {
674
+ let totalScore = 0;
675
+ let totalWeight = 0;
676
+ const combinedReasons = {};
677
+
678
+ for (const pcMonitor of this.clientMonitor.peerConnections) {
679
+ if (pcMonitor.calculatedStabilityScore.value !== undefined) {
680
+ totalScore += pcMonitor.calculatedStabilityScore.value;
681
+ totalWeight += 1;
682
+
683
+ // Combine reasons
684
+ Object.assign(combinedReasons, pcMonitor.calculatedStabilityScore.reasons || {});
685
+ }
686
+ }
687
+
688
+ const clientScore = totalWeight > 0 ? totalScore / totalWeight : 5.0;
689
+ this.clientMonitor.setScore(clientScore, combinedReasons);
690
+ }
691
+
692
+ // Optional: Custom encoding for reasons
693
+ encodeClientScoreReasons(reasons) {
694
+ return JSON.stringify(reasons || {});
695
+ }
696
+ }
697
+
698
+ // Apply custom calculator
699
+ const monitor = new ClientMonitor();
700
+ monitor.scoreCalculator = new CustomScoreCalculator(monitor);
701
+ ```
702
+
703
+ ## Collecting and Adapting Stats
704
+
705
+ The monitor collects WebRTC statistics periodically and adapts them for consistent processing across different browsers and integrations.
706
+
707
+ ### Stats Collection Process
708
+
709
+ 1. **Collection Trigger**: Timer-based collection every `collectingPeriodInMs`
710
+ 2. **Raw Stats Retrieval**: Calls `getStats()` on peer connections
711
+ 3. **Stats Adaptation**: Applies browser-specific adaptations
712
+ 4. **Monitor Updates**: Updates all relevant monitor objects
713
+ 5. **Detector Updates**: Runs all attached detectors
714
+ 6. **Score Calculation**: Updates performance scores
715
+
716
+ ### Stats Adapters
717
+
718
+ Stats adapters handle browser-specific differences and integration requirements:
719
+
720
+ #### Browser Adaptations
721
+
722
+ - **Firefox**: Handles track identifier format differences
723
+ - **Chrome/Safari**: Handles various stats format variations
724
+ - **Mediasoup**: Filters probator tracks and adapts mediasoup-specific stats
725
+
726
+ #### Custom Stats Adapters
727
+
728
+ Add custom adaptation logic:
729
+
730
+ ```javascript
731
+ monitor.statsAdapters.add((stats) => {
732
+ // Custom adaptation logic
733
+ return stats.map((stat) => {
734
+ if (stat.type === "inbound-rtp" && stat.trackIdentifier) {
735
+ // Custom track identifier handling
736
+ stat.trackIdentifier = stat.trackIdentifier.replace(/[{}]/g, "");
737
+ }
738
+ return stat;
739
+ });
740
+ });
741
+ ```
742
+
743
+ ### Available WebRTC Stats
744
+
745
+ The monitor collects and processes all standard WebRTC statistics:
746
+
747
+ #### RTP Statistics
748
+
749
+ - **Inbound RTP**: Receiving stream statistics
750
+ - **Outbound RTP**: Sending stream statistics
751
+ - **Remote Inbound RTP**: Remote peer's receiving statistics
752
+ - **Remote Outbound RTP**: Remote peer's sending statistics
753
+
754
+ #### Connection Statistics
755
+
756
+ - **ICE Candidate**: ICE candidate information
757
+ - **ICE Candidate Pair**: ICE candidate pair statistics
758
+ - **ICE Transport**: ICE transport layer statistics
759
+ - **Certificate**: Security certificate information
760
+
761
+ #### Media Statistics
762
+
763
+ - **Codec**: Codec configuration and usage
764
+ - **Media Source**: Local media source statistics
765
+ - **Media Playout**: Audio playout statistics
766
+ - **Data Channel**: Data channel statistics
767
+
768
+ ## Sampling
769
+
770
+ Sampling creates periodic snapshots (`ClientSample`) containing the complete state of the monitored client.
771
+
772
+ ### Sample Structure
773
+
774
+ A `ClientSample` includes:
775
+
776
+ - **Client metadata**: clientId, callId, timestamp, score
777
+ - **Peer connection samples**: All monitored peer connections
778
+ - **Events**: Client events since last sample
779
+ - **Issues**: Detected issues since last sample
780
+ - **Extension stats**: Custom application statistics
781
+
782
+ ### Automatic Sampling
783
+
784
+ Enable automatic sampling by setting `samplingPeriodInMs`:
785
+
786
+ ```javascript
787
+ const monitor = new ClientMonitor({
788
+ collectingPeriodInMs: 2000,
789
+ samplingPeriodInMs: 4000, // Create sample every 4 seconds
790
+ });
791
+
792
+ monitor.on("sample-created", (sample) => {
793
+ console.log("Sample created:", sample);
794
+ // Send to analytics backend
795
+ sendToAnalytics(sample);
796
+ });
797
+ ```
798
+
799
+ ### Manual Sampling
800
+
801
+ Create samples on demand:
802
+
803
+ ```javascript
804
+ const monitor = new ClientMonitor({
805
+ collectingPeriodInMs: 2000,
806
+ bufferingEventsForSamples: true, // Required for manual sampling
807
+ });
808
+
809
+ // Create sample manually
810
+ const sample = monitor.createSample();
811
+ if (sample) {
812
+ console.log("Manual sample:", sample);
813
+ }
814
+ ```
815
+
816
+ ### Sample Compression
817
+
818
+ For efficient data transmission and storage, ObserveRTC provides dedicated compression packages for `ClientSample` objects:
819
+
820
+ **@observertc/samples-encoder** - Compresses ClientSample objects for transmission:
821
+
822
+ ```javascript
823
+ import { SamplesEncoder } from "@observertc/samples-encoder";
824
+
825
+ const encoder = new SamplesEncoder();
826
+ const sample = monitor.createSample();
827
+
828
+ // Encode the sample for efficient transmission
829
+ const encodedSample = encoder.encode(sample);
830
+
831
+ // Send compressed data over the network
832
+ fetch("/api/samples", {
833
+ method: "POST",
834
+ headers: {
835
+ "Content-Type": "application/octet-stream",
836
+ },
837
+ body: encodedSample,
838
+ });
839
+ ```
840
+
841
+ **@observertc/samples-decoder** - Decompresses received ClientSample objects:
842
+
843
+ ```javascript
844
+ import { SamplesDecoder } from "@observertc/samples-decoder";
845
+
846
+ const decoder = new SamplesDecoder();
847
+
848
+ // Receive compressed sample data
849
+ const compressedData = await response.arrayBuffer();
850
+
851
+ // Decode back to ClientSample object
852
+ const decodedSample = decoder.decode(compressedData);
853
+
854
+ // Process the restored sample
855
+ console.log("Decoded sample:", decodedSample);
856
+ ```
857
+
858
+ **Benefits of Using Compression:**
859
+
860
+ - **Reduced Bandwidth**: Compressed samples require significantly less network bandwidth
861
+ - **Faster Transmission**: Smaller payloads improve upload/download times
862
+ - **Storage Efficiency**: Compressed samples consume less storage space
863
+ - **Schema Consistency**: Ensures proper serialization/deserialization of all ClientSample fields
864
+
865
+ **Installation:**
866
+
867
+ ```bash
868
+ # For encoding (client-side)
869
+ npm install @observertc/samples-encoder
870
+
871
+ # For decoding (server-side)
872
+ npm install @observertc/samples-decoder
873
+
874
+ # Both packages (if needed)
875
+ npm install @observertc/samples-encoder @observertc/samples-decoder
876
+ ```
877
+
878
+ **Integration with ObserveRTC Stack:**
879
+ These compression packages are part of the broader ObserveRTC ecosystem and are designed to work seamlessly with:
880
+
881
+ - Client Monitor (sample generation)
882
+ - Observer Service (sample processing)
883
+ - Schema definitions (data consistency)
884
+
885
+ The compression format maintains full compatibility with the ObserveRTC schema definitions and can be used with any transport mechanism (WebSocket, HTTP REST, etc.).
886
+
887
+ ## Events and Issues
888
+
889
+ The monitor generates events for WebRTC state changes and issues for detected problems.
890
+
891
+ ### Client Events
892
+
893
+ Automatically generated events include:
894
+
895
+ - **PEER_CONNECTION_OPENED**: New peer connection
896
+ - **PEER_CONNECTION_CLOSED**: Peer connection closed
897
+ - **MEDIA_TRACK_ADDED**: New media track
898
+ - **MEDIA_TRACK_REMOVED**: Media track ended
899
+ - **ICE_CANDIDATE**: ICE candidate discovered
900
+ - **NEGOTIATION_NEEDED**: SDP negotiation required
901
+
902
+ ### Client Issues
903
+
904
+ Issues are generated by detectors:
905
+
906
+ - **congestion**: Network congestion detected
907
+ - **cpu-limitation**: CPU performance issues
908
+ - **audio-desync**: Audio synchronization problems
909
+ - **video-freeze**: Video track frozen
910
+ - **dry-track**: Track not flowing data
911
+
912
+ ### Custom Events and Issues
913
+
914
+ Add custom events and issues:
915
+
916
+ ```javascript
917
+ // Custom event
918
+ monitor.addEvent({
919
+ type: "user-action",
920
+ payload: { action: "mute-audio" },
921
+ timestamp: Date.now(),
922
+ });
923
+
924
+ // Custom issue
925
+ monitor.addIssue({
926
+ type: "custom-problem",
927
+ payload: { severity: "high", description: "Custom issue detected" },
928
+ timestamp: Date.now(),
929
+ });
930
+ ```
931
+
932
+ ### Event Listeners
933
+
934
+ Listen for real-time events:
935
+
936
+ ```javascript
937
+ // Sample created
938
+ monitor.on("sample-created", (sample) => {
939
+ console.log("New sample:", sample);
940
+ });
941
+
942
+ // Issue detected
943
+ monitor.on("issue", (issue) => {
944
+ console.log("Issue:", issue.type, issue.payload);
945
+ });
946
+
947
+ // Score updated
948
+ monitor.on("score", ({ clientScore, scoreReasons }) => {
949
+ console.log("Score:", clientScore, "Reasons:", scoreReasons);
950
+ });
951
+
952
+ // Congestion detected
953
+ monitor.on("congestion", ({ peerConnectionMonitor, availableIncomingBitrate }) => {
954
+ console.log("Congestion detected on PC:", peerConnectionMonitor.peerConnectionId);
955
+ });
956
+
957
+ // Stats collected
958
+ monitor.on("stats-collected", ({ durationOfCollectingStatsInMs, collectedStats }) => {
959
+ console.log("Stats collection took:", durationOfCollectingStatsInMs, "ms");
960
+ });
961
+ ```
962
+
963
+ ## WebRTC Stats Monitors
35
964
 
36
- ## Quick Start
965
+ The monitor creates specialized monitor objects for each WebRTC statistics type, providing navigation, derived fields, and lifecycle management.
37
966
 
38
- Install it from [npm](https://www.npmjs.com/package/@observertc/client-monitor-js) package repository.
967
+ ### Monitor Hierarchy
39
968
 
40
969
  ```
41
- npm i @observertc/client-monitor-js
970
+ ClientMonitor
971
+ ├── PeerConnectionMonitor[]
972
+ │ ├── InboundRtpMonitor[]
973
+ │ ├── OutboundRtpMonitor[]
974
+ │ ├── RemoteInboundRtpMonitor[]
975
+ │ ├── RemoteOutboundRtpMonitor[]
976
+ │ ├── MediaSourceMonitor[]
977
+ │ ├── CodecMonitor[]
978
+ │ ├── IceTransportMonitor[]
979
+ │ ├── IceCandidateMonitor[]
980
+ │ ├── IceCandidatePairMonitor[]
981
+ │ ├── CertificateMonitor[]
982
+ │ ├── DataChannelMonitor[]
983
+ │ └── MediaPlayoutMonitor[]
984
+ ├── InboundTrackMonitor[]
985
+ └── OutboundTrackMonitor[]
42
986
  ```
43
987
 
44
- or
988
+ ### Track Monitors
45
989
 
46
- ```
47
- yarn add @observertc/client-monitor-js
48
- ```
990
+ #### InboundTrackMonitor
49
991
 
50
- Add `@observertc/client-monitor-js` to your WebRTC app.
992
+ Monitors incoming media tracks with attached detectors:
51
993
 
52
- ```javascript
53
- import { createClientMonitor } from "@observertc/client-monitor-js";
54
- const monitor = createClientMonitor();
55
- const collector = monitor.collectors.addRTCPeerConnection(peerConnection);
994
+ **Properties:**
56
995
 
57
- monitor.on("stats-collected", () => {
58
- console.log(`Sending audio bitrate: ${monitor.sendingAudioBitrate}`);
59
- console.log(`Sending video bitrate: ${monitor.sendingVideoBitrate}`);
60
- console.log(`Receiving audio bitrate: ${monitor.receivingAudioBitrate}`);
61
- console.log(`Receiving video bitrate: ${monitor.receivingVideoBitrate}`);
62
- });
63
- ```
996
+ - `score`: Calculated quality score
997
+ - `bitrate`: Receiving bitrate
998
+ - `jitter`: Network jitter
999
+ - `fractionLost`: Packet loss fraction
1000
+ - `dtxMode`: Discontinuous transmission mode
1001
+ - `detectors`: Attached detectors
64
1002
 
65
- The above example do as follows:
1003
+ **Detectors:**
66
1004
 
67
- 1. Create a client monitor, which collect stats every (by default in every 5s)
68
- 2. Setup a collector collect stats from a peer connection
69
- 3. Register an event called after stats are collected
1005
+ - AudioDesyncDetector (for audio tracks)
1006
+ - FreezedVideoTrackDetector (for video tracks)
1007
+ - DryInboundTrackDetector
1008
+ - PlayoutDiscrepancyDetector (for video tracks)
70
1009
 
71
- **NOTE**: `createClientMonitor()` method creates a monitor instance with default configurations.
72
- You can pass a configuration object to the `createClientMonitor(config: ClientMonitorConfig)` method to customize the monitor instance.
73
- See the full list of configurations [here](#configurations).
1010
+ #### OutboundTrackMonitor
74
1011
 
75
- ## Integrations
1012
+ Monitors outgoing media tracks:
76
1013
 
77
- ### Mediasoup
1014
+ **Properties:**
78
1015
 
79
- ```javascript
80
- import { createClientMonitor } from "@observertc/client-monitor-js";
81
- import mediasoup from "mediaousp-client";
1016
+ - `score`: Calculated quality score
1017
+ - `bitrate`: Aggregate sending bitrate
1018
+ - `sendingPacketRate`: Packet sending rate
1019
+ - `remoteReceivedPacketRate`: Remote receiving rate
1020
+ - `detectors`: Attached detectors
82
1021
 
83
- const mediasoupDevice = new mediasoup.Device();
84
- const monitor = createClientMonitor();
85
- const collector = monitor.collectors.addMediasoupDevice(mediasoupDevice);
1022
+ **Methods:**
86
1023
 
87
- monitor.on("stats-collected", () => {
88
- console.log(`Sending audio bitrate: ${monitor.sendingAudioBitrate}`);
89
- console.log(`Sending video bitrate: ${monitor.sendingVideoBitrate}`);
90
- console.log(`Receiving audio bitrate: ${monitor.receivingAudioBitrate}`);
91
- console.log(`Receiving video bitrate: ${monitor.receivingVideoBitrate}`);
92
- });
93
- ```
1024
+ - `getHighestLayer()`: Gets highest bitrate layer
1025
+ - `getOutboundRtps()`: Gets all outbound RTP monitors
94
1026
 
95
- **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:
1027
+ ### RTP Monitors
96
1028
 
97
- ```javascript
98
- const myTransport = collector.addTransport(myTransport); // your transport created before the device is added to the monitor
99
- ```
1029
+ #### InboundRtpMonitor
100
1030
 
101
- ## Collected Metrics
1031
+ Extended inbound RTP statistics with derived fields:
102
1032
 
103
- 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.
104
- 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.
1033
+ **Derived Fields:**
105
1034
 
106
- ### Calculated Updates
1035
+ - `bitrate`: Calculated receiving bitrate
1036
+ - `packetRate`: Packet receiving rate
1037
+ - `deltaPacketsLost`: Packets lost since last collection
1038
+ - `deltaJitterBufferDelay`: Jitter buffer delay change
1039
+ - `ewmaFps`: Exponentially weighted moving average FPS
107
1040
 
108
- 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.
1041
+ #### OutboundRtpMonitor
109
1042
 
110
- For example by accessing `storage` you can get the following calculated updates:
1043
+ Extended outbound RTP statistics:
111
1044
 
112
- ```javascript
113
- monitor.on("stats-collected", () => {
114
- const {
115
- totalInboundPacketsLost,
116
- totalInboundPacketsReceived,
117
- totalOutboundPacketsLost,
118
- totalOutboundPacketsReceived,
119
- totalOutboundPacketsSent,
120
- totalSentAudioBytes,
121
- totalSentVideoBytes,
122
- totalReceivedAudioBytes,
123
- totalReceivedVideoBytes,
124
- totalDataChannelBytesSent,
125
- totalDataChannelBytesReceived,
126
-
127
- deltaInboundPacketsLost,
128
- deltaInboundPacketsReceived,
129
- deltaOutboundPacketsLost,
130
- deltaOutboundPacketsReceived,
131
- deltaOutboundPacketsSent,
132
- deltaSentAudioBytes,
133
- deltaSentVideoBytes,
134
- deltaReceivedAudioBytes,
135
- deltaReceivedVideoBytes,
136
- deltaDataChannelBytesSent,
137
- deltaDataChannelBytesReceived,
138
-
139
- avgRttInS,
140
- sendingAudioBitrate,
141
- sendingVideoBitrate,
142
- receivingAudioBitrate,
143
- receivingVideoBitrate,
144
-
145
- highestSeenSendingBitrate,
146
- highestSeenReceivingBitrate,
147
- highestSeenAvailableOutgoingBitrate,
148
- highestSeenAvailableIncomingBitrate,
149
- } = monitor.storage;
150
-
151
- console.log(`Total inbound packets lost: ${totalInboundPacketsLost}`);
152
- console.log(`Total inbound packets received: ${totalInboundPacketsReceived}`);
153
- console.log(`Total outbound packets lost: ${totalOutboundPacketsLost}`);
154
- console.log(`Total outbound packets received: ${totalOutboundPacketsReceived}`);
155
- console.log(`Total outbound packets sent: ${totalOutboundPacketsSent}`);
156
- console.log(`Total sent audio bytes: ${totalSentAudioBytes}`);
157
- console.log(`Total sent video bytes: ${totalSentVideoBytes}`);
158
- console.log(`Total received audio bytes: ${totalReceivedAudioBytes}`);
159
- console.log(`Total received video bytes: ${totalReceivedVideoBytes}`);
160
- console.log(`Total data channel bytes sent: ${totalDataChannelBytesSent}`);
161
- console.log(`Total data channel bytes received: ${totalDataChannelBytesReceived}`);
162
-
163
- console.log(`Lost Inbound packets since last collecting: ${deltaInboundPacketsLost}`);
164
- console.log(`Received Inbound packets since last collecting: ${deltaInboundPacketsReceived}`);
165
- console.log(`Lost Outbound packets since last collecting: ${deltaOutboundPacketsLost}`);
166
- console.log(`Received Outbound packets since last collecting: ${deltaOutboundPacketsReceived}`);
167
- console.log(`Sent Outbound packets since last collecting: ${deltaOutboundPacketsSent}`);
168
- console.log(`Sent audio bytes since last collecting: ${deltaSentAudioBytes}`);
169
- console.log(`Sent video bytes since last collecting: ${deltaSentVideoBytes}`);
170
- console.log(`Received audio bytes since last collecting: ${deltaReceivedAudioBytes}`);
171
- console.log(`Received video bytes since last collecting: ${deltaReceivedVideoBytes}`);
172
- console.log(`Data channel bytes sent since last collecting: ${deltaDataChannelBytesSent}`);
173
- console.log(`Data channel bytes received since last collecting: ${deltaDataChannelBytesReceived}`);
174
-
175
- console.log(`Average RTT: ${avgRttInS}`);
176
- console.log(`Sending audio bitrate: ${sendingAudioBitrate}`);
177
- console.log(`Sending video bitrate: ${sendingVideoBitrate}`);
178
- console.log(`Receiving audio bitrate: ${receivingAudioBitrate}`);
179
- console.log(`Receiving video bitrate: ${receivingVideoBitrate}`);
180
-
181
- console.log(`Highest seen sending bitrate: ${highestSeenSendingBitrate}`);
182
- console.log(`Highest seen receiving bitrate: ${highestSeenReceivingBitrate}`);
183
- console.log(`Highest seen available outgoing bitrate: ${highestSeenAvailableOutgoingBitrate}`);
184
- console.log(`Highest seen available incoming bitrate: ${highestSeenAvailableIncomingBitrate}`);
185
- });
186
- ```
187
-
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
191
-
192
- **Accessing Stats**:
193
-
194
- ```javascript
195
- // from monitor
196
- monitor.peerConnections.forEach((pc) => console.log(`PeerConnection: ${pc.statsId}`, pc.stats));
197
-
198
- // or from storage
199
- [...monitor.storage.peerConnections()].forEach((pc) => console.log(`PeerConnection: ${pc.statsId}`, pc.stats));
200
- ```
201
-
202
- **Accessing Calculated Updates**:
1045
+ **Derived Fields:**
1046
+
1047
+ - `bitrate`: Calculated sending bitrate
1048
+ - `payloadBitrate`: Payload-only bitrate
1049
+ - `packetRate`: Packet sending rate
1050
+ - `retransmissionRate`: Retransmission rate
1051
+
1052
+ **Navigation:**
1053
+
1054
+ - `getRemoteInboundRtp()`: Navigate to corresponding remote stats
1055
+ - `getMediaSource()`: Navigate to media source
1056
+
1057
+ ### Connection Monitors
1058
+
1059
+ #### IceCandidatePairMonitor
1060
+
1061
+ ICE candidate pair with derived metrics:
1062
+
1063
+ **Derived Fields:**
1064
+
1065
+ - `availableIncomingBitrate`: Calculated available bandwidth
1066
+ - `availableOutgoingBitrate`: Calculated available bandwidth
1067
+
1068
+ #### IceTransportMonitor
1069
+
1070
+ ICE transport layer monitoring:
1071
+
1072
+ **Properties:**
1073
+
1074
+ - `selectedCandidatePair`: Currently selected candidate pair
1075
+ - All standard ICE transport fields
1076
+
1077
+ ### appData and attachments
1078
+
1079
+ Every monitor supports two types of additional data properties that serve different purposes:
1080
+
1081
+ **`attachments`** - Data shipped with ClientSample:
1082
+
1083
+ - Included in the `ClientSample` when `createSample()` is called
1084
+ - Sent to your analytics backend/server
1085
+ - Used for server-side processing, analysis, and correlation
1086
+ - Survives the monitoring lifecycle and becomes part of the permanent sample data
1087
+
1088
+ **`appData`** - Application-specific data (not shipped):
1089
+
1090
+ - Never included in `ClientSample` creation
1091
+ - Used exclusively for local application logic
1092
+ - Temporary data for runtime decisions and local processing
1093
+ - Does not consume bandwidth or storage in your analytics pipeline
203
1094
 
204
1095
  ```javascript
205
- monitor.on("stats-collected", () => {
206
- for (const pc of monitor.peerConnections) {
207
- console.log(
208
- `Between this and last collecting, the following stats were calculated for PeerConnection`,
209
- [
210
- `Received bytes through data channel ${pc.deltaDataChannelBytesReceived}`,
211
- `Sent bytes through data channel ${pc.deltaDataChannelBytesSent}`,
212
- `Received bytes through inbound rtps ${pc.deltaInboundPacketsReceived}`,
213
- `Sent bytes through outbound rtps ${pc.deltaOutboundPacketsSent}`,
214
- `Lost packets on inbound rtps ${pc.deltaInboundPacketsReceived}`,
215
- `Lost packets on outbound rtps ${pc.deltaOutboundPacketsSent}`,
216
- `Outbound audio bitrate${pc.sendingAudioBitrate}`,
217
- `Outbound video bitrate${pc.sendingVideoBitrate}`,
218
- `Inbound audio bitrate${pc.receivingAudioBitrate}`,
219
- `Inbound video bitrate${pc.receivingVideoBitrate}`,
220
- `Received bytes on all audio tracks ${pc.deltaReceivedAudioBytes}`,
221
- `Received bytes on all video tracks ${pc.deltaReceivedVideoBytes}`,
222
- `Sent bytes on all audio tracks ${pc.deltaSentAudioBytes}`,
223
- `Sent bytes on all video tracks ${pc.deltaSentVideoBytes}`,
224
- ].join("\n")
225
- );
226
- }
227
- });
1096
+ // Set application data (not shipped with samples)
1097
+ trackMonitor.appData = {
1098
+ userId: "user-123",
1099
+ internalTrackId: "track-abc",
1100
+ localProcessingFlags: { enableProcessing: true },
1101
+ };
1102
+
1103
+ // Set attachments (shipped with samples)
1104
+ trackMonitor.attachments = {
1105
+ roomId: "room-456",
1106
+ participantRole: "presenter",
1107
+ mediaType: "screen-share",
1108
+ customMetrics: { quality: "high" },
1109
+ };
228
1110
  ```
229
1111
 
230
- **Accessing related entries**:
1112
+ Every monitor in the hierarchy supports both properties:
231
1113
 
232
- ```javascript
233
- const pc = monitor.getPeerConnectionStats(peerConnectionId);
234
- if (pc) {
235
- [...pc.inboundRtps()].forEach((inboundRtp) => void 0);
236
- [...pc.outboundRtps()].forEach((outboundRtp) => void 0);
1114
+ - `ClientMonitor.attachments` / `ClientMonitor.appData`
1115
+ - `PeerConnectionMonitor.appData` (attachments set via tracks)
1116
+ - All track monitors: `InboundTrackMonitor`, `OutboundTrackMonitor`
1117
+ - All RTP monitors: `InboundRtpMonitor`, `OutboundRtpMonitor`, etc.
1118
+ - All connection monitors: `IceCandidatePairMonitor`, `IceTransportMonitor`, etc.
1119
+
1120
+ **Use Cases:**
1121
+
1122
+ _attachments_ for:
1123
+
1124
+ - User/session identification for server-side analysis
1125
+ - Room/conference context for grouping samples
1126
+ - A/B testing flags for performance comparison
1127
+ - Custom quality metrics for specialized analysis
1128
+
1129
+ _appData_ for:
1130
+
1131
+ - Local UI state management
1132
+ - Runtime feature toggles
1133
+ - Temporary computation results
1134
+ - Internal application routing information
237
1135
 
238
- [...pc.localCandidates()].forEach((localICECandidate) => void 0);
239
- [...pc.remoteCandidates()].forEach((remoteICECandidate) => void 0);
240
- [...pc.iceCandidatePairs()].forEach((iceCandidatePair) => void 0);
1136
+ ## Stats Adapters
241
1137
 
242
- [...pc.sctpTransports()].forEach((sctpTransport) => void 0);
243
- [...pc.transceivers()].forEach((transceiver) => void 0);
244
- [...pc.senders()].forEach((sender) => void 0);
245
- [...pc.receivers()].forEach((receiver) => void 0);
246
- [...pc.transports()].forEach((transport) => void 0);
247
- [...pc.certificates()].forEach((certificate) => void 0);
248
- [...pc.iceServers()].forEach((iceServer) => void 0);
1138
+ Stats adapters provide a powerful mechanism to customize how WebRTC statistics are processed before being consumed by monitors. They handle browser-specific differences and allow custom preprocessing logic.
1139
+
1140
+ ### Built-in Adapters
1141
+
1142
+ The library includes several built-in adapters that are automatically applied based on browser detection:
1143
+
1144
+ #### Firefox Adapters
1145
+
1146
+ - **Firefox94StatsAdapter**: Normalizes `mediaType` to `kind` field for RTP stats
1147
+ - **FirefoxTransportStatsAdapter**: Creates transport stats from ICE candidate pairs when native transport stats are missing
1148
+
1149
+ #### Browser-Specific Adaptations
1150
+
1151
+ Stats adapters are automatically added based on detected browser:
1152
+
1153
+ ```javascript
1154
+ // Automatically applied for Firefox
1155
+ if (browser.name === "firefox") {
1156
+ pcMonitor.statsAdapters.add(new Firefox94StatsAdapter());
1157
+ pcMonitor.statsAdapters.add(new FirefoxTransportStatsAdapter());
249
1158
  }
250
1159
  ```
251
1160
 
252
- ### MediaStreamTrack Entry
1161
+ ### Custom Stats Adapters
253
1162
 
254
- **Collected tracks**:
1163
+ Create custom adapters by implementing the `StatsAdapter` interface:
255
1164
 
256
1165
  ```javascript
257
- monitor.on("stats-collected", () => {
258
- for (const track of monitor.tracks) {
259
- console.log(`Track ${track.trackId} is ${track.kind}`);
1166
+ import { StatsAdapter } from "@observertc/client-monitor-js";
1167
+
1168
+ class CustomStatsAdapter {
1169
+ name = "custom-stats-adapter";
1170
+
1171
+ adapt(stats) {
1172
+ // Pre-processing: runs before monitor updates
1173
+ return stats.map((stat) => {
1174
+ if (stat.type === "inbound-rtp" && stat.trackIdentifier) {
1175
+ // Custom track identifier normalization
1176
+ stat.trackIdentifier = stat.trackIdentifier.replace(/[{}]/g, "");
1177
+ }
1178
+
1179
+ if (stat.type === "outbound-rtp" && stat.mediaSourceId) {
1180
+ // Add custom metadata
1181
+ stat.customQualityFlag = this.calculateQualityFlag(stat);
1182
+ }
1183
+
1184
+ return stat;
1185
+ });
260
1186
  }
261
- });
1187
+
1188
+ postAdapt(stats) {
1189
+ // Post-processing: runs after initial monitor updates
1190
+ // Useful for cross-stat calculations
1191
+ const inboundStats = stats.filter((s) => s.type === "inbound-rtp");
1192
+ const outboundStats = stats.filter((s) => s.type === "outbound-rtp");
1193
+
1194
+ // Add custom correlation stats
1195
+ if (inboundStats.length > 0 && outboundStats.length > 0) {
1196
+ stats.push({
1197
+ type: "custom-correlation",
1198
+ id: "correlation-metrics",
1199
+ timestamp: Date.now(),
1200
+ totalStreams: inboundStats.length + outboundStats.length,
1201
+ avgBitrate: this.calculateAvgBitrate(inboundStats, outboundStats),
1202
+ });
1203
+ }
1204
+
1205
+ return stats;
1206
+ }
1207
+
1208
+ calculateQualityFlag(stat) {
1209
+ // Custom quality assessment logic
1210
+ return stat.bitrate > 1000000 ? "high" : "standard";
1211
+ }
1212
+
1213
+ calculateAvgBitrate(inbound, outbound) {
1214
+ // Custom correlation calculation
1215
+ const totalBitrate = [...inbound, ...outbound].reduce((sum, stat) => sum + (stat.bitrate || 0), 0);
1216
+ return totalBitrate / (inbound.length + outbound.length);
1217
+ }
1218
+ }
1219
+
1220
+ // Add to peer connection monitor
1221
+ const adapter = new CustomStatsAdapter();
1222
+ pcMonitor.statsAdapters.add(adapter);
1223
+
1224
+ // Remove adapter
1225
+ pcMonitor.statsAdapters.remove(adapter);
1226
+ // or by name
1227
+ pcMonitor.statsAdapters.remove("custom-stats-adapter");
262
1228
  ```
263
1229
 
264
- **Accessing Calculated Updates**:
1230
+ ### Adapter Processing Flow
265
1231
 
266
- ```javascript
267
- monitor.on("stats-collected", () => {
268
- for (const track of monitor.tracks) {
269
- if (track?.direction === "outbound") {
270
- console.log(`Stats belongs to Track ${track.trackId} `);
271
- console.log(`Lost packets reported by remote endpoint: ${track.remoteLostPackets}`);
272
- console.log(`Received packets reported by remote endpoint: ${track.remoteReceivedPackets}`);
273
- console.log(`Sent packets reported by local endpoint: ${track.sentPackets}`);
274
- console.log(`Sending bitrate ${track.sendingBitrate}`);
275
- console.log(`Associated sfu stream id: ${track.sfuStreamId}`);
276
- }
1232
+ Adapters are processed in a specific order during stats collection:
277
1233
 
278
- if (trackStats?.direction === "inbound") {
279
- console.log(`Stats belongs to Track ${track.trackId} `);
280
- console.log(`Lost packets reported by local endpoint: ${track.lostPackets}`);
281
- console.log(`Received packets reported by local endpoint: ${track.receivedPackets}`);
282
- console.log(`Received bitrate ${track.receivingBitrate}`);
283
- console.log(`Associated sfu stream id: ${track.sfuStreamId}`);
284
- console.log(`Associated sfu sink id: ${track.sfuSinkId}`);
285
- }
1234
+ 1. **Raw Stats Collection**: `getStats()` called on peer connection
1235
+ 2. **Pre-Adaptation**: `adapt()` method called on all adapters in order
1236
+ 3. **Monitor Updates**: Monitors process adapted stats and update derived fields
1237
+ 4. **Post-Adaptation**: `postAdapt()` method called for advanced cross-stat processing
1238
+ 5. **Final Processing**: Detectors run and scores calculated
1239
+
1240
+ ### Advanced Adapter Examples
1241
+
1242
+ #### Mediasoup Probator Filter
1243
+
1244
+ ```javascript
1245
+ class MediasoupProbatorFilter {
1246
+ name = "mediasoup-probator-filter";
1247
+
1248
+ adapt(stats) {
1249
+ // Filter out mediasoup probator tracks
1250
+ return stats.filter((stat) => {
1251
+ if (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") {
1252
+ return stat.trackIdentifier !== "probator";
1253
+ }
1254
+ return true;
1255
+ });
286
1256
  }
287
- });
1257
+ }
288
1258
  ```
289
1259
 
290
- **Accessing related entries**
1260
+ #### Bandwidth Estimation Adapter
291
1261
 
292
1262
  ```javascript
293
- monitor.on("stats-collected", () => {
294
- for (const track of monitor.tracks) {
295
- console.log(`Track ${track.trackId} is ${track.kind}`);
296
- if (track.direction === "outbound") {
297
- [...track.outboundRtps()].forEach((outboundRtp) => {
298
- console.log(`Outbound RTP ${outboundRtp.getSsrc()} has ${outboundRtp.sentPackets} sent packets`);
1263
+ class BandwidthEstimationAdapter {
1264
+ name = "bandwidth-estimation-adapter";
1265
+
1266
+ postAdapt(stats) {
1267
+ const candidatePairs = stats.filter((s) => s.type === "candidate-pair");
1268
+ const selectedPair = candidatePairs.find((p) => p.state === "succeeded");
1269
+
1270
+ if (selectedPair && selectedPair.availableIncomingBitrate) {
1271
+ // Add custom bandwidth metrics
1272
+ stats.push({
1273
+ type: "custom-bandwidth",
1274
+ id: "bandwidth-estimation",
1275
+ timestamp: Date.now(),
1276
+ estimatedBandwidth: selectedPair.availableIncomingBitrate,
1277
+ bandwidthUtilization: this.calculateUtilization(stats, selectedPair),
299
1278
  });
300
1279
  }
301
1280
 
302
- if (track.direction === "inbound") {
303
- [...track.inboundRtps()].forEach((inboundRtp) => {
304
- console.log(`Inbound RTP ${inboundRtp.getSsrc()} has ${inboundRtp.receivedPackets} received packets`);
305
- });
306
- }
1281
+ return stats;
307
1282
  }
308
- });
1283
+
1284
+ calculateUtilization(stats, selectedPair) {
1285
+ const totalBitrate = stats
1286
+ .filter((s) => s.type === "inbound-rtp")
1287
+ .reduce((sum, s) => sum + (s.bitrate || 0), 0);
1288
+ return totalBitrate / selectedPair.availableIncomingBitrate;
1289
+ }
1290
+ }
309
1291
  ```
310
1292
 
311
- ### InboundRTP Entry
1293
+ ## Derived Metrics
1294
+
1295
+ The library automatically calculates numerous derived metrics from raw WebRTC statistics, providing enhanced insights into connection quality and performance. These metrics are computed during stats processing and are available on monitor objects.
312
1296
 
313
- **Accessing Stats**:
1297
+ ### Client-Level Derived Metrics
1298
+
1299
+ Available on `ClientMonitor`:
314
1300
 
315
1301
  ```javascript
316
- monitor.on("stats-collected", () => {
317
- for (const inboundRtp of monitor.inboundRtps) {
318
- console.log(`InboundRtp ${inboundRtp.statsId} collected stats`, inboundRtp.stats);
319
- }
320
- });
1302
+ const monitor = new ClientMonitor();
1303
+
1304
+ // Aggregated bitrates across all peer connections
1305
+ console.log(monitor.sendingAudioBitrate); // Total audio sending bitrate (bps)
1306
+ console.log(monitor.sendingVideoBitrate); // Total video sending bitrate (bps)
1307
+ console.log(monitor.receivingAudioBitrate); // Total audio receiving bitrate (bps)
1308
+ console.log(monitor.receivingVideoBitrate); // Total video receiving bitrate (bps)
1309
+
1310
+ // Network capacity metrics
1311
+ console.log(monitor.totalAvailableIncomingBitrate); // Available bandwidth for receiving
1312
+ console.log(monitor.totalAvailableOutgoingBitrate); // Available bandwidth for sending
1313
+
1314
+ // Connection quality
1315
+ console.log(monitor.avgRttInSec); // Average RTT across connections (seconds)
1316
+ console.log(monitor.score); // Calculated quality score (0.0-5.0)
1317
+ console.log(monitor.durationOfCollectingStatsInMs); // Time to collect stats (performance indicator)
321
1318
  ```
322
1319
 
323
- **Accessing Calculated Updates**:
1320
+ ### Peer Connection Derived Metrics
1321
+
1322
+ Available on `PeerConnectionMonitor`:
324
1323
 
325
1324
  ```javascript
326
- monitor.on("stats-collected", () => {
327
- for (const inboundRtp of monitor.inboundRtps) {
328
- console.log(`InboundRtp ${inboundRtp.getSsrc()} `);
329
- console.log(`Received packets since last collecting: ${inboundRtp.receivedBytes}`);
330
- console.log(`Received bitrate since last collecting: ${inboundRtp.receivingBitrate}`);
331
- console.log(`Received packets since last collecting: ${inboundRtp.receivedPackets}`);
332
- console.log(`Received frames since last collecting: ${inboundRtp.receivedFrames}`);
333
- console.log(`Lost packets since last collecting: ${inboundRtp.lostPackets}`);
334
- console.log(`Average jitter buffer delay since last collecting: ${inboundRtp.avgJitterBufferDelayInMs}`);
335
- console.log(`Average RTT since last collecting: ${inboundRtp.avgRttInS}s`);
336
-
337
- console.log(`Associated sfu stream id: ${inboundRtp.sfuStreamId}`);
338
- console.log(`Associated sfu sink id: ${inboundRtp.sfuSinkId}`);
339
- }
340
- });
1325
+ const pcMonitor = /* get from monitor.peerConnections */;
1326
+
1327
+ // Bitrate metrics by media type
1328
+ console.log(pcMonitor.sendingAudioBitrate); // Audio sending bitrate (bps)
1329
+ console.log(pcMonitor.sendingVideoBitrate); // Video sending bitrate (bps)
1330
+ console.log(pcMonitor.receivingAudioBitrate); // Audio receiving bitrate (bps)
1331
+ console.log(pcMonitor.receivingVideoBitrate); // Video receiving bitrate (bps)
1332
+
1333
+ // Packet loss rates
1334
+ console.log(pcMonitor.outboundFractionLost); // Outbound packet loss fraction
1335
+ console.log(pcMonitor.inboundFractionalLost); // Inbound packet loss fraction
1336
+
1337
+ // Delta metrics (change since last collection)
1338
+ console.log(pcMonitor.deltaInboundPacketsLost); // Packets lost in period
1339
+ console.log(pcMonitor.deltaInboundPacketsReceived); // Packets received in period
1340
+ console.log(pcMonitor.deltaOutboundPacketsSent); // Packets sent in period
1341
+ console.log(pcMonitor.deltaAudioBytesSent); // Audio bytes sent in period
1342
+ console.log(pcMonitor.deltaVideoBytesSent); // Video bytes sent in period
1343
+ console.log(pcMonitor.deltaDataChannelBytesSent); // Data channel bytes sent
1344
+
1345
+ // Connection timing and RTT
1346
+ console.log(pcMonitor.avgRttInSec); // Current average RTT (seconds)
1347
+ console.log(pcMonitor.ewmaRttInSec); // EWMA smoothed RTT (seconds)
1348
+ console.log(pcMonitor.connectingStartedAt); // Connection start timestamp
1349
+ console.log(pcMonitor.connectedAt); // Connection established timestamp
1350
+
1351
+ // Network topology detection
1352
+ console.log(pcMonitor.usingTURN); // Boolean: using TURN relay
1353
+ console.log(pcMonitor.usingTCP); // Boolean: using TCP transport
1354
+ console.log(pcMonitor.iceState); // ICE connection state
1355
+
1356
+ // Historical peaks
1357
+ console.log(pcMonitor.highestSeenSendingBitrate); // Peak sending bitrate seen
1358
+ console.log(pcMonitor.highestSeenReceivingBitrate); // Peak receiving bitrate seen
1359
+ console.log(pcMonitor.highestSeenAvailableIncomingBitrate); // Peak available incoming
1360
+ console.log(pcMonitor.highestSeenAvailableOutgoingBitrate); // Peak available outgoing
341
1361
  ```
342
1362
 
343
- **Accessing related entries**:
1363
+ ### Track-Level Derived Metrics
1364
+
1365
+ #### Inbound Track Metrics
1366
+
1367
+ Available on `InboundTrackMonitor`:
344
1368
 
345
1369
  ```javascript
346
- monitor.on("stats-collected", () => {
347
- for (const inboundRtp of monitor.inboundRtps) {
348
- console.log(
349
- `inbound RTP associated with SSRC ${inboundRtp.getSsrc()} uses codec ${
350
- inboundRtp.getCodec()?.stats.mimeType
351
- }`
352
- );
353
- console.log(
354
- `inbound RTP associated with SSRC ${inboundRtp.getSsrc()} remote endpoint sent ${
355
- inboundRtp.getRemoteOutboundRtp()?.stats.packetsSent
356
- } packets`
357
- );
358
-
359
- inboundRtp.kind === "audio" &&
360
- console.log(
361
- `inbound RTP associated with SSRC ${inboundRtp.getSsrc()} has played out ${
362
- inboundRtp.getAudioPlayout()?.stats.totalSamplesCount
363
- } audio samples`
364
- );
365
-
366
- const audioPlayout = inboundRtp.getAudioPlayout(); // The AudioPlayoutEntry associated with the inboundRtp
367
- const peerConnection = inboundRtp.getPeerConnection(); // The PeerConnectionEntry associated with the inboundRtp
368
- const trackId = inboundRtp.getTrackId(); // The trackId associated with the inboundRtp
369
- }
370
- });
1370
+ const inboundTrack = /* get from monitor.tracks */;
1371
+
1372
+ console.log(inboundTrack.bitrate); // Receiving bitrate (bps)
1373
+ console.log(inboundTrack.jitter); // Network jitter (seconds)
1374
+ console.log(inboundTrack.fractionLost); // Packet loss fraction
1375
+ console.log(inboundTrack.score); // Track quality score (0.0-5.0)
371
1376
  ```
372
1377
 
373
- ### OutboundRTP Entry
1378
+ #### Outbound Track Metrics
374
1379
 
375
- **Accessing Stats**:
1380
+ Available on `OutboundTrackMonitor`:
376
1381
 
377
1382
  ```javascript
378
- monitor.on("stats-collected", () => {
379
- for (const outboundRtp of monitor.outboundRtps) {
380
- console.log(`OutboundRtp ${outboundRtp.statsId} collected stats`, outboundRtp.stats);
381
- }
382
- });
1383
+ const outboundTrack = /* get from monitor.tracks */;
1384
+
1385
+ console.log(outboundTrack.bitrate); // Sending bitrate (bps)
1386
+ console.log(outboundTrack.sendingPacketRate); // Packets sent per second
1387
+ console.log(outboundTrack.remoteReceivedPacketRate); // Remote packets received per second
1388
+ console.log(outboundTrack.jitter); // Remote reported jitter
1389
+ console.log(outboundTrack.fractionLost); // Remote reported packet loss
1390
+ console.log(outboundTrack.score); // Track quality score (0.0-5.0)
383
1391
  ```
384
1392
 
385
- **Accessing Calculated Updates**:
1393
+ ### RTP-Level Derived Metrics
1394
+
1395
+ #### Inbound RTP Metrics
1396
+
1397
+ Available on `InboundRtpMonitor`:
386
1398
 
387
1399
  ```javascript
388
- monitor.on("stats-collected", () => {
389
- for (const outboundRtp of monitor.outboundRtps) {
390
- console.log(`OutboundRtp ${outboundRtp.getSsrc()} `);
391
- console.log(`Sent packets since last collecting: ${outboundRtp.sentPackets}`);
392
- console.log(`Sending bitrate since last collecting: ${outboundRtp.sendingBitrate}`);
393
- console.log(`Sent bytes since last collecting: ${outboundRtp.sentBytes}`);
394
- console.log(`Associated sfu stream id: ${outboundRtp.sfuStreamId}`);
395
- }
396
- });
1400
+ const inboundRtp = /* get from pcMonitor.mappedInboundRtpMonitors */;
1401
+
1402
+ // Bitrate and packet metrics
1403
+ console.log(inboundRtp.bitrate); // Calculated receiving bitrate (bps)
1404
+ console.log(inboundRtp.packetRate); // Packets received per second
1405
+ console.log(inboundRtp.fractionLost); // Calculated packet loss fraction
1406
+ console.log(inboundRtp.bitPerPixel); // Video: bits per pixel efficiency
1407
+
1408
+ // Video-specific derived metrics
1409
+ console.log(inboundRtp.avgFramesPerSec); // Average FPS over recent samples
1410
+ console.log(inboundRtp.ewmaFps); // EWMA smoothed FPS
1411
+ console.log(inboundRtp.fpsVolatility); // FPS stability (lower is better)
1412
+ console.log(inboundRtp.isFreezed); // Boolean: video appears frozen
1413
+
1414
+ // Audio-specific metrics
1415
+ console.log(inboundRtp.receivingAudioSamples); // Audio samples received in period
1416
+ console.log(inboundRtp.desync); // Boolean: audio desync detected
1417
+
1418
+ // Delta metrics (change since last collection)
1419
+ console.log(inboundRtp.deltaPacketsLost); // Packets lost in period
1420
+ console.log(inboundRtp.deltaPacketsReceived); // Packets received in period
1421
+ console.log(inboundRtp.deltaBytesReceived); // Bytes received in period
1422
+ console.log(inboundRtp.deltaJitterBufferDelay); // Jitter buffer delay change
1423
+ console.log(inboundRtp.deltaFramesDecoded); // Video frames decoded in period
1424
+ console.log(inboundRtp.deltaFramesReceived); // Video frames received in period
1425
+ console.log(inboundRtp.deltaFramesRendered); // Video frames rendered in period
1426
+ console.log(inboundRtp.deltaCorruptionProbability); // Frame corruption change
1427
+ console.log(inboundRtp.deltaTime); // Elapsed time for calculations (ms)
397
1428
  ```
398
1429
 
399
- **Accessing related entries**:
1430
+ #### Outbound RTP Metrics
1431
+
1432
+ Available on `OutboundRtpMonitor`:
400
1433
 
401
1434
  ```javascript
402
- monitor.on("stats-collected", () => {
403
- for (const outboundRtp of monitor.outboundRtps) {
404
- console.log(
405
- `outbound RTP associated with SSRC ${outboundRtp.getSsrc()} uses codec ${
406
- outboundRtp.getCodec()?.stats.mimeType
407
- }`
408
- );
409
- console.log(
410
- `outbound RTP associated with SSRC ${outboundRtp.getSsrc()} remote endpoint received ${
411
- outboundRtp.getRemoteInboundRtp()?.stats.packetsReceived
412
- } packets`
413
- );
414
-
415
- const remoteInboundRtp = outboundRtp.getRemoteInboundRtp(); // The RemoteInboundRtpEntry associated with the outboundRtp
416
- const peerConnection = outboundRtp.getPeerConnection(); // The PeerConnectionEntry associated with the outboundRtp
417
- const trackId = outboundRtp.getTrackId(); // The trackId associated with the outboundRtp
418
- }
419
- });
1435
+ const outboundRtp = /* get from pcMonitor.mappedOutboundRtpMonitors */;
1436
+
1437
+ // Bitrate metrics
1438
+ console.log(outboundRtp.bitrate); // Total sending bitrate (bps)
1439
+ console.log(outboundRtp.payloadBitrate); // Payload-only bitrate (excluding headers/retransmissions)
1440
+ console.log(outboundRtp.packetRate); // Packets sent per second
1441
+ console.log(outboundRtp.bitPerPixel); // Video: bits per pixel efficiency
1442
+
1443
+ // Delta metrics
1444
+ console.log(outboundRtp.deltaPacketsSent); // Packets sent in period
1445
+ console.log(outboundRtp.deltaBytesSent); // Bytes sent in period
420
1446
  ```
421
1447
 
422
- ## Detectors
1448
+ #### Remote RTP Metrics
423
1449
 
424
- ClientMonitor can create detectors to detect various issues and anomalies in the collected stats. These detectors are updated when stats are polled, monitoring the stats and setting an alert if certain thresholds are hit. You can create detectors and subscribe to alerts using an instantiated client-monitor-js.
1450
+ **Remote Inbound RTP** (remote peer's receiving stats):
425
1451
 
426
- Currently, the following detectors are available in ClientMonitor:
1452
+ ```javascript
1453
+ const remoteInboundRtp = /* get from pcMonitor.mappedRemoteInboundRtpMonitors */;
427
1454
 
428
- ### Congestion Detector
1455
+ console.log(remoteInboundRtp.packetRate); // Remote receiving packet rate
1456
+ console.log(remoteInboundRtp.deltaPacketsLost); // Remote packets lost in period
1457
+ ```
429
1458
 
430
- The Congestion Detector identifies congestion issues in media streaming. When congestion is detected, it triggers an event providing insights into the available incoming and outgoing bitrates before and after congestion.
1459
+ **Remote Outbound RTP** (remote peer's sending stats):
431
1460
 
432
1461
  ```javascript
433
- const detector = monitor.createCongestionDetector();
1462
+ const remoteOutboundRtp = /* get from pcMonitor.mappedRemoteOutboundRtpMonitors */;
434
1463
 
435
- detector.on("congestion", (event) => {
436
- console.log("congestion detected on media streaming");
437
- console.log("Available incoming bitrate before congestion", event.incomingBitrateBeforeCongestion);
438
- console.log("Available outgoing bitrate before congestion", event.outgoingBitrateBeforeCongestion);
439
- console.log("Available incoming bitrate after congestion", event.incomingBitrateAfterCongestion);
440
- console.log("Available outgoing bitrate after congestion", event.outgoingBitrateAfterCongestion);
441
- });
1464
+ console.log(remoteOutboundRtp.bitrate); // Remote sending bitrate
442
1465
  ```
443
1466
 
444
- ### Audio Desync Detector
1467
+ ### ICE Transport Derived Metrics
445
1468
 
446
- ```javascript
447
- const detector = monitor.createAudioDesyncDetector();
1469
+ Available on `IceTransportMonitor` and `IceCandidatePairMonitor`:
448
1470
 
449
- detector.on("statechanged", (event) => {
450
- console.log(`Audio is ${event.state} for track ${event.trackId}`);
451
- });
1471
+ ```javascript
1472
+ const iceTransport = /* get from pcMonitor.mappedIceTransportMonitors */;
1473
+
1474
+ // Transport-level bitrates
1475
+ console.log(iceTransport.sendingBitrate); // Transport sending bitrate
1476
+ console.log(iceTransport.receivingBitrate); // Transport receiving bitrate
1477
+
1478
+ // Delta metrics
1479
+ console.log(iceTransport.deltaPacketsSent); // Packets sent in period
1480
+ console.log(iceTransport.deltaPacketsReceived); // Packets received in period
1481
+ console.log(iceTransport.deltaBytesSent); // Bytes sent in period
1482
+ console.log(iceTransport.deltaBytesReceived); // Bytes received in period
1483
+
1484
+ // ICE candidate pair specific
1485
+ const candidatePair = /* get from pcMonitor.mappedIceCandidatePairMonitors */;
1486
+ console.log(candidatePair.availableIncomingBitrate); // Bandwidth estimation for receiving
1487
+ console.log(candidatePair.availableOutgoingBitrate); // Bandwidth estimation for sending
452
1488
  ```
453
1489
 
454
- #### Audio Desync Detector Configuration
1490
+ ### Data Channel Derived Metrics
1491
+
1492
+ Available on `DataChannelMonitor`:
455
1493
 
456
1494
  ```javascript
457
- const detector = monitor.createAudioDesyncDetector({
458
- // Indicate if we want to create an issue automatically upon detection.
459
- // Issues created by the monitor can be caught by monitor.on('issue', (issue) => {}), and
460
- // automatically added to the sample.
461
- createIssueOnDetection: {
462
- severity: "major",
463
- attachments: {
464
- // various custom data
465
- },
466
- },
467
- /**
468
- * The fractional threshold used to determine if the audio desynchronization
469
- * correction is considered significant or not.
470
- * It represents the minimum required ratio of corrected samples to total samples.
471
- * For example, a value of 0.1 means that if the corrected samples ratio
472
- * exceeds 0.1, it will be considered a significant audio desynchronization issue.
473
- */
474
- fractionalCorrectionAlertOnThreshold: 0.1,
475
- /**
476
- * The fractional threshold used to determine if the audio desynchronization
477
- * correction is considered negligible and the alert should be turned off.
478
- * It represents the maximum allowed ratio of corrected samples to total samples.
479
- * For example, a value of 0.05 means that if the corrected samples ratio
480
- * falls below 0.05, the audio desynchronization alert will be turned off.
481
- */
482
- fractionalCorrectionAlertOffThreshold: 0.05,
483
- });
1495
+ const dataChannel = /* get from pcMonitor.mappedDataChannelMonitors */;
1496
+
1497
+ console.log(dataChannel.deltaBytesSent); // Bytes sent in period
1498
+ console.log(dataChannel.deltaBytesReceived); // Bytes received in period
484
1499
  ```
485
1500
 
486
- ### CPU Performance Detector
1501
+ ### Media Source and Playout Metrics
487
1502
 
488
- The CPU Performance Detector is designed to monitor and detect issues related to CPU performance that may impact the functionality and efficiency of your application.
1503
+ **Media Source derived metrics** (local media):
489
1504
 
490
1505
  ```javascript
491
- const detector = monitor.createCpuPerformanceIssueDetector();
1506
+ const mediaSource = /* get from pcMonitor.mappedMediaSourceMonitors */;
1507
+ // Media source stats are mostly raw WebRTC stats
1508
+ // Derived metrics are primarily calculated at RTP level
1509
+ ```
492
1510
 
493
- detector.on("statechanged", (state) => {
494
- console.log("CPU limitation state changed", state);
495
- });
1511
+ **Media Playout derived metrics** (audio playout):
1512
+
1513
+ ```javascript
1514
+ const mediaPlayout = /* get from pcMonitor.mappedMediaPlayoutMonitors */;
1515
+
1516
+ console.log(mediaPlayout.deltaSynthesizedSamplesDuration); // Synthesized audio duration in period
1517
+ console.log(mediaPlayout.deltaSamplesDuration); // Total samples duration in period
496
1518
  ```
497
1519
 
498
- #### CPU Performance Detector Configuration
1520
+ ### Accessing Derived Metrics
499
1521
 
500
1522
  ```javascript
501
- const detector = monitor.createCpuPerformanceIssueDetector({
502
- // Indicate if we want to create an issue automatically upon detection.
503
- // Issues created by the monitor can be caught by monitor.on('issue', (issue) => {}), and
504
- // automatically added to the sample.
505
- createIssueOnDetection: {
506
- severity: "major",
507
- attachments: {
508
- // various custom data
509
- },
510
- },
1523
+ // Access derived metrics through monitor hierarchy
1524
+ monitor.on("stats-collected", () => {
1525
+ // Client-level aggregates
1526
+ console.log("Total sending bitrate:", monitor.sendingAudioBitrate + monitor.sendingVideoBitrate);
1527
+
1528
+ // Per-connection metrics
1529
+ monitor.peerConnections.forEach((pc) => {
1530
+ console.log(`PC ${pc.peerConnectionId} RTT:`, pc.avgRttInSec * 1000, "ms");
1531
+
1532
+ // Per-track metrics
1533
+ pc.mappedInboundTracks.forEach((track) => {
1534
+ if (track.kind === "video") {
1535
+ const inboundRtp = track.getInboundRtp();
1536
+ console.log(`Video FPS: ${inboundRtp?.ewmaFps}, Volatility: ${inboundRtp?.fpsVolatility}`);
1537
+ }
1538
+ });
1539
+ });
511
1540
  });
1541
+
1542
+ // Manual access to specific metrics
1543
+ const videoTrack = monitor.tracks.find((t) => t.kind === "video" && t.direction === "inbound");
1544
+ if (videoTrack) {
1545
+ const rtp = videoTrack.getInboundRtp();
1546
+ console.log("Video quality metrics:", {
1547
+ bitrate: rtp.bitrate,
1548
+ fps: rtp.ewmaFps,
1549
+ volatility: rtp.fpsVolatility,
1550
+ packetLoss: rtp.fractionLost,
1551
+ });
1552
+ }
1553
+ ```
1554
+
1555
+ ## Schema Reference
1556
+
1557
+ ### ClientSample
1558
+
1559
+ The main sample structure containing complete client state:
1560
+
1561
+ ```typescript
1562
+ type ClientSample = {
1563
+ timestamp: number;
1564
+ clientId?: string;
1565
+ callId?: string;
1566
+ score?: number;
1567
+ scoreReasons?: string;
1568
+ attachments?: Record<string, unknown>;
1569
+ peerConnections?: PeerConnectionSample[];
1570
+ clientEvents?: ClientEvent[];
1571
+ clientIssues?: ClientIssue[];
1572
+ clientMetaItems?: ClientMetaData[];
1573
+ extensionStats?: ExtensionStat[];
1574
+ };
1575
+ ```
1576
+
1577
+ ### PeerConnectionSample
1578
+
1579
+ Per-peer-connection statistics:
1580
+
1581
+ ```typescript
1582
+ type PeerConnectionSample = {
1583
+ peerConnectionId: string;
1584
+ score?: number;
1585
+ scoreReasons?: string;
1586
+ attachments?: Record<string, unknown>;
1587
+ inboundTracks?: InboundTrackSample[];
1588
+ outboundTracks?: OutboundTrackSample[];
1589
+ codecs?: CodecStats[];
1590
+ inboundRtps?: InboundRtpStats[];
1591
+ outboundRtps?: OutboundRtpStats[];
1592
+ remoteInboundRtps?: RemoteInboundRtpStats[];
1593
+ remoteOutboundRtps?: RemoteOutboundRtpStats[];
1594
+ mediaSources?: MediaSourceStats[];
1595
+ mediaPlayouts?: MediaPlayoutStats[];
1596
+ dataChannels?: DataChannelStats[];
1597
+ iceTransports?: IceTransportStats[];
1598
+ iceCandidates?: IceCandidateStats[];
1599
+ iceCandidatePairs?: IceCandidatePairStats[];
1600
+ certificates?: CertificateStats[];
1601
+ };
512
1602
  ```
513
1603
 
514
- ### Video Freeze Detector
1604
+ ### Stats Types
1605
+
1606
+ All stats types include standard WebRTC fields plus:
1607
+
1608
+ - `timestamp`: When the stats were collected
1609
+ - `id`: Unique identifier
1610
+ - `attachments`: Additional data for sampling
1611
+
1612
+ **Key Stats Types:**
1613
+
1614
+ - `InboundRtpStats`: Receiving stream statistics
1615
+ - `OutboundRtpStats`: Sending stream statistics
1616
+ - `IceCandidatePairStats`: ICE candidate pair information
1617
+ - `CodecStats`: Codec configuration
1618
+ - `MediaSourceStats`: Local media source stats
1619
+
1620
+ ## Examples
1621
+
1622
+ ### Basic Monitoring Setup
515
1623
 
516
1624
  ```javascript
517
- const detector = monitor.createVideoFreezesDetector();
1625
+ import { ClientMonitor } from "@observertc/client-monitor-js";
1626
+
1627
+ const monitor = new ClientMonitor({
1628
+ clientId: "client-123",
1629
+ callId: "call-456",
1630
+ collectingPeriodInMs: 2000,
1631
+ samplingPeriodInMs: 5000,
1632
+ });
518
1633
 
519
- detector.on("freezedVideoStarted", (event) => {
520
- console.log("Freezed video started");
521
- console.log("TrackId", event.trackId);
522
- console.log("PeerConnectionId", event.peerConnectionId);
523
- console.log("SSRC:", event.ssrc);
1634
+ // Add peer connection
1635
+ const pc = new RTCPeerConnection();
1636
+ monitor.addSource(pc);
1637
+
1638
+ // Handle samples
1639
+ monitor.on("sample-created", (sample) => {
1640
+ // Send to analytics
1641
+ fetch("/analytics", {
1642
+ method: "POST",
1643
+ body: JSON.stringify(sample),
1644
+ headers: { "Content-Type": "application/json" },
1645
+ });
524
1646
  });
525
1647
 
526
- detector.on("freezedVideoEnded", (event) => {
527
- console.log("Freezed video ended");
528
- console.log("TrackId", event.trackId);
529
- console.log("Freeze duration in Seconds", event.durationInS);
530
- console.log("PeerConnectionId", event.peerConnectionId);
531
- console.log("SSRC:", event.ssrc);
1648
+ // Handle issues
1649
+ monitor.on("issue", (issue) => {
1650
+ console.warn("Issue detected:", issue.type, issue.payload);
532
1651
  });
533
1652
  ```
534
1653
 
535
- #### Video Freeze Detector Configuration
1654
+ ### Advanced Configuration
536
1655
 
537
1656
  ```javascript
538
- const detector = monitor.createVideoFreezesDetector({
539
- // Indicate if we want to create an issue automatically upon detection.
540
- // Issues created by the monitor can be caught by monitor.on('issue', (issue) => {}), and
541
- // automatically added to the sample.
542
- createIssueOnDetection: {
543
- severity: "major",
544
- attachments: {
545
- // various custom data
1657
+ const monitor = new ClientMonitor({
1658
+ clientId: "advanced-client",
1659
+ collectingPeriodInMs: 1000,
1660
+ samplingPeriodInMs: 3000,
1661
+
1662
+ // Sensitive congestion detection
1663
+ congestionDetector: {
1664
+ sensitivity: "high",
1665
+ createIssue: true,
1666
+ },
1667
+
1668
+ // Strict CPU monitoring
1669
+ cpuPerformanceDetector: {
1670
+ fpsVolatilityThresholds: {
1671
+ lowWatermark: 0.05,
1672
+ highWatermark: 0.2,
546
1673
  },
1674
+ durationOfCollectingStatsThreshold: {
1675
+ lowWatermark: 3000,
1676
+ highWatermark: 6000,
1677
+ },
1678
+ },
1679
+
1680
+ // Quick dry track detection
1681
+ dryInboundTrackDetector: {
1682
+ thresholdInMs: 3000,
1683
+ },
1684
+
1685
+ appData: {
1686
+ version: "1.0.0",
1687
+ feature: "screen-share",
547
1688
  },
548
1689
  });
549
1690
  ```
550
1691
 
551
- ### Stucked Inbound Track Detector
552
-
553
- The Stucked Inbound Track Detector is designed to identify tracks that are experiencing a complete lack of traffic since their creation. These tracks, termed "stucked inbound tracks," are characterized by the absence of any packet reception from the moment of their creation. In essence, stucked inbound tracks refer to tracks that have not received any data packets since their inception.
1692
+ ### Mediasoup Integration
554
1693
 
555
1694
  ```javascript
556
- const detector = monitor.createStuckedInboundTrackDetector();
1695
+ import mediasoup from "mediasoup-client";
557
1696
 
558
- detector.on("stuckedtrack", (event) => {
559
- console.log("Stucked inbound track detected");
560
- console.log("TrackId", event.trackId);
561
- console.log("PeerConnectionId", event.peerConnectionId);
562
- console.log("SSRC:", event.ssrc);
1697
+ const device = new mediasoup.Device();
1698
+ const monitor = new ClientMonitor({
1699
+ clientId: "mediasoup-client",
563
1700
  });
564
- ```
565
1701
 
566
- If an inbound track is paused by remote (the outbound track is muted or the it is known that the SFU does not forward traffic),
567
- you can add the trackId to the ignoredTrackIds of this detector to avoid false positive detections.
1702
+ // Load device capabilities
1703
+ await device.load({ routerRtpCapabilities });
568
1704
 
569
- ```javascript
570
- detector.ignoredTrackIds.add(trackId);
571
- ```
1705
+ // Add device for monitoring
1706
+ monitor.addSource(device);
572
1707
 
573
- #### Stucked Inbound Track Detector Configuration
1708
+ // Create transport
1709
+ const sendTransport = device.createSendTransport({
1710
+ // transport options
1711
+ });
574
1712
 
575
- ```javascript
576
- const detector = monitor.createStuckedInboundTrackDetector({
577
- // Indicate if we want to create an issue automatically upon detection.
578
- // Issues created by the monitor can be caught by monitor.on('issue', (issue) => {}), and
579
- // automatically added to the sample.
580
- createIssueOnDetection: {
581
- severity: "major",
582
- attachments: {
583
- // various custom data
584
- },
585
- },
586
- // Minimum duration in milliseconds for a track to be considered stuck
587
- minStuckedDurationInMs: 5000,
1713
+ // The monitor automatically detects the new transport
1714
+ // For existing transports, add manually:
1715
+ // monitor.addSource(sendTransport);
1716
+
1717
+ // Produce media
1718
+ const producer = await sendTransport.produce({
1719
+ track: videoTrack,
1720
+ codecOptions: {},
588
1721
  });
589
- ```
590
1722
 
591
- ### Long Peer Connection Establishment Detector
1723
+ // Track is automatically monitored
1724
+ ```
592
1725
 
593
- The Long Peer Connection Establishment Detector is designed to identify peer connections that take an unusually long time to establish. This detector is particularly useful for detecting peer connections that are stuck in the process of establishing a connection.
1726
+ ### Custom Detector
594
1727
 
595
1728
  ```javascript
596
- const detector = monitor.createLongPeerConnectionEstablishmentDetector();
1729
+ class NetworkLatencyDetector {
1730
+ name = "network-latency-detector";
1731
+
1732
+ constructor(pcMonitor) {
1733
+ this.pcMonitor = pcMonitor;
1734
+ this.highLatencyThreshold = 200; // ms
1735
+ }
1736
+
1737
+ update() {
1738
+ const rttMs = (this.pcMonitor.avgRttInSec || 0) * 1000;
1739
+
1740
+ if (rttMs > this.highLatencyThreshold) {
1741
+ this.pcMonitor.parent.emit("high-latency", {
1742
+ peerConnectionId: this.pcMonitor.peerConnectionId,
1743
+ rttMs,
1744
+ });
1745
+
1746
+ this.pcMonitor.parent.addIssue({
1747
+ type: "high-latency",
1748
+ payload: { rttMs, threshold: this.highLatencyThreshold },
1749
+ });
1750
+ }
1751
+ }
1752
+ }
597
1753
 
598
- detector.on("too-long-connection-establishment", (event) => {
599
- console.log("Long peer connection establishment detected");
600
- console.log("PeerConnectionId", event.peerConnectionId);
1754
+ // Add to peer connection monitor
1755
+ monitor.on("peer-connection-opened", ({ peerConnectionMonitor }) => {
1756
+ const detector = new NetworkLatencyDetector(peerConnectionMonitor);
1757
+ peerConnectionMonitor.detectors.add(detector);
601
1758
  });
602
1759
  ```
603
1760
 
604
- #### Long Peer Connection Establishment Detector Configuration
1761
+ ### Real-time Monitoring Dashboard
605
1762
 
606
1763
  ```javascript
607
- const detector = monitor.createLongPeerConnectionEstablishmentDetector({
608
- // Indicate if we want to create an issue automatically upon detection.
609
- // Issues created by the monitor can be caught by monitor.on('issue', (issue) => {}), and
610
- // automatically added to the sample.
611
- createIssueOnDetection: {
612
- severity: "major",
613
- attachments: {
614
- // various custom data
615
- },
616
- },
617
- // Minimum duration in milliseconds for a peer connection to be considered stuck
618
- thresholdInMs: 5000,
619
- });
1764
+ class MonitoringDashboard {
1765
+ constructor(monitor) {
1766
+ this.monitor = monitor;
1767
+ this.setupEventListeners();
1768
+ }
1769
+
1770
+ setupEventListeners() {
1771
+ this.monitor.on("score", ({ clientScore, scoreReasons }) => {
1772
+ this.updateScoreDisplay(clientScore, scoreReasons);
1773
+ });
1774
+
1775
+ this.monitor.on("congestion", ({ availableIncomingBitrate, availableOutgoingBitrate }) => {
1776
+ this.showCongestionAlert(availableIncomingBitrate, availableOutgoingBitrate);
1777
+ });
1778
+
1779
+ this.monitor.on("stats-collected", ({ durationOfCollectingStatsInMs }) => {
1780
+ this.updatePerformanceMetrics(durationOfCollectingStatsInMs);
1781
+ });
1782
+
1783
+ this.monitor.on("issue", (issue) => {
1784
+ this.addIssueToLog(issue);
1785
+ });
1786
+ }
1787
+
1788
+ updateScoreDisplay(score, reasons) {
1789
+ document.getElementById("score").textContent = score.toFixed(1);
1790
+ document.getElementById("score-reasons").textContent = JSON.stringify(reasons, null, 2);
1791
+ }
1792
+
1793
+ showCongestionAlert(incoming, outgoing) {
1794
+ const alert = document.createElement("div");
1795
+ alert.className = "congestion-alert";
1796
+ alert.textContent = `Congestion detected! Available: ${incoming}/${outgoing} kbps`;
1797
+ document.body.appendChild(alert);
1798
+ }
1799
+
1800
+ updatePerformanceMetrics(duration) {
1801
+ document.getElementById("collection-time").textContent = `${duration}ms`;
1802
+ }
1803
+
1804
+ addIssueToLog(issue) {
1805
+ const log = document.getElementById("issue-log");
1806
+ const entry = document.createElement("div");
1807
+ entry.textContent = `${new Date().toISOString()}: ${issue.type} - ${JSON.stringify(issue.payload)}`;
1808
+ log.appendChild(entry);
1809
+ }
1810
+ }
1811
+
1812
+ // Initialize dashboard
1813
+ const dashboard = new MonitoringDashboard(monitor);
620
1814
  ```
621
1815
 
622
- ### Issues
1816
+ ## Troubleshooting
1817
+
1818
+ ### Common Issues
1819
+
1820
+ #### High Memory Usage
623
1821
 
624
- Issues encountered during monitoring sessions are categorized by severity, which can be one of the following values: critical, major, or minor.
1822
+ ```javascript
1823
+ // Limit stored scores history
1824
+ monitor.scoreCalculator.constructor.lastNScoresMaxLength = 5;
1825
+
1826
+ // Disable unnecessary detectors
1827
+ monitor.config.audioDesyncDetector.disabled = true;
625
1828
 
626
- When a detector is configured to create an issue upon detection, it automatically adds the issue to the monitor instance once the specified condition is met.
1829
+ // Reduce collection frequency
1830
+ monitor.setCollectingPeriod(5000);
1831
+ ```
627
1832
 
628
- Issues can be added to the monitor instance using the addIssue method. For instance:
1833
+ #### Missing Statistics
629
1834
 
630
1835
  ```javascript
631
- monitor.addIssue({
632
- severity: "critical",
633
- description: "Media device is crashed",
634
- timestamp: Date.now(),
635
- attachments: {
636
- clientId,
637
- reason,
638
- },
1836
+ // Check if source is properly added
1837
+ console.log("Peer connections:", monitor.peerConnections.length);
1838
+
1839
+ // Verify stats collection
1840
+ monitor.on("stats-collected", ({ collectedStats }) => {
1841
+ console.log("Collected stats from PCs:", collectedStats.length);
1842
+ });
1843
+
1844
+ // Check for adaptation issues
1845
+ monitor.statsAdapters.add((stats) => {
1846
+ console.log("Raw stats count:", stats.length);
1847
+ return stats;
639
1848
  });
640
1849
  ```
641
1850
 
642
- Furthermore, events related to issues are dispatched through the monitor. For example:
1851
+ #### Browser Compatibility
643
1852
 
644
1853
  ```javascript
645
- monitor.on("issue", (issue) => {
646
- console.log("An issue is detected", issue);
1854
+ // Check browser support
1855
+ if (!window.RTCPeerConnection) {
1856
+ console.error("WebRTC not supported");
1857
+ }
1858
+
1859
+ // Handle browser-specific issues
1860
+ monitor.on("stats-collected", ({ collectedStats }) => {
1861
+ if (collectedStats.length === 0) {
1862
+ console.warn("No stats collected - possible browser issue");
1863
+ }
647
1864
  });
648
1865
  ```
649
1866
 
650
- Each issue detected is included in the subsequent sample created by the monitor, provided that `samplingTick > 0` in the monitor configuration.
1867
+ ### Debug Information
651
1868
 
652
- Additionally, each detector can be configured to automatically create an issue upon detection. For instance:
1869
+ Enable debug logging:
653
1870
 
654
1871
  ```javascript
655
- const detector = monitor.createCongestionDetector({
656
- createIssueOnDetection: {
657
- severity: "major",
658
- attachments: {
659
- // various custom data
660
- },
661
- },
1872
+ import { setLogger } from "@observertc/client-monitor-js";
1873
+
1874
+ setLogger({
1875
+ trace: console.trace,
1876
+ debug: console.debug,
1877
+ info: console.info,
1878
+ warn: console.warn,
1879
+ error: console.error,
662
1880
  });
663
1881
  ```
664
1882
 
665
- ## Configurations
1883
+ ### Performance Optimization
666
1884
 
667
1885
  ```javascript
668
- const config = {
669
- /**
670
- * By setting it, the monitor calls the added statsCollectors periodically
671
- * and pulls the stats.
672
- *
673
- * DEFAULT: 2000
674
- */
675
- collectingPeriodInMs: 2000,
1886
+ // Optimize for large numbers of tracks
1887
+ const monitor = new ClientMonitor({
1888
+ collectingPeriodInMs: 3000, // Reduce frequency
1889
+ samplingPeriodInMs: 10000, // Less frequent sampling
1890
+
1891
+ // Disable resource-intensive detectors
1892
+ cpuPerformanceDetector: { disabled: true },
1893
+ audioDesyncDetector: { disabled: true },
1894
+ });
676
1895
 
677
- /**
678
- * By setting this, the monitor makes samples after n number or collected stats.
679
- *
680
- * For example if the value is 10, the observer makes a sample after 10 collected stats (in every 10 collectingPeriodInMs).
681
- * if the value is less or equal than 0
682
- *
683
- * DEFAULT: 3
684
- */
685
- samplingTick: 3,
686
-
687
- /**
688
- * By setting this to true monitor will
689
- *
690
- * DEFAULT: 0
691
- */
692
- integrateNavigatorMediaDevices: true,
693
- /**
694
- * If true, the monitor creates a CLIENT_JOINED event when the monitor is created.
695
- *
696
- * additionally you can add attachments to the join event by setting it to:
697
- * {
698
- * attachments: {
699
- * my_custom_id: 'customId',
700
- * }
701
- * }
702
- *
703
- * DEFAULT: true
704
- */
705
- createClientJoinedEvent: true
706
- /**
707
- * Configuration for detecting issues.
708
- *
709
- * By default, all detectors are enabled.
710
- */
711
- detectIssues: {
712
- /**
713
- * Configuration for detecting congestion issues.
714
- *
715
- * DEFAULT: 'major'
716
- */
717
- congestion: 'major',
718
-
719
- /**
720
- * Configuration for detecting audio desynchronization issues.
721
- */
722
- audioDesync: 'minor',
723
-
724
- /**
725
- * Configuration for detecting frozen video issues.
726
- */
727
- freezedVideo: 'minor',
728
-
729
- /**
730
- * Configuration for detecting CPU limitation issues.
731
- */
732
- cpuLimitation: 'major',
733
-
734
- /**
735
- * Configuration for detecting stucked inbound track issues.
736
- */
737
- stuckedInboundTrack: 'major',
738
-
739
- /**
740
- * Configuration for detecting long peer connection establishment issues.
741
- */
742
- longPcConnectionEstablishment: 'major',
743
- }
744
- };
1896
+ // Manual garbage collection
1897
+ setInterval(() => {
1898
+ // Clear old data periodically
1899
+ monitor.scoreCalculator.totalReasons = {};
1900
+ }, 60000);
745
1901
  ```
746
1902
 
747
- ## Events
1903
+ ## API Reference
1904
+
1905
+ ### Types
748
1906
 
749
- 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.
1907
+ ```typescript
1908
+ // Configuration
1909
+ type ClientMonitorConfig = {
1910
+ /* ... */
1911
+ };
750
1912
 
751
- - `CLIENT_JOINED`: A client has joined
752
- - `CLIENT_LEFT`: A client has left
753
- - `PEER_CONNECTION_OPENED`: A peer connection is opened
754
- - `PEER_CONNECTION_CLOSED`: A peer connection is closed
755
- - `MEDIA_TRACK_ADDED`: A media track is added
756
- - `MEDIA_TRACK_REMOVED`: A media track is removed
757
- - `MEDIA_TRACK_MUTED`: A media track is muted
758
- - `MEDIA_TRACK_UNMUTED`: A media track is unmuted
759
- - `ICE_GATHERING_STATE_CHANGED`: The ICE gathering state has changed
760
- - `PEER_CONNECTION_STATE_CHANGED`: The peer connection state has changed
761
- - `ICE_CONNECTION_STATE_CHANGED`: The ICE connection state has changed
762
- - `DATA_CHANNEL_OPEN`: A data channel is opened
763
- - `DATA_CHANNEL_CLOSED`: A data channel is closed
764
- - `DATA_CHANNEL_ERROR`: A data channel error occurred
1913
+ // Core types
1914
+ type ClientSample = {
1915
+ /* ... */
1916
+ };
1917
+ type ClientEvent = { type: string; payload?: any; timestamp: number };
1918
+ type ClientIssue = { type: string; payload?: any; timestamp: number };
765
1919
 
766
- For Mediasoup integration the following events are detected:
1920
+ // Monitor types
1921
+ class InboundTrackMonitor {
1922
+ /* ... */
1923
+ }
1924
+ class OutboundTrackMonitor {
1925
+ /* ... */
1926
+ }
1927
+ class PeerConnectionMonitor {
1928
+ /* ... */
1929
+ }
767
1930
 
768
- - `PRODUCER_ADDED`: A producer is added
769
- - `PRODUCER_REMOVED`: A producer is removed
770
- - `PRODUCER_PAUSED`: A producer is paused
771
- - `PRODUCER_RESUMED`: A producer is resumed
772
- - `CONSUMER_ADDED`: A consumer is added
773
- - `CONSUMER_REMOVED`: A consumer is removed
774
- - `CONSUMER_PAUSED`: A consumer is paused
775
- - `CONSUMER_RESUMED`: A consumer is resumed
776
- - `DATA_PRODUCER_ADDED`: A data producer is added
777
- - `DATA_PRODUCER_REMOVED`: A data producer is removed
778
- - `DATA_CONSUMER_ADDED`: A data consumer is added
779
- - `DATA_CONSUMER_REMOVED`: A data consumer is removed
1931
+ // Detector interface
1932
+ interface Detector {
1933
+ readonly name: string;
1934
+ update(): void;
1935
+ }
1936
+ ```
780
1937
 
781
- ### CLIENT_JOINED Event
1938
+ ### Events
1939
+
1940
+ ```typescript
1941
+ interface ClientMonitorEvents {
1942
+ "sample-created": (sample: ClientSample) => void;
1943
+ "stats-collected": (data: {
1944
+ durationOfCollectingStatsInMs: number;
1945
+ collectedStats: [string, RTCStats[]][];
1946
+ }) => void;
1947
+ score: (data: { clientScore: number; scoreReasons?: Record<string, number> }) => void;
1948
+ issue: (issue: ClientIssue) => void;
1949
+ congestion: (data: CongestionEvent) => void;
1950
+ close: () => void;
1951
+ // ... detector-specific events
1952
+ }
1953
+ ```
782
1954
 
783
- A client event is automatically generated when the first sample is created, and
784
- the joined timestamp is set to the time of the monitor creation. However, you can
785
- manually set the event by calling the `monitor.join()` method.
1955
+ ## FAQ
786
1956
 
787
- **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.
1957
+ ### Q: How often should I collect statistics?
788
1958
 
789
- ```javascript
790
- monitor.join({
791
- timestamp: Date.now(),
792
- });
793
- ```
1959
+ **A**: The default 2-second interval (2000ms) works well for most applications. For real-time applications or debugging, you might use 1 second. For low-bandwidth situations, 5 seconds is acceptable.
794
1960
 
795
- ### CLIENT_LEFT Event
1961
+ ### Q: What's the difference between collectingPeriod and samplingPeriod?
796
1962
 
797
- 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.
1963
+ **A**:
798
1964
 
799
- **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.
1965
+ - `collectingPeriod`: How often to collect WebRTC stats from browser APIs
1966
+ - `samplingPeriod`: How often to create complete client samples (includes events, issues, metadata)
800
1967
 
801
- ```javascript
802
- monitor.leave({
803
- timestamp: Date.now(),
804
- });
805
- ```
1968
+ ### Q: How do I reduce bandwidth usage?
806
1969
 
807
- ### Custom Call Event
1970
+ **A**:
808
1971
 
809
- You can create a custom event by calling the `monitor.addCustomCallEvent()` method. The method takes an object with the following properties:
1972
+ 1. Increase sampling period
1973
+ 2. Use sample compression (@observertc/samples-encoder)
1974
+ 3. Filter samples before sending
1975
+ 4. Disable unnecessary detectors
810
1976
 
811
- ```javascript
812
- monitor.addCustomCallEvent({
813
- name: "MY CUSTOM EVENT", // mandatory
1977
+ ### Q: Can I use this with React Native?
814
1978
 
815
- message: "my custom message", // optional;
816
- attachments: JSON.stringify({
817
- value: "my custom value",
818
- }), // optional
819
- timestamp: Date.now(), // optional
820
- value: `simple string value`, // optional
821
- peerConnectionId: "peer-connection-id", // optional;
822
- mediaTrackId: "media-track-id", // optional;
823
- });
824
- ```
1979
+ **A**: The library is designed for web browsers with WebRTC support. For React Native, you'd need WebRTC polyfills and may encounter platform-specific issues.
825
1980
 
826
- ### Extension Stats Event
1981
+ ### Q: How do I handle multiple peer connections?
827
1982
 
828
- You can create an extension state event by calling the `monitor.addExtensionStats()` method. The method takes an object with the following properties:
1983
+ **A**: Just add each peer connection as a source:
829
1984
 
830
1985
  ```javascript
831
- monitor.addExtensionStats({
832
- type: "CLIENT_CPU_STATS",
833
- payload: {
834
- cpuUsage: 0.5,
835
- memoryUsage: 0.3,
836
- },
837
- });
1986
+ const pc1 = new RTCPeerConnection();
1987
+ const pc2 = new RTCPeerConnection();
1988
+
1989
+ monitor.addSource(pc1);
1990
+ monitor.addSource(pc2);
838
1991
  ```
839
1992
 
840
- As a rule of thumb for when to use `addCustomCallEvent` and `addExtensionStateEvent`:
1993
+ ### Q: What happens when a peer connection is closed?
841
1994
 
842
- - Use `addCustomCallEvent` for events that are related to the call itself, such as user clicked a button.
843
- - Use `addExtensionStats` for events that are related to the client's environment, such as CPU usage and collected periodically.
1995
+ **A**: The monitor automatically cleans up associated resources and emits appropriate events. You don't need to manually remove closed connections.
844
1996
 
845
- ## Sampling
1997
+ ### Q: How accurate are the quality scores?
846
1998
 
847
- 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:
1999
+ **A**: Scores are based on standard WebRTC metrics and industry best practices. They provide good relative quality assessment but should be calibrated based on your specific use case and user feedback.
2000
+
2001
+ ### Q: Can I customize which events are included in samples?
2002
+
2003
+ **A**: Yes, you can filter events before sampling or add custom logic in event handlers to control what gets included.
2004
+
2005
+ ### Q: How do I monitor screen sharing vs camera streams differently?
2006
+
2007
+ **A**: Use the `attachments` property to tag tracks:
848
2008
 
849
2009
  ```javascript
850
- monitor.on("sample-created", ({ clientSample, elapsedSinceLastSampleInMs }) => {
851
- console.log("The created client sample is:", clientSample);
852
- console.log("Elapsed time in milliseconds since the last sample was created:", elapsedSinceLastSampleInMs);
853
- });
2010
+ // When adding a screen share track
2011
+ trackMonitor.attachments = { mediaType: "screen-share" };
854
2012
  ```
855
2013
 
856
- 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.
857
-
858
- ## NPM package
2014
+ ### Q: What's the performance impact of monitoring?
859
2015
 
860
- https://www.npmjs.com/package/@observertc/client-monitor-js
2016
+ **A**: The library is designed to be lightweight. Typical overhead is <1% CPU usage. The main cost is the periodic `getStats()` calls, which is why the collection period is configurable.
861
2017
 
862
- ## API docs
2018
+ ## NPM Package
863
2019
 
864
- https://observertc.org/docs/api/client-monitor-js-v2/
2020
+ https://www.npmjs.com/package/@observertc/client-monitor-js
865
2021
 
866
2022
  ## Schemas
867
2023
 
868
- https://github.com/observertc/schemas
2024
+ Schema definitions are available at https://github.com/observertc/schemas
869
2025
 
870
2026
  ## Getting Involved
871
2027
 
872
- Client-monitor is made with the intention to provide an open-source monitoring solution for
873
- WebRTC developers. If you are interested in getting involved
874
- please read our [contribution](CONTRIBUTING.md) guideline.
2028
+ Client-monitor is made with the intention to provide an open-source monitoring solution for WebRTC developers. If you are interested in getting involved, please read our [contribution guidelines](CONTRIBUTING.md).
875
2029
 
876
2030
  ## License
877
2031