@observertc/observer-js 1.0.1-ec27289.0 → 1.1.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 (210) hide show
  1. package/README.md +2184 -1
  2. package/dist/index.d.mts +6842 -0
  3. package/dist/index.d.ts +6842 -0
  4. package/dist/index.js +7736 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +7608 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +44 -18
  9. package/.eslintrc.json +0 -143
  10. package/.prettierignore +0 -5
  11. package/.prettierrc +0 -7
  12. package/jest.config.js +0 -9
  13. package/lib/ObservedCall.d.ts +0 -83
  14. package/lib/ObservedCall.d.ts.map +0 -1
  15. package/lib/ObservedCall.js +0 -203
  16. package/lib/ObservedCallSummary.d.ts +0 -18
  17. package/lib/ObservedCallSummary.d.ts.map +0 -1
  18. package/lib/ObservedCallSummary.js +0 -2
  19. package/lib/ObservedCertificate.d.ts +0 -19
  20. package/lib/ObservedCertificate.d.ts.map +0 -1
  21. package/lib/ObservedCertificate.js +0 -38
  22. package/lib/ObservedClient.d.ts +0 -131
  23. package/lib/ObservedClient.d.ts.map +0 -1
  24. package/lib/ObservedClient.js +0 -520
  25. package/lib/ObservedClientEventMonitor.d.ts +0 -89
  26. package/lib/ObservedClientEventMonitor.d.ts.map +0 -1
  27. package/lib/ObservedClientEventMonitor.js +0 -247
  28. package/lib/ObservedClientSummary.d.ts +0 -23
  29. package/lib/ObservedClientSummary.d.ts.map +0 -1
  30. package/lib/ObservedClientSummary.js +0 -2
  31. package/lib/ObservedCodec.d.ts +0 -22
  32. package/lib/ObservedCodec.d.ts.map +0 -1
  33. package/lib/ObservedCodec.js +0 -45
  34. package/lib/ObservedDataChannel.d.ts +0 -30
  35. package/lib/ObservedDataChannel.d.ts.map +0 -1
  36. package/lib/ObservedDataChannel.js +0 -75
  37. package/lib/ObservedIceCandidate.d.ts +0 -29
  38. package/lib/ObservedIceCandidate.d.ts.map +0 -1
  39. package/lib/ObservedIceCandidate.js +0 -59
  40. package/lib/ObservedIceCandidatePair.d.ts +0 -44
  41. package/lib/ObservedIceCandidatePair.d.ts.map +0 -1
  42. package/lib/ObservedIceCandidatePair.js +0 -111
  43. package/lib/ObservedIceTransport.d.ts +0 -36
  44. package/lib/ObservedIceTransport.d.ts.map +0 -1
  45. package/lib/ObservedIceTransport.js +0 -93
  46. package/lib/ObservedInboundRtp.d.ts +0 -92
  47. package/lib/ObservedInboundRtp.d.ts.map +0 -1
  48. package/lib/ObservedInboundRtp.js +0 -193
  49. package/lib/ObservedInboundTrack.d.ts +0 -31
  50. package/lib/ObservedInboundTrack.d.ts.map +0 -1
  51. package/lib/ObservedInboundTrack.js +0 -59
  52. package/lib/ObservedMediaPlayout.d.ts +0 -22
  53. package/lib/ObservedMediaPlayout.d.ts.map +0 -1
  54. package/lib/ObservedMediaPlayout.js +0 -42
  55. package/lib/ObservedMediaSource.d.ts +0 -28
  56. package/lib/ObservedMediaSource.d.ts.map +0 -1
  57. package/lib/ObservedMediaSource.js +0 -55
  58. package/lib/ObservedOutboundRtp.d.ts +0 -62
  59. package/lib/ObservedOutboundRtp.d.ts.map +0 -1
  60. package/lib/ObservedOutboundRtp.js +0 -144
  61. package/lib/ObservedOutboundTrack.d.ts +0 -31
  62. package/lib/ObservedOutboundTrack.d.ts.map +0 -1
  63. package/lib/ObservedOutboundTrack.js +0 -59
  64. package/lib/ObservedPeerConnection.d.ts +0 -185
  65. package/lib/ObservedPeerConnection.d.ts.map +0 -1
  66. package/lib/ObservedPeerConnection.js +0 -710
  67. package/lib/ObservedPeerConnectionTransport.d.ts +0 -17
  68. package/lib/ObservedPeerConnectionTransport.d.ts.map +0 -1
  69. package/lib/ObservedPeerConnectionTransport.js +0 -34
  70. package/lib/ObservedRemoteInboundRtp.d.ts +0 -30
  71. package/lib/ObservedRemoteInboundRtp.d.ts.map +0 -1
  72. package/lib/ObservedRemoteInboundRtp.js +0 -60
  73. package/lib/ObservedRemoteOutboundRtp.d.ts +0 -30
  74. package/lib/ObservedRemoteOutboundRtp.d.ts.map +0 -1
  75. package/lib/ObservedRemoteOutboundRtp.js +0 -60
  76. package/lib/ObservedTURN.d.ts +0 -31
  77. package/lib/ObservedTURN.d.ts.map +0 -1
  78. package/lib/ObservedTURN.js +0 -58
  79. package/lib/ObservedTurnServer.d.ts +0 -25
  80. package/lib/ObservedTurnServer.d.ts.map +0 -1
  81. package/lib/ObservedTurnServer.js +0 -59
  82. package/lib/Observer.d.ts +0 -61
  83. package/lib/Observer.d.ts.map +0 -1
  84. package/lib/Observer.js +0 -156
  85. package/lib/ObserverSummary.d.ts +0 -13
  86. package/lib/ObserverSummary.d.ts.map +0 -1
  87. package/lib/ObserverSummary.js +0 -2
  88. package/lib/common/Middleware.d.ts +0 -17
  89. package/lib/common/Middleware.d.ts.map +0 -1
  90. package/lib/common/Middleware.js +0 -60
  91. package/lib/common/SingleExecutor.d.ts +0 -3
  92. package/lib/common/SingleExecutor.d.ts.map +0 -1
  93. package/lib/common/SingleExecutor.js +0 -31
  94. package/lib/common/logger.d.ts +0 -17
  95. package/lib/common/logger.d.ts.map +0 -1
  96. package/lib/common/logger.js +0 -50
  97. package/lib/common/types.d.ts +0 -3
  98. package/lib/common/types.d.ts.map +0 -1
  99. package/lib/common/types.js +0 -3
  100. package/lib/common/utils.d.ts +0 -11
  101. package/lib/common/utils.d.ts.map +0 -1
  102. package/lib/common/utils.js +0 -63
  103. package/lib/detectors/Detector.d.ts +0 -5
  104. package/lib/detectors/Detector.d.ts.map +0 -1
  105. package/lib/detectors/Detector.js +0 -3
  106. package/lib/detectors/Detectors.d.ts +0 -11
  107. package/lib/detectors/Detectors.d.ts.map +0 -1
  108. package/lib/detectors/Detectors.js +0 -34
  109. package/lib/index.d.ts +0 -27
  110. package/lib/index.d.ts.map +0 -1
  111. package/lib/index.js +0 -47
  112. package/lib/monitors/TurnUsageMonitor.d.ts +0 -1
  113. package/lib/monitors/TurnUsageMonitor.d.ts.map +0 -1
  114. package/lib/monitors/TurnUsageMonitor.js +0 -146
  115. package/lib/schema/ClientEventTypes.d.ts +0 -100
  116. package/lib/schema/ClientEventTypes.d.ts.map +0 -1
  117. package/lib/schema/ClientEventTypes.js +0 -38
  118. package/lib/schema/ClientMetaTypes.d.ts +0 -34
  119. package/lib/schema/ClientMetaTypes.d.ts.map +0 -1
  120. package/lib/schema/ClientMetaTypes.js +0 -16
  121. package/lib/schema/ClientSample.d.ts +0 -1333
  122. package/lib/schema/ClientSample.d.ts.map +0 -1
  123. package/lib/schema/ClientSample.js +0 -4
  124. package/lib/scores/CalculatedScore.d.ts +0 -6
  125. package/lib/scores/CalculatedScore.d.ts.map +0 -1
  126. package/lib/scores/CalculatedScore.js +0 -2
  127. package/lib/scores/DefaultCallScoreCalculator.d.ts +0 -7
  128. package/lib/scores/DefaultCallScoreCalculator.d.ts.map +0 -1
  129. package/lib/scores/DefaultCallScoreCalculator.js +0 -21
  130. package/lib/scores/ScoreCalculator.d.ts +0 -4
  131. package/lib/scores/ScoreCalculator.d.ts.map +0 -1
  132. package/lib/scores/ScoreCalculator.js +0 -2
  133. package/lib/updaters/CallUpdater.d.ts +0 -5
  134. package/lib/updaters/CallUpdater.d.ts.map +0 -1
  135. package/lib/updaters/CallUpdater.js +0 -2
  136. package/lib/updaters/ObserverUpdater.d.ts +0 -5
  137. package/lib/updaters/ObserverUpdater.d.ts.map +0 -1
  138. package/lib/updaters/ObserverUpdater.js +0 -2
  139. package/lib/updaters/OnAllCallObserverUpdater_.d.ts +0 -13
  140. package/lib/updaters/OnAllCallObserverUpdater_.d.ts.map +0 -1
  141. package/lib/updaters/OnAllCallObserverUpdater_.js +0 -38
  142. package/lib/updaters/OnAllClientCallUpdater.d.ts +0 -13
  143. package/lib/updaters/OnAllClientCallUpdater.d.ts.map +0 -1
  144. package/lib/updaters/OnAllClientCallUpdater.js +0 -37
  145. package/lib/updaters/OnAnyCallObserverUpdater_.d.ts +0 -11
  146. package/lib/updaters/OnAnyCallObserverUpdater_.d.ts.map +0 -1
  147. package/lib/updaters/OnAnyCallObserverUpdater_.js +0 -27
  148. package/lib/updaters/OnAnyClientCallUpdater.d.ts +0 -11
  149. package/lib/updaters/OnAnyClientCallUpdater.d.ts.map +0 -1
  150. package/lib/updaters/OnAnyClientCallUpdater.js +0 -27
  151. package/lib/updaters/OnIntervalUpdater.d.ts +0 -10
  152. package/lib/updaters/OnIntervalUpdater.d.ts.map +0 -1
  153. package/lib/updaters/OnIntervalUpdater.js +0 -17
  154. package/lib/updaters/Updater.d.ts +0 -6
  155. package/lib/updaters/Updater.d.ts.map +0 -1
  156. package/lib/updaters/Updater.js +0 -2
  157. package/lib/utils/RemoteTrackResolver.d.ts +0 -7
  158. package/lib/utils/RemoteTrackResolver.d.ts.map +0 -1
  159. package/lib/utils/RemoteTrackResolver.js +0 -2
  160. package/src/ObservedCall.ts +0 -322
  161. package/src/ObservedCallSummary.ts +0 -22
  162. package/src/ObservedCertificate.ts +0 -43
  163. package/src/ObservedClient.ts +0 -673
  164. package/src/ObservedClientEventMonitor.ts +0 -301
  165. package/src/ObservedClientSummary.ts +0 -24
  166. package/src/ObservedCodec.ts +0 -49
  167. package/src/ObservedDataChannel.ts +0 -85
  168. package/src/ObservedIceCandidate.ts +0 -64
  169. package/src/ObservedIceCandidatePair.ts +0 -118
  170. package/src/ObservedIceTransport.ts +0 -99
  171. package/src/ObservedInboundRtp.ts +0 -205
  172. package/src/ObservedInboundTrack.ts +0 -73
  173. package/src/ObservedMediaPlayout.ts +0 -49
  174. package/src/ObservedMediaSource.ts +0 -60
  175. package/src/ObservedOutboundRtp.ts +0 -156
  176. package/src/ObservedOutboundTrack.ts +0 -72
  177. package/src/ObservedPeerConnection.ts +0 -1019
  178. package/src/ObservedPeerConnectionTransport.ts +0 -38
  179. package/src/ObservedRemoteInboundRtp.ts +0 -64
  180. package/src/ObservedRemoteOutboundRtp.ts +0 -65
  181. package/src/ObservedTURN.ts +0 -86
  182. package/src/ObservedTurnServer.ts +0 -72
  183. package/src/Observer.ts +0 -239
  184. package/src/ObserverSummary.ts +0 -15
  185. package/src/common/Middleware.ts +0 -84
  186. package/src/common/SingleExecutor.ts +0 -36
  187. package/src/common/logger.ts +0 -82
  188. package/src/common/types.ts +0 -4
  189. package/src/common/utils.ts +0 -69
  190. package/src/detectors/Detector.ts +0 -6
  191. package/src/detectors/Detectors.ts +0 -38
  192. package/src/index.ts +0 -28
  193. package/src/monitors/TurnUsageMonitor.ts +0 -187
  194. package/src/schema/ClientEventTypes.ts +0 -112
  195. package/src/schema/ClientMetaTypes.ts +0 -41
  196. package/src/schema/ClientSample.ts +0 -1682
  197. package/src/scores/CalculatedScore.ts +0 -6
  198. package/src/scores/DefaultCallScoreCalculator.ts +0 -23
  199. package/src/scores/ScoreCalculator.ts +0 -3
  200. package/src/updaters/CallUpdater.ts +0 -5
  201. package/src/updaters/ObserverUpdater.ts +0 -5
  202. package/src/updaters/OnAllCallObserverUpdater_.ts +0 -46
  203. package/src/updaters/OnAllClientCallUpdater.ts +0 -46
  204. package/src/updaters/OnAnyCallObserverUpdater_.ts +0 -32
  205. package/src/updaters/OnAnyClientCallUpdater.ts +0 -32
  206. package/src/updaters/OnIntervalUpdater.ts +0 -19
  207. package/src/updaters/Updater.ts +0 -5
  208. package/src/utils/RemoteTrackResolver.ts +0 -12
  209. package/tsconfig.json +0 -19
  210. package/tslint.json +0 -6
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/common/logger.ts","../src/ObservedCall.ts","../src/ObservedClient.ts","../src/ObservedPeerConnection.ts","../src/utils/stats.ts","../src/ObservedInboundRtp.ts","../src/ObservedOutboundRtp.ts","../src/ObservedCertificate.ts","../src/ObservedCodec.ts","../src/ObservedDataChannel.ts","../src/ObservedIceCandidate.ts","../src/ObservedIceCandidatePair.ts","../src/ObservedIceTransport.ts","../src/ObservedMediaSource.ts","../src/ObservedPeerConnectionTransport.ts","../src/ObservedMediaPlayout.ts","../src/ObservedRemoteInboundRtp.ts","../src/ObservedRemoteOutboundRtp.ts","../src/ObservedInboundTrack.ts","../src/common/utils.ts","../src/ObservedOutboundTrack.ts","../src/schema/ClientEventTypes.ts","../src/schema/ClientMetaTypes.ts","../src/issues/ActiveClientIssue.ts","../src/scores/DefaultCallScoreCalculator.ts","../src/detectors/Detectors.ts","../src/issues/ObservedClientIssueRegistry.ts","../src/detectors/UnconsumedTrackDetector.ts","../src/detectors/TrackDeliveryMismatchDetector.ts","../src/detectors/IssueConclusion.ts","../src/detectors/CallConcurrentIssueDetector.ts","../src/detectors/IssueFanOutDetector.ts","../src/detectors/PublisherFaultCorroborationDetector.ts","../src/Observer.ts","../src/ObservedTURN.ts","../src/ObservedTurnServer.ts","../src/common/Middleware.ts","../src/ObservedMediasoupRouter.ts","../src/issues/ActiveIssuesRegistry.ts","../src/summaries/CallSummary.ts","../src/summaries/CallSummaryCollector.ts","../src/detectors/SfuCongestionDetector.ts","../src/detectors/ObserverConcurrentIssueDetector.ts","../src/utils/geohash.ts","../src/detectors/ClientPopulationIssueDetector.ts","../src/detectors/TurnServerHealthDetector.ts","../src/detectors/TurnServerOutageDetector.ts","../src/utils/SlidingWindow.ts","../src/validators/SimulcastReceiverValidator.ts","../src/validators/RemoteTrackResolverValidator.ts","../src/validators/CodecConsistencyValidator.ts","../src/schema/ClientSample.ts","../src/utils/CallHealthAggregator.ts","../src/common/Issue.ts","../src/utils/TrendTester.ts","../src/sinks/ClientSampleSink.ts","../src/sinks/JsonlFileSink.ts","../src/sinks/InMemorySink.ts","../src/resolvers/RemoteTrackResolver.ts","../src/resolvers/RemoteTrackResolverFactories.ts"],"sourcesContent":["export type { ObserverEvents, SampleRejectedReason, AcceptContext, CallAppDataFactory, ClientAppDataFactory, AcceptMiddleware, AcceptMiddlewarePayload } from './Observer';\nexport type {\n\tObserverEventBase,\n\tObservedCallScope,\n\tObservedClientScope,\n\tObservedPeerConnectionScope,\n\tObservedMediasoupRouterScope,\n} from './ObserverEvents';\nexport type {\n\tObservedMediasoupRouterSettings,\n\tObservedMediasoupRouterEvents,\n\tMediasoupSampleEnricher,\n} from './ObservedMediasoupRouter';\nexport type {\n\tMediasoupRouterSample,\n\tMediasoupTransportSample,\n\tMediasoupProducerSample,\n\tMediasoupConsumerSample,\n\tMediasoupDataProducerSample,\n\tMediasoupDataConsumerSample,\n\tMediasoupWebRtcTransportSample,\n\tMediasoupPlainTransportSample,\n\tMediasoupPipeTransportSample,\n\tMediasoupDirectTransportSample,\n\tMediasoupWebRtcTransportSampleEventMap,\n\tMediasoupPlainTransportSampleEventMap,\n\tMediasoupPipeTransportSampleEventMap,\n\tMediasoupDirectTransportSampleEventMap,\n\tMediasoupProducerSampleEvent,\n\tMediasoupConsumerSampleEvent,\n} from './schema/MediasoupRouter';\n\nexport { Observer } from './Observer';\nexport { ObservedCall } from './ObservedCall';\n// The settings object `createObservedCall` / `getOrCreateObservedCall` take.\nexport type { ObservedCallSettings } from './ObservedCall';\nexport { ObservedInboundTrack } from './ObservedInboundTrack';\nexport { ObservedOutboundTrack } from './ObservedOutboundTrack';\nexport { ObservedClient } from './ObservedClient';\n// The settings object `createObservedClient` / `getOrCreateObservedClient` take.\nexport type { ObservedClientSettings } from './ObservedClient';\nexport { ObservedPeerConnection } from './ObservedPeerConnection';\nexport { ObservedMediaSource } from './ObservedMediaSource';\nexport { ObservedMediaPlayout } from './ObservedMediaPlayout';\nexport { ObservedCodec } from './ObservedCodec';\nexport { ObservedCertificate } from './ObservedCertificate';\nexport { ObservedDataChannel } from './ObservedDataChannel';\nexport { ObservedInboundRtp } from './ObservedInboundRtp';\nexport { ObservedOutboundRtp } from './ObservedOutboundRtp';\nexport { ObservedRemoteInboundRtp } from './ObservedRemoteInboundRtp';\nexport { ObservedRemoteOutboundRtp } from './ObservedRemoteOutboundRtp';\nexport { ObservedMediasoupRouter } from './ObservedMediasoupRouter';\nexport { ObservedIceCandidatePair } from './ObservedIceCandidatePair';\nexport { ObservedIceCandidate } from './ObservedIceCandidate';\nexport { ObservedIceTransport } from './ObservedIceTransport';\n// Both are reachable from public fields: `ObservedPeerConnection.observedTurnServer`, and the\n// TURN view a call/observer keeps.\nexport { ObservedTurnServer } from './ObservedTurnServer';\nexport { ObservedTURN } from './ObservedTURN';\nexport type { ObservedTURNEventMap } from './ObservedTURN';\nexport { ObservedPeerConnectionTransport } from './ObservedPeerConnectionTransport';\nexport { ClientEventTypes } from './schema/ClientEventTypes';\nexport { ClientMetaTypes } from './schema/ClientMetaTypes';\nexport { ClientSample, ClientIssue, ClientEvent, ClientMetaData } from './schema/ClientSample';\n// The rest of the sample schema. These appear in public signatures (`ObservedInboundRtp.stats`,\n// `injectExtensionStat`, the `client-extension-stats` payload, …), so a consumer writing a typed\n// helper against one needs to be able to name it. `schemaVersion` says which generation of the\n// schema this build speaks.\nexport { schemaVersion } from './schema/ClientSample';\nexport type {\n\tExtensionStat,\n\tPeerConnectionSample,\n\tInboundTrackSample,\n\tOutboundTrackSample,\n\tInboundRtpStats,\n\tOutboundRtpStats,\n\tRemoteInboundRtpStats,\n\tRemoteOutboundRtpStats,\n\tQualityLimitationDurations,\n\tPsnrSum,\n\tCodecStats,\n\tDataChannelStats,\n\tMediaSourceStats,\n\tMediaPlayoutStats,\n\tPeerConnectionTransportStats,\n\tIceTransportStats,\n\tIceCandidateStats,\n\tIceCandidatePairStats,\n\tCertificateStats,\n} from './schema/ClientSample';\nexport { ScoreCalculator } from './scores/ScoreCalculator';\nexport type { Detector } from './detectors/Detector';\nexport { Detectors } from './detectors/Detectors';\nexport type {\n\tAvailableDetectorsConfigs,\n\tAvailableCallScopeDetectorsConfigs,\n\tAvailableObserverScopeDetectorsConfigs,\n} from './detectors/Detectors';\nexport {\n\tCallHealthAggregator,\n\tdefaultClientHealthThresholds,\n} from './utils/CallHealthAggregator';\nexport type {\n\tCallHealth,\n\tClientHealth,\n\tClientHealthThresholds,\n} from './utils/CallHealthAggregator';\n// Detectors are never created implicitly. Register observer-scoped ones with\n// `observer.addObserverDetector(name, config)` and call-scoped ones with\n// `observer.addCallDetector(name, config)` (stored in `observer.callDetectorConfigs`, applied to\n// every call the observer creates) or `observedCall.addDetector(name, config)` for one call.\n// Issue-driven correlation: the client reports WHAT is wrong for itself, the observer correlates\n// WHO ELSE is in the same state and WHERE in publisher -> SFU -> subscriber the fault sits.\n// Issues are PUSHED to whoever registered for them rather than scanned for; a detector implements\n// `ActiveIssueTracker` and is fed the types it consumes.\nexport { ActiveIssuesRegistry } from './issues/ActiveIssuesRegistry';\nexport { ObservedClientIssueRegistry } from './issues/ObservedClientIssueRegistry';\nexport type { ActiveIssueTracker } from './issues/ActiveIssueTracker';\nexport {\n\tRESOLVED_ISSUE_SUFFIX,\n\tbaseIssueType,\n\tisClientIssueResolutionEntry,\n} from './issues/ActiveClientIssue';\nexport type { ActiveClientIssue, ResolvedActiveClientIssue } from './issues/ActiveClientIssue';\n// Server-raised findings. Unlike `ClientIssue` (a wire type, string payload) these are delivered to\n// an in-process handler, so the payload is the object itself — no stringify/parse. `CallIssue` and\n// `ObserverIssue` differ by the scope that raised them, which each carries as `scope`.\nexport { issuePayloadAsString } from './common/Issue';\nexport type { Issue, IssueBase, CallIssue, ObserverIssue } from './common/Issue';\n// \"Is this meeting in trouble?\" and \"is our infrastructure in trouble?\" are separate detectors with\n// separate gates and separate findings — not one detector branching on what it was handed.\nexport {\n\tCallConcurrentIssueDetector,\n\tCallConcurrentIssueTypes,\n} from './detectors/CallConcurrentIssueDetector';\nexport type {\n\tCallConcurrentIssueDetectorConfig,\n\tCallConcurrentIssueGroup,\n} from './detectors/CallConcurrentIssueDetector';\nexport {\n\tObserverConcurrentIssueDetector,\n\tObserverConcurrentIssueTypes,\n} from './detectors/ObserverConcurrentIssueDetector';\nexport type {\n\tObserverConcurrentIssueDetectorConfig,\n\tObserverConcurrentIssueGroup,\n} from './detectors/ObserverConcurrentIssueDetector';\nexport {\n\tIssueFanOutDetector,\n\tIssueFanOutTypes,\n} from './detectors/IssueFanOutDetector';\nexport type { IssueFanOutDetectorConfig } from './detectors/IssueFanOutDetector';\n// Both ends of one published track complaining at once — the source is implicated, not inferred.\nexport {\n\tPublisherFaultCorroborationDetector,\n\tPublisherFaultTypes,\n} from './detectors/PublisherFaultCorroborationDetector';\nexport type {\n\tPublisherFaultCorroborationDetectorConfig,\n\tCorroboratedPublisherFault,\n} from './detectors/PublisherFaultCorroborationDetector';\n// The one correlation that is neither per-call nor per-server: an issue concentrated on one browser,\n// one browser version or one OS. Endpoint symptoms spread across unrelated calls implicate the\n// clients, not the SFU.\nexport {\n\tClientPopulationIssueDetector,\n\tClientPopulationIssueTypes,\n} from './detectors/ClientPopulationIssueDetector';\nexport type {\n\tClientPopulationIssueDetectorConfig,\n\tClientPopulationAxis,\n\tClientLocationResolver,\n\tClientPopulation,\n} from './detectors/ClientPopulationIssueDetector';\n// Geographic grouping: a grid cell key, not a radius — see the module for why.\nexport { geohash, GEOHASH_CELL_SIZES } from './utils/geohash';\nexport type { ClientLocation } from './utils/geohash';\nexport { isPublicIpAddress } from './common/utils';\nexport {\n\tSfuCongestionDetector,\n} from './detectors/SfuCongestionDetector';\nexport type {\n\tSfuCongestionDetectorConfig,\n\tSfuCongestionDetectorBucket,\n\tSfuCongestionDetectorReport,\n\tSfuCongestionDetectorEvaluation,\n} from './detectors/SfuCongestionDetector';\nexport {\n\tTurnServerHealthDetector,\n\tTurnServerHealthTypes,\n} from './detectors/TurnServerHealthDetector';\nexport type { TurnServerHealthDetectorConfig, TurnServerHealth } from './detectors/TurnServerHealthDetector';\nexport {\n\tTurnServerOutageDetector,\n\tTurnServerOutageTypes,\n} from './detectors/TurnServerOutageDetector';\nexport type { TurnServerOutageDetectorConfig } from './detectors/TurnServerOutageDetector';\nexport {\n\tTrackDeliveryMismatchDetector,\n\tTrackDeliveryMismatchTypes,\n} from './detectors/TrackDeliveryMismatchDetector';\nexport type { TrackDeliveryMismatchDetectorConfig } from './detectors/TrackDeliveryMismatchDetector';\nexport {\n\tUnconsumedTrackDetector,\n\tUnconsumedTrackTypes,\n} from './detectors/UnconsumedTrackDetector';\nexport type { UnconsumedTrackDetectorConfig } from './detectors/UnconsumedTrackDetector';\n// NOTE there is no ICE detector here. ICE trouble is reported by client-monitor-js >= 4.6.0 as the\n// keyed issues `ice-disconnected` / `ice-connection-failed` / `ice-transport-stalled` /\n// `unstable-ice-path`, with hysteresis behind each verdict. Correlating them is a matter of config,\n// not of another class:\n//\n// observer.addObserverDetector('observer-concurrent-issue-detector', {\n// issueTypes: [ 'ice-disconnected', 'ice-connection-failed', 'ice-transport-stalled' ],\n// });\n// Validators: one-shot structural checks. A detector answers \"is something wrong right now?\" and runs\n// forever; a validator answers \"is this deployment built correctly?\", reports once, and is dropped.\n// Start one with `observer.addValidator(name, config)`.\nexport type { Validator, RunningValidator, ValidationReport } from './validators/Validator';\nexport type { AvailableValidatorConfigs, ValidatorName } from './validators/Validators';\nexport {\n\tSimulcastReceiverValidator,\n\tLOWEST_COMMON_DENOMINATOR_ISSUE,\n} from './validators/SimulcastReceiverValidator';\nexport type {\n\tSimulcastReceiverValidatorConfig,\n\tSimulcastReceiverReportPayload,\n\tSimulcastReceiverEvidence,\n} from './validators/SimulcastReceiverValidator';\nexport {\n\tRemoteTrackResolverValidator,\n\tUNRESOLVED_TRACK_LINKS_ISSUE,\n} from './validators/RemoteTrackResolverValidator';\nexport type {\n\tRemoteTrackResolverValidatorConfig,\n\tRemoteTrackResolverReportPayload,\n\tRemoteTrackLinkEvidence,\n} from './validators/RemoteTrackResolverValidator';\nexport {\n\tCodecConsistencyValidator,\n\tCODEC_MISMATCH_ISSUE,\n} from './validators/CodecConsistencyValidator';\nexport type {\n\tCodecConsistencyValidatorConfig,\n\tCodecConsistencyReportPayload,\n\tCodecEvidence,\n} from './validators/CodecConsistencyValidator';\n// Call summaries: the record of what happened in a call, accumulated over its life and delivered on\n// `call-summary` when it closes. Opt-in with `new Observer({ callSummary: { ... } })`; an absent\n// section means \"not collected\", never \"nothing happened\".\nexport { createCallSummary, defaultCallSummaryConfig } from './summaries/CallSummary';\nexport type {\n\tCallSummary,\n\tCallSummaryConfig,\n\tCallSummarySection,\n\tCallSummaryEnrichers,\n\tCallSummaryEnricher,\n\tCallScopedEventName,\n\tCallSummaryClients,\n\tCallSummaryTurnServers,\n\tCallSummaryScores,\n\tCallSummaryTruncation,\n} from './summaries/CallSummary';\nexport { CallSummaryCollector } from './summaries/CallSummaryCollector';\n// The interpretation layer: what a correlated cohort implies, and where to look.\nexport { concludeCallIssue, concludeObserverIssue } from './detectors/IssueConclusion';\nexport type {\n\tIssueConclusion,\n\tIssueFaultDomain,\n\tCallIssueSpread,\n\tObserverIssueSpread,\n} from './detectors/IssueConclusion';\n// Statistics helpers for building your own detectors.\nexport {\n\tpercentile,\n\tpercentileOfSorted,\n\tmedian,\n\tmedianAbsoluteDeviation,\n\trobustZScore,\n\tsummarize,\n\tcounterDelta,\n\tcorrelation,\n\tpageHinkley,\n\tmannKendall,\n\tmannKendallVerdict,\n} from './utils/stats';\nexport type { StatsSummary, PageHinkleyResult, MannKendallResult } from './utils/stats';\nexport { SlidingWindow } from './utils/SlidingWindow';\nexport type { SlidingWindowEntry } from './utils/SlidingWindow';\nexport { TrendTester } from './utils/TrendTester';\nexport type { TrendTesterConfig } from './utils/TrendTester';\nexport { createLogger, setObserverLogger } from './common/logger';\nexport type { Logger, ObserverLogger } from './common/logger';\n// Per-client sample sinks. `ClientSampleSink` is the base class to subclass for a custom\n// destination; `JsonlFileSink` / `InMemorySink` are the built-ins.\nexport {\n\tClientSampleSink,\n\tJsonlFileSink,\n\tcreateJsonlFileSink,\n\tcreateJsonlFileSinkFactory,\n\tInMemorySink,\n\tcreateInMemorySink,\n} from './sinks';\nexport type {\n\tClientSampleSinkEvents,\n\tClientSampleSinkFactory,\n\tJsonlFileSinkOptions,\n\tJsonlFileSinkFactoryOptions,\n} from './sinks';\nexport { Middleware } from './common/Middleware';\n// `Observer.acceptMiddlewares` is a public field of this type, so the type has to be nameable.\nexport { MiddlewareProcessor } from './common/Middleware';\n// `kind` on every track/RTP/media entity, and the `calculatedScore` every scored entity carries.\nexport type { MediaKind } from './common/types';\nexport type { CalculatedScore } from './scores/CalculatedScore';\n// Remote track correlation: the generic resolver + built-in strategy factories.\nexport { RemoteTrackResolver } from './resolvers/RemoteTrackResolver';\nexport type { RemoteTrackResolvers, RemoteTrackResolverFactory } from './resolvers/RemoteTrackResolver';\nexport {\n\tcreateDefaultMediasoupRemoteTrackResolverFactory,\n\tcreateP2pRemoteTrackResolverFactory,\n} from './resolvers/RemoteTrackResolverFactories';\n","/**\n * What each internal module holds, obtained via {@link createLogger}.\n *\n * Every level method is variadic and `console`-style: a printf-like format string\n * (`%s`, `%d`, `%o`, `%j`) followed by its substitution values, and sometimes a\n * trailing object. Calls are forwarded to the process-wide {@link ObserverLogger}\n * with the module name prepended as the first argument.\n */\nexport interface Logger {\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\ttrace(...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\tdebug(...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\tinfo(...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\twarn(...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\terror(...args: any[]): void;\n}\n\n/**\n * The single process-wide sink every {@link Logger} forwards to, installed with\n * {@link setObserverLogger}.\n *\n * **Argument order:** the originating module name is a separate *leading* argument —\n * it is never part of the message. What follows is the `console`-style format string\n * and its substitution values:\n *\n * ```ts\n * observerLogger.warn('ObservedPeerConnection', 'Received sample without callId. %o', sample);\n * // ^ module ^ format string ^ values\n * ```\n *\n * **Pitfall:** do not spread those arguments into a logger whose first parameter is the\n * message (pino, bunyan, roarr). `log.warn(module, ...args)` makes the module name the\n * entire message, and the real format string and its values are then consumed as\n * interpolation values for a string with no placeholders — i.e. silently dropped. Pass\n * the module as structured metadata (`log.warn({ module }, ...args)`) or collapse the\n * arguments yourself with `util.format`. `console` is fine either way.\n *\n * See `docs/logging.md` for pino / winston / console recipes.\n */\nexport interface ObserverLogger {\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\ttrace(module: string, ...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\tdebug(module: string, ...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\tinfo(module: string, ...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\twarn(module: string, ...args: any[]): void;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\terror(module: string, ...args: any[]): void;\n}\n\nlet mainLogger: ObserverLogger = new class implements ObserverLogger {\n\ttrace = () => void 0;\n\t// trace(module: string, ...args: any[]) {\n\t// \t// eslint-disable-next-line no-console\n\t// \tconsole.log(`[TRACE] ${module}`, ...args);\n\t// }\n\n\tdebug(module: string, ...args: any[]) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.log(`[DEBUG] ${module}`, ...args);\n\t}\n\n\tinfo(module: string, ...args: any[]) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.info(`[INFO] ${module}`, ...args);\n\t}\n\n\twarn(module: string, ...args: any[]) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.warn(`[WARN] ${module}`, ...args);\n\t}\n\n\terror(module: string, ...args: any[]) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.error(`[ERROR] ${module}`, ...args);\n\t}\n}();\n\n/**\n * Create a {@link Logger} bound to `moduleName`. Every call forwards to the process-wide\n * {@link ObserverLogger}, passing `moduleName` as the first argument. Exported so host\n * applications can route their own modules through the same sink.\n */\nexport function createLogger(moduleName: string): Logger {\n\treturn new class implements Logger {\n\t\ttrace(...args: any[]) {\n\t\t\tmainLogger.trace(moduleName, ...args);\n\t\t}\n\n\t\tdebug(...args: any[]) {\n\t\t\tmainLogger.debug(moduleName, ...args);\n\t\t}\n\n\t\tinfo(...args: any[]) {\n\t\t\tmainLogger.info(moduleName, ...args);\n\t\t}\n\n\t\twarn(...args: any[]) {\n\t\t\tmainLogger.warn(moduleName, ...args);\n\t\t}\n\n\t\terror(...args: any[]) {\n\t\t\tmainLogger.error(moduleName, ...args);\n\t\t}\n\n\t}();\n}\n\n/**\n * Replace the process-wide log sink. Global and last-wins: one call reroutes all logging\n * from every `Observer` instance in the process. The sink is read on every log call, so\n * this can be called at any time, before or after constructing an `Observer`.\n *\n * Mind the argument order of {@link ObserverLogger} — the module name comes first.\n */\nexport function setObserverLogger(logger: ObserverLogger) {\n\tmainLogger = logger;\n}\n","import { EventEmitter } from 'events';\nimport { createLogger } from './common/logger';\nimport { ObservedClient, ObservedClientEvents, ObservedClientSettings } from './ObservedClient';\nimport { Observer } from './Observer';\nimport { ScoreCalculator } from './scores/ScoreCalculator';\nimport { CalculatedScore } from './scores/CalculatedScore';\nimport { DefaultCallScoreCalculator } from './scores/DefaultCallScoreCalculator';\nimport { RemoteTrackResolver } from './resolvers/RemoteTrackResolver';\nimport type { ObservedOutboundTrack } from './ObservedOutboundTrack';\nimport { AvailableCallScopeDetectorsConfigs, Detectors } from './detectors/Detectors';\nimport type { CallIssue } from './common/Issue';\nimport type { CallSummary } from './summaries/CallSummary';\nimport type { AcceptContext } from './Observer';\nimport type { ObserverEvents, ObservedCallScope } from './ObserverEvents';\nimport { ActiveIssuesRegistry } from './issues/ActiveIssuesRegistry';\nimport { ObservedClientIssueRegistry } from './issues/ObservedClientIssueRegistry';\nimport { Detector } from './detectors/Detector';\nimport { UnconsumedTrackDetector } from './detectors/UnconsumedTrackDetector';\nimport { TrackDeliveryMismatchDetector } from './detectors/TrackDeliveryMismatchDetector';\nimport { CallConcurrentIssueDetector } from './detectors/CallConcurrentIssueDetector';\nimport { IssueFanOutDetector } from './detectors/IssueFanOutDetector';\nimport { PublisherFaultCorroborationDetector } from './detectors/PublisherFaultCorroborationDetector';\n\nconst logger = createLogger('ObservedCall');\n\nexport type ObservedCallSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {\n\tcallId: string;\n\tappData?: AppData;\n\tcloseCallIfEmptyForMs?: number,\n\n\t/**\n\t * When `true`, the call's `update()` is invoked whenever a client accepts a sample. When `false`, it is not.\n\t *\n\t * DEFAULT: `true` — the call is updated on every client sample, which is the most common use case.\n\t */\n\tautoUpdateOnClientUpdate?: boolean;\n};\n\nexport type ObservedCallEvents = {\n\tupdate: [],\n\tnewclient: [ObservedClient],\n\tempty: [],\n\t'not-empty': [],\n\tclose: [],\n}\n\nexport declare interface ObservedCall {\n\ton<U extends keyof ObservedCallEvents>(event: U, listener: (...args: ObservedCallEvents[U]) => void): this;\n\toff<U extends keyof ObservedCallEvents>(event: U, listener: (...args: ObservedCallEvents[U]) => void): this;\n\tonce<U extends keyof ObservedCallEvents>(event: U, listener: (...args: ObservedCallEvents[U]) => void): this;\n\temit<U extends keyof ObservedCallEvents>(event: U, ...args: ObservedCallEvents[U]): boolean;\n}\n\nexport class ObservedCall<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter {\n\tpublic scoreCalculator: ScoreCalculator;\n\tpublic readonly detectors: Detectors;\n\tpublic readonly callId: string;\n\tpublic readonly observedClients = new Map<string, ObservedClient>();\n\tpublic readonly clientsUsedTurn = new Set<string>();\n\tpublic readonly calculatedScore: CalculatedScore = {\n\t\tweight: 1,\n\t\tvalue: undefined,\n\t};\n\tpublic remoteTrackResolver?: RemoteTrackResolver;\n\n\t/**\n\t * The accumulating record of this call's life, or `undefined` when no summary was configured.\n\t *\n\t * Live — read it at any point during the call. It is also delivered once on `call-summary` when\n\t * the call closes. See `CallSummary`: an absent section means \"not collected\", never \"nothing\n\t * happened\".\n\t */\n\tpublic summary?: CallSummary;\n\n\t/**\n\t * Published tracks that currently have **no** subscriber linked to them.\n\t *\n\t * Maintained by the `RemoteTrackResolver` at the exact moments a track gains or loses its last\n\t * subscriber — the only moments the answer can change. `UnconsumedTrackDetector` reads this\n\t * instead of walking every published track in the call, so in a healthy call (where the set is\n\t * empty) it does no work at all.\n\t *\n\t * Empty when no resolver is configured: without links, \"no subscribers\" is unknowable.\n\t */\n\tpublic readonly unconsumedOutboundTracks = new Set<ObservedOutboundTrack>();\n\n\tpublic totalAddedClients = 0;\n\tpublic totalRemovedClients = 0;\n\n\tpublic numberOfIssues = 0;\n\tpublic numberOfPeerConnections = 0;\n\tpublic numberOfInboundRtpStreams = 0;\n\tpublic numberOfOutboundRtpStreams = 0;\n\tpublic numberOfDataChannels = 0;\n\n\tpublic maxNumberOfClients = 0;\n\tpublic deltaNumberOfIssues = 0;\n\n\t// public deltaRttLt50Measurements = 0;\n\t// public deltaRttLt150Measurements = 0;\n\t// public deltaRttLt300Measurements = 0;\n\t// public deltaRttGtOrEq300Measurements = 0;\n\n\tpublic appData: AppData;\n\tpublic closed = false;\n\tpublic startedAt?: number;\n\tpublic endedAt?: number;\n\tpublic closedAt?: number;\n\n\tpublic readonly settings: Pick<ObservedCallSettings, 'closeCallIfEmptyForMs' | 'autoUpdateOnClientUpdate'>;\n\n\t/** Ancestry base shared by all Observer-bus events originating at this call. */\n\tpublic readonly eventScope: ObservedCallScope;\n\tprivate closeTimer?: ReturnType<typeof setTimeout>;\n\n\tpublic constructor(\n\t\tsettings: ObservedCallSettings<AppData>,\n\t\tpublic readonly observer: Observer,\n\t\tpublic readonly activeIssuesRegistry: ActiveIssuesRegistry,\n\t) {\n\t\tsuper();\n\t\tthis.setMaxListeners(Infinity);\n\n\t\tthis.eventScope = { observer: this.observer, observedCall: this };\n\t\tthis.callId = settings.callId;\n\t\tthis.appData = (settings.appData ?? {}) as AppData;\n\t\tthis.scoreCalculator = new DefaultCallScoreCalculator(this);\n\t\tthis.detectors = new Detectors();\n\n\t\tthis.settings = {\n\t\t\tcloseCallIfEmptyForMs: settings.closeCallIfEmptyForMs,\n\t\t\tautoUpdateOnClientUpdate: settings.autoUpdateOnClientUpdate ?? true,\n\t\t};\n\t}\n\n\tpublic get numberOfClients() {\n\t\treturn this.observedClients.size;\n\t}\n\n\tpublic get score() {\n\t\treturn this.calculatedScore.value;\n\t}\n\n\t/**\n\t * Build a call-scoped detector onto this call. Chainable.\n\t *\n\t * To get a handle on what was built — to inspect it, or to remove that exact instance later — read\n\t * it back off the registry: `call.detectors.getAll(name)`, or `call.detectors.instances`.\n\t */\n\tpublic addDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config: Partial<AvailableCallScopeDetectorsConfigs[K]> = {}): this {\n\t\tif (this.closed) return this;\n\n\t\tlet detector: Detector | undefined;\n\n\t\tswitch (name) {\n\t\t\tcase UnconsumedTrackDetector.NAME: {\n\t\t\t\tdetector = new UnconsumedTrackDetector(this, config);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase TrackDeliveryMismatchDetector.NAME: {\n\t\t\t\tdetector = new TrackDeliveryMismatchDetector(this, config);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase CallConcurrentIssueDetector.NAME: {\n\t\t\t\tdetector = new CallConcurrentIssueDetector(this, config);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase IssueFanOutDetector.NAME: {\n\t\t\t\tdetector = new IssueFanOutDetector(this, config);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase PublisherFaultCorroborationDetector.NAME: {\n\t\t\t\tdetector = new PublisherFaultCorroborationDetector(this, config);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tlogger.warn('Unknown detector name %s; skipping', name);\n\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\n\t\tthis.detectors.add(detector);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Start accumulating this call's summary, if the observer was configured for summaries.\n\t *\n\t * Called by `createObservedCall`; you should not need it. It takes no configuration of its own on\n\t * purpose: the collector subscribes to exactly the events the observer's `include` requires, so a\n\t * per-call section outside that set would be created and then never written to — an empty section\n\t * that reads as \"nothing happened\". One shape per observer is the only shape that can be filled.\n\t *\n\t * The collector builds it rather than this method, so the resolved configuration never has to\n\t * leave the one object that owns it. Returns `undefined` when summaries are off, and is\n\t * idempotent: an existing summary is kept, not restarted.\n\t */\n\tpublic enableSummary(): CallSummary | undefined {\n\t\treturn (this.summary ??= this.observer.callSummaryCollector?.createSummary(this.callId));\n\t}\n\n\t/**\n\t * Remove a detector from **this call** by name, returning how many were removed.\n\t *\n\t * **Every** instance under the name goes — a name can legitimately be registered more than once.\n\t * When you want one of them specifically, go through the registry, which deals in instances:\n\t *\n\t * ```ts\n\t * const [ first ] = call.detectors.getAll('issue-fan-out-detector');\n\t *\n\t * call.detectors.remove(first);\n\t * ```\n\t *\n\t * Either route `close()`s the detector, so it unsubscribes from `activeIssuesRegistry` — without\n\t * that the registry keeps feeding a detector nobody is running any more, and its tracked set grows\n\t * for the life of the call.\n\t *\n\t * To stop building it on *future* calls too, use `observer.removeCallDetector(name)`.\n\t */\n\tpublic removeDetector(name: keyof AvailableCallScopeDetectorsConfigs): number {\n\t\treturn this.detectors.removeByName(name);\n\t}\n\n\t/**\n\t * Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.\n\t *\n\t * `payload` is an **object** and holds evidence only — it is delivered to an in-process handler,\n\t * so there is nothing to serialise for. `scope` is stamped here, and the `callId` is already on\n\t * the event, so neither belongs in the payload. Put the interpretation in `conclusion`.\n\t */\n\tpublic addIssue(issue: Omit<CallIssue, 'scope'>) {\n\t\tif (this.closed) return;\n\n\t\t// `scope` is stamped here; see `Observer.addIssue`. The call itself is the event's scope, so\n\t\t// the payload has no reason to carry a `callId` either.\n\t\tthis._notify('call-issue', { ...this.eventScope, issue: { ...issue, scope: 'call' } });\n\t}\n\n\tpublic close() {\n\t\tif (this.closed) return;\n\t\tthis.update(); // last update before closing\n\t\tthis.closed = true;\n\n\t\t// Disarm the empty-call timer. It is armed when the last client leaves, so closing the call\n\t\t// directly (rather than letting the timer do it) previously left a live handle behind.\n\t\tif (this.closeTimer) {\n\t\t\tclearTimeout(this.closeTimer);\n\t\t\tthis.closeTimer = undefined;\n\t\t}\n\n\t\tlet minSampleTimestamps: number | undefined;\n\t\tlet maxSampleTimestamps: number | undefined;\n\t\tconst clients = [ ...this.observedClients.values() ];\n\n\t\tfor (const client of clients) {\n\t\t\tclient.close();\n\n\t\t\tif (client.joinedAt) minSampleTimestamps = Math.min(minSampleTimestamps ?? client.joinedAt, client.joinedAt);\n\t\t\tif (client.leftAt) maxSampleTimestamps = Math.max(maxSampleTimestamps ?? client.leftAt, client.leftAt);\n\t\t}\n\n\t\tif (this.startedAt === undefined) this.startedAt = minSampleTimestamps;\n\t\tif (this.endedAt === undefined) this.endedAt = maxSampleTimestamps;\n\n\t\tthis.closedAt = Date.now();\n\n\t\tif (this.summary) {\n\t\t\t// Finalise and deliver while the call is still reachable: `call-closed` below removes it\n\t\t\t// from `observer.observedCalls`, and a summary handed over after that has no call to read.\n\t\t\tthis.observer.callSummaryCollector?.finalise(this);\n\t\t\tthis._notify('call-summary', { ...this.eventScope, summary: this.summary });\n\t\t}\n\n\t\tthis.detectors.clear();\n\t\tthis.activeIssuesRegistry.clear();\n\t\tthis.emit('close');\n\t\tthis._notify('call-closed', { ...this.eventScope });\n\t}\n\n\tpublic getObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(clientId: string): ObservedClient<ClientAppData> | undefined {\n\t\tif (this.closed || !this.observedClients.has(clientId)) return;\n\n\t\treturn this.observedClients.get(clientId) as ObservedClient<ClientAppData>;\n\t}\n\n\tpublic createObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(\n\t\tsettings: ObservedClientSettings<ClientAppData>, acceptCtx?: AcceptContext\n\t): ObservedClient<ClientAppData> | undefined {\n\t\tif (this.closed) {\n\t\t\tlogger.warn('Attempted to create a client (clientId: %s) on a closed call %s', settings.clientId, this.callId);\n\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.observedClients.has(settings.clientId)) {\n\t\t\tlogger.warn('Client with id %s already exists in call %s; returning the existing instance', settings.clientId, this.callId);\n\n\t\t\treturn this.observedClients.get(settings.clientId) as ObservedClient<ClientAppData>;\n\t\t}\n\n\t\tconst clientSettings: ObservedClientSettings<ClientAppData> = {\n\t\t\t...settings,\n\t\t\tcloseClientIfIdleForMs: settings.closeClientIfIdleForMs || this.observer.config.closeClientIfIdleForMs,\n\t\t\tappData: settings.appData ?? this.observer.config.createClientAppData?.({\n\t\t\t\tclientId: settings.clientId,\n\t\t\t\tobservedCall: this,\n\t\t\t\tacceptCtx,\n\t\t\t}) as ClientAppData,\n\t\t};\n\n\t\tconst observedClientIssueRegistry = new ObservedClientIssueRegistry(this.activeIssuesRegistry);\n\t\tconst result = new ObservedClient<ClientAppData>(\n\t\t\tclientSettings,\n\t\t\tthis,\n\t\t\tobservedClientIssueRegistry,\n\t\t);\n\t\tconst wasEmpty = this.observedClients.size === 0;\n\t\tconst onUpdate = (...args: ObservedClientEvents['update']) => this._onClientUpdate(result, args[2]);\n\t\tconst joined = () => this._clientJoined(result);\n\t\tconst left = () => this._clientLeft(result);\n\n\t\tresult.once('close', () => {\n\t\t\tresult.off('update', onUpdate);\n\t\t\tresult.off('joined', joined);\n\t\t\tresult.off('left', left);\n\t\t\tthis.observedClients.delete(settings.clientId);\n\t\t\tthis.clientsUsedTurn.delete(settings.clientId);\n\n\t\t\t// Not while the call itself is closing: that close is what removed the clients, the call is\n\t\t\t// not \"empty and waiting\", and arming the empty-call timer here would leave a live handle\n\t\t\t// on a closed call (`close()` disarms the timer *before* it closes the clients).\n\t\t\tif (this.observedClients.size === 0 && !this.closed) {\n\t\t\t\tthis.emit('empty');\n\t\t\t\tthis._notify('call-empty', { ...this.eventScope });\n\n\t\t\t\tif (this.settings.closeCallIfEmptyForMs) {\n\t\t\t\t\tthis.closeTimer = setTimeout(() => {\n\t\t\t\t\t\tthis.close();\n\t\t\t\t\t}, this.settings.closeCallIfEmptyForMs);\n\n\t\t\t\t\t// See `ObservedClient`: a monitoring timer must not be the reason a process stays\n\t\t\t\t\t// alive. It still fires while the application runs.\n\t\t\t\t\tthis.closeTimer.unref?.();\n\t\t\t\t}\n\t\t\t}\n\t\t\t++this.totalRemovedClients;\n\t\t});\n\t\tresult.on('update', onUpdate);\n\t\tresult.on('joined', joined);\n\t\tresult.on('left', left);\n\t\t++this.totalAddedClients;\n\n\t\tthis.observedClients.set(settings.clientId, result);\n\t\tthis.maxNumberOfClients = Math.max(this.maxNumberOfClients, this.observedClients.size);\n\n\t\tif (this.closeTimer) {\n\t\t\tclearTimeout(this.closeTimer);\n\t\t\tthis.closeTimer = undefined;\n\t\t}\n\n\t\tthis.emit('newclient', result);\n\t\tthis._notify('client-added', { ...this.eventScope, observedClient: result });\n\n\t\tif (result.sink) {\n\t\t\tthis._notify('client-sink-created', { ...this.eventScope, observedClient: result, sink: result.sink });\n\t\t}\n\n\t\tif (wasEmpty) {\n\t\t\tthis.emit('not-empty');\n\t\t\tthis._notify('call-not-empty', { ...this.eventScope });\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tpublic getOrCreateObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(\n\t\tsettings: ObservedClientSettings<ClientAppData>, acceptCtx?: AcceptContext\n\t): ObservedClient<ClientAppData> | undefined {\n\t\treturn this.getObservedClient<ClientAppData>(settings.clientId) ?? this.createObservedClient<ClientAppData>(settings, acceptCtx);\n\t}\n\n\tpublic update(context?: AcceptContext) {\n\t\tif (this.closed) return;\n\n\t\tthis.numberOfInboundRtpStreams = 0;\n\t\tthis.numberOfOutboundRtpStreams = 0;\n\t\tthis.numberOfPeerConnections = 0;\n\t\tthis.numberOfDataChannels = 0;\n\n\t\tfor (const client of this.observedClients.values()) {\n\t\t\tthis.numberOfInboundRtpStreams += client.numberOfInboundRtpStreams;\n\t\t\tthis.numberOfOutboundRtpStreams += client.numberOfOutboundRtpStreams;\n\t\t\tthis.numberOfPeerConnections += client.numberOfPeerConnections;\n\t\t\tthis.numberOfDataChannels += client.numberOfDataChannels;\n\t\t}\n\n\t\tthis.scoreCalculator.update();\n\t\tthis.detectors.update();\n\n\t\tthis.emit('update');\n\t\tthis._notify('call-updated', { ...this.eventScope, context });\n\n\t\tthis.deltaNumberOfIssues = 0;\n\t}\n\n\tprivate _onClientUpdate(client: ObservedClient, context?: AcceptContext) {\n\t\tthis.deltaNumberOfIssues += client.deltaNumberOfIssues;\n\t\tthis.numberOfIssues += client.deltaNumberOfIssues;\n\n\t\tif (client.usingTURN) this.clientsUsedTurn.add(client.clientId);\n\t\telse this.clientsUsedTurn.delete(client.clientId);\n\n\t\tif (this.settings.autoUpdateOnClientUpdate) {\n\t\t\tthis.update(context);\n\t\t}\n\t}\n\n\tprivate _clientJoined(client: ObservedClient) {\n\t\tif (!client.joinedAt) return;\n\n\t\tthis.startedAt = Math.min(this.startedAt ?? client.joinedAt, client.joinedAt);\n\t}\n\n\tprivate _clientLeft(client: ObservedClient) {\n\t\tif (!client.leftAt) return;\n\n\t\tthis.endedAt = Math.max(this.endedAt ?? client.leftAt, client.leftAt);\n\t}\n\n\t/** Emit an Observer-bus event scoped to this call. */\n\tprivate _notify<K extends keyof ObserverEvents>(type: K, ...args: ObserverEvents[K]): void {\n\t\tthis.observer.emit(type, ...args);\n\t}\n}","import { EventEmitter } from 'events';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { createLogger } from './common/logger';\n// eslint-disable-next-line camelcase\nimport { ClientEvent, ClientMetaData, ClientSample, PeerConnectionSample, ClientIssue, ExtensionStat } from './schema/ClientSample';\nimport * as MetaData from './schema/ClientMetaTypes';\nimport { ClientEventTypes } from './schema/ClientEventTypes';\nimport { ObservedCall } from './ObservedCall';\nimport { ClientMetaTypes } from './schema/ClientMetaTypes';\nimport { normalizeScoreReasons, parseJsonAs, parseJsonObject } from './common/utils';\nimport { CalculatedScore } from './scores/CalculatedScore';\nimport type { AcceptContext } from './Observer';\nimport type { ObserverEvents, ObservedClientScope } from './ObserverEvents';\nimport type { ClientSampleSink } from './sinks/ClientSampleSink';\nimport { ObservedClientIssueRegistry } from './issues/ObservedClientIssueRegistry';\nimport { ActiveClientIssue, RESOLVED_ISSUE_SUFFIX, baseIssueType, isClientIssueResolutionEntry } from './issues/ActiveClientIssue';\nimport type { ResolvedActiveClientIssue } from './issues/ActiveClientIssue';\n\nconst logger = createLogger('ObservedClient');\n\nexport type ObservedClientSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {\n\tclientId: string;\n\tappData?: AppData;\n\tcloseClientIfIdleForMs?: number;\n};\n\n// Local lifecycle/coordination events only. Everything worth subscribing to is\n// emitted on the Observer bus (see ObserverEvents). These remain local so the\n// parent ObservedCall and the updaters can wire to them for teardown/aggregation.\nexport type ObservedClientEvents = {\n\tupdate: [sample: ClientSample, elapsedTimeInMs: number, context?: AcceptContext];\n\tclose: [];\n\tjoined: [];\n\tleft: [];\n};\n\nexport declare interface ObservedClient {\n\ton<U extends keyof ObservedClientEvents>(event: U, listener: (...args: ObservedClientEvents[U]) => void): this;\n\toff<U extends keyof ObservedClientEvents>(event: U, listener: (...args: ObservedClientEvents[U]) => void): this;\n\tonce<U extends keyof ObservedClientEvents>(event: U, listener: (...args: ObservedClientEvents[U]) => void): this;\n\temit<U extends keyof ObservedClientEvents>(event: U, ...args: ObservedClientEvents[U]): boolean;\n}\n\nexport class ObservedClient<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter {\n\tpublic readonly clientId: string;\n\tpublic readonly observedPeerConnections = new Map<string, ObservedPeerConnection>();\n\n\t/** Per-client sink (from `ObserverConfig.createClientSink`), if any. */\n\tpublic readonly sink?: ClientSampleSink;\n\n\t/** Ancestry base shared by all Observer-bus events originating at this client. */\n\tpublic readonly eventScope: ObservedClientScope;\n\tpublic readonly calculatedScore: CalculatedScore = {\n\t\tweight: 1,\n\t\tvalue: undefined,\n\t};\n\n\tpublic readonly settings: Pick<ObservedClientSettings, 'closeClientIfIdleForMs'>;\n\n\tpublic appData: AppData;\n\tpublic attachments?: Record<string, unknown>;\n\n\tpublic updated = Date.now();\n\tpublic acceptedSamples = 0;\n\tpublic closed = false;\n\n\tpublic joinedAt?: number;\n\tpublic leftAt?: number;\n\tpublic closedAt?: number;\n\tpublic lastSampleTimestamp?: number;\n\t// the timestamp of the CLIENT_JOINED event\n\tpublic operationSystem?: MetaData.OperationSystem;\n\tpublic engine?: MetaData.Engine;\n\tpublic platform?: MetaData.Platform;\n\tpublic browser?: MetaData.Browser;\n\tpublic mediaConstraints: string[] = [];\n\n\tpublic usingTURN = false;\n\tpublic usingTCP = false;\n\n\tpublic publicAddress?: string;\n\tpublic availableOutgoingBitrate = 0;\n\tpublic availableIncomingBitrate = 0;\n\n\tpublic deltaReceivedAudioBytes = 0;\n\tpublic deltaReceivedVideoBytes = 0;\n\tpublic deltaSentAudioBytes = 0;\n\tpublic deltaSentVideoBytes = 0;\n\tpublic deltaDataChannelBytesSent = 0;\n\tpublic deltaDataChannelBytesReceived = 0;\n\tpublic deltaDataChannelMessagesSent = 0;\n\tpublic deltaDataChannelMessagesReceived = 0;\n\tpublic deltaInboundPacketsLost = 0;\n\tpublic deltaInboundPacketsReceived = 0;\n\tpublic deltaOutboundPacketsSent = 0;\n\tpublic deltaTransportSentBytes = 0;\n\tpublic deltaTransportReceivedBytes = 0;\n\tpublic deltaRttLt50Measurements = 0;\n\tpublic deltaRttLt150Measurements = 0;\n\tpublic deltaRttLt300Measurements = 0;\n\tpublic deltaRttGtOrEq300Measurements = 0;\n\n\tpublic currentMaxRttInMs?: number;\n\tpublic currentMinRttInMs?: number;\n\tpublic currentAvgRttInMs?: number;\n\n\tpublic sendingAudioBitrate = 0;\n\tpublic sendingVideoBitrate = 0;\n\tpublic receivingAudioBitrate = 0;\n\tpublic receivingVideoBitrate = 0;\n\n\tpublic numberOfInboundRtpStreams = 0;\n\tpublic numberOfInbundTracks = 0;\n\tpublic numberOfOutboundRtpStreams = 0;\n\tpublic numberOfOutboundTracks = 0;\n\tpublic numberOfDataChannels = 0;\n\n\tpublic deltaNumberOfIssues = 0;\n\n\tpublic totalScoreSum = 0;\n\tpublic numberOfScoreMeasurements = 0;\n\t// public totalNumberOfIssues = 0;\n\n\tpublic readonly mediaDevices: MetaData.MediaDeviceInfo[] = [];\n\n\t/**\n\t * The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side\n\t * mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).\n\t *\n\t * This turns point-in-time symptom reports into intervals, which is what lets detectors ask\n\t * \"are these clients broken *at the same time*\" instead of \"did they both report something\n\t * recently\". Entries are opened by a raise, closed by the matching `<type>-resolved` entry, and\n\t * force-closed when the client closes.\n\t */\n\tpublic readonly activeIssues: ObservedClientIssueRegistry;\n\n\tprivate _pendingInjections: Pick<ClientSample, 'clientEvents' | 'clientIssues' | 'extensionStats' | 'attachments' | 'clientMetaItems'> = {};\n\tprivate _activeSample?: ClientSample;\n\n\t/** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */\n\tprivate readonly _peerConnectionsToClose = new Set<ObservedPeerConnection>();\n\n\t/** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */\n\tprivate readonly _closedPeerConnectionIds = new Set<string>();\n\n\tprivate closeTimer?: ReturnType<typeof setTimeout>;\n\n\tpublic constructor(\n\t\tsettings: ObservedClientSettings<AppData>,\n\t\tpublic readonly call: ObservedCall,\n\t\tactiveIssues: ObservedClientIssueRegistry\n\t) {\n\t\tsuper();\n\t\tthis.setMaxListeners(Infinity);\n\n\t\tthis.eventScope = { observer: call.observer, observedCall: call, observedClient: this };\n\t\tthis.sink = call.observer.config.createClientSink?.({ clientId: settings.clientId, observedCall: call });\n\t\tthis.clientId = settings.clientId;\n\t\tthis.appData = (settings.appData ?? {}) as AppData;\n\n\t\tthis.settings = {\n\t\t\tcloseClientIfIdleForMs: settings.closeClientIfIdleForMs,\n\t\t};\n\n\t\tif (this.sink) {\n\t\t\tconst onError = (err: Error) => logger.warn('ClientSampleSink error for client %s: %o', this.clientId, err);\n\n\t\t\tthis.sink.once('close', () => this.sink?.off('error', onError));\n\t\t\tthis.sink.on('error', onError);\n\t\t}\n\t\tthis.activeIssues = activeIssues;\n\t}\n\n\t/** The distinct `publicAddress`es currently known by this client's open peer connections. */\n\tpublic get publicAddresses(): string[] {\n\t\tconst result = new Set<string>();\n\n\t\tfor (const observedPeerConnection of this.observedPeerConnections.values()) {\n\t\t\tif (observedPeerConnection.publicAddress) result.add(observedPeerConnection.publicAddress);\n\t\t}\n\n\t\treturn [ ...result ];\n\t}\n\n\tpublic get numberOfPeerConnections() {\n\t\treturn this.observedPeerConnections.size;\n\t}\n\n\tpublic get score() {\n\t\treturn this.calculatedScore.value;\n\t}\n\n\tpublic close() {\n\t\tif (this.closed) return;\n\n\t\t// The idle timer is armed on every accepted sample and was never disarmed here, so closing a\n\t\t// client left a live handle for up to `closeClientIfIdleForMs` (60 s by default) that would\n\t\t// eventually fire `close()` on an already-closed client. Harmless in effect, but it pins the\n\t\t// event loop — which is why a test suite would not exit.\n\t\tif (this.closeTimer) {\n\t\t\tclearTimeout(this.closeTimer);\n\t\t\tthis.closeTimer = undefined;\n\t\t}\n\n\t\tthis._flushPendingInjections();\n\n\t\tconst activeIssues = [ ...this.activeIssues.values() ];\n\n\t\tfor (const issue of activeIssues) {\n\t\t\tthis._resolveIssue({ key: issue.key, type: `${issue.type}${RESOLVED_ISSUE_SUFFIX}` }, 'client-closed');\n\t\t}\n\n\t\tthis.closed = true;\n\n\t\tArray.from(this.observedPeerConnections.values()).forEach((peerConnection) => peerConnection.close());\n\t\tif (!this.leftAt) {\n\t\t\tthis.leftAt = this.lastSampleTimestamp;\n\n\t\t\tif (this.leftAt) {\n\t\t\t\tthis.emit('left');\n\t\t\t\tthis._notify('client-left', { ...this.eventScope });\n\t\t\t}\n\t\t}\n\t\tthis.closedAt = Date.now();\n\n\t\tthis.emit('close');\n\t\tthis._notify('client-closed', { ...this.eventScope });\n\n\t\ttry {\n\t\t\tthis.sink?.end();\n\t\t} catch (err) {\n\t\t\tlogger.warn('ClientSampleSink.end failed for client %s: %o', this.clientId, err);\n\t\t}\n\t}\n\n\tpublic accept(sample: ClientSample, context?: AcceptContext): void {\n\t\tif (this.closed) {\n\t\t\tlogger.warn('Attempted to accept a sample on a closed client %s', this.clientId);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.closeTimer) {\n\t\t\tclearTimeout(this.closeTimer);\n\t\t\tthis.closeTimer = undefined;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst elapsedInMs = now - this.updated;\n\t\tconst elapsedInSeconds = elapsedInMs / 1000;\n\t\tlet sumOfRtts = 0;\n\t\tlet numberOfRttMeasurements = 0;\n\n\t\t++this.acceptedSamples;\n\n\t\tthis.availableIncomingBitrate = 0;\n\t\tthis.availableOutgoingBitrate = 0;\n\t\tthis.deltaDataChannelBytesReceived = 0;\n\t\tthis.deltaDataChannelBytesSent = 0;\n\t\tthis.deltaDataChannelMessagesReceived = 0;\n\t\tthis.deltaDataChannelMessagesSent = 0;\n\t\tthis.deltaInboundPacketsLost = 0;\n\t\tthis.deltaInboundPacketsReceived = 0;\n\t\tthis.deltaOutboundPacketsSent = 0;\n\t\tthis.deltaReceivedAudioBytes = 0;\n\t\tthis.deltaReceivedVideoBytes = 0;\n\t\tthis.deltaSentAudioBytes = 0;\n\t\tthis.deltaSentVideoBytes = 0;\n\t\tthis.deltaTransportReceivedBytes = 0;\n\t\tthis.deltaTransportSentBytes = 0;\n\t\tthis.deltaRttLt50Measurements = 0;\n\t\tthis.deltaRttLt150Measurements = 0;\n\t\tthis.deltaRttLt300Measurements = 0;\n\t\tthis.deltaRttGtOrEq300Measurements = 0;\n\t\tthis.deltaNumberOfIssues = 0;\n\n\t\tthis.numberOfDataChannels = 0;\n\t\tthis.numberOfInboundRtpStreams = 0;\n\t\tthis.numberOfInbundTracks = 0;\n\t\tthis.numberOfOutboundRtpStreams = 0;\n\t\tthis.numberOfOutboundTracks = 0;\n\t\tthis.usingTURN = false;\n\t\tthis.usingTCP = false;\n\t\tthis.currentMinRttInMs = undefined;\n\t\tthis.currentMaxRttInMs = undefined;\n\n\t\tsample.scoreReasons = normalizeScoreReasons(sample.scoreReasons);\n\t\tfor (const pcSample of sample.peerConnections ?? []) {\n\t\t\tpcSample.scoreReasons = normalizeScoreReasons(pcSample.scoreReasons);\n\n\t\t\tfor (const trackSample of pcSample.inboundTracks ?? []) {\n\t\t\t\ttrackSample.scoreReasons = normalizeScoreReasons(trackSample.scoreReasons);\n\t\t\t}\n\t\t\tfor (const trackSample of pcSample.outboundTracks ?? []) {\n\t\t\t\ttrackSample.scoreReasons = normalizeScoreReasons(trackSample.scoreReasons);\n\t\t\t}\n\t\t}\n\n\t\tthis._mergePendingInjections(sample);\n\n\t\t// From here until the end of accept, `inject*` targets this sample directly (see `_activeSample`).\n\t\tthis._activeSample = sample;\n\n\t\tconst clientEventsPostBuffer: ClientEvent[] = [];\n\n\t\tfor (const clientEvent of sample.clientEvents ?? []) {\n\t\t\tthis._processClientEvent(clientEvent, clientEventsPostBuffer);\n\t\t}\n\n\t\tfor (const metaData of sample.clientMetaItems ?? []) {\n\t\t\tthis.addMetadata(metaData);\n\t\t}\n\n\t\tfor (const issue of sample.clientIssues ?? []) {\n\t\t\tthis.addIssue(issue);\n\n\t\t\t++this.deltaNumberOfIssues;\n\t\t}\n\n\t\tfor (const extensionStat of sample.extensionStats ?? []) {\n\t\t\tthis.addExtensionStats(extensionStat);\n\t\t}\n\n\t\t// The most recently changed peer-connection public address wins (a network switch shows up on\n\t\t// the PC that restarted ICE first).\n\t\tlet latestPublicAddress: string | undefined;\n\t\tlet latestPublicAddressUpdatedAt = -1;\n\n\t\tfor (const pcSample of sample.peerConnections ?? []) {\n\t\t\tconst observedPeerConnection = this._updatePeerConnection(pcSample, context);\n\n\t\t\tif (!observedPeerConnection) continue;\n\n\t\t\tif (observedPeerConnection.publicAddress && latestPublicAddressUpdatedAt < (observedPeerConnection.publicAddressUpdatedAt ?? 0)) {\n\t\t\t\tlatestPublicAddress = observedPeerConnection.publicAddress;\n\t\t\t\tlatestPublicAddressUpdatedAt = observedPeerConnection.publicAddressUpdatedAt ?? 0;\n\t\t\t}\n\n\t\t\tthis.deltaDataChannelBytesReceived += observedPeerConnection.deltaDataChannelBytesReceived;\n\t\t\tthis.deltaDataChannelBytesSent += observedPeerConnection.deltaDataChannelBytesSent;\n\t\t\tthis.deltaDataChannelMessagesReceived += observedPeerConnection.deltaDataChannelMessagesReceived;\n\t\t\tthis.deltaDataChannelMessagesSent += observedPeerConnection.deltaDataChannelMessagesSent;\n\t\t\tthis.deltaInboundPacketsLost += observedPeerConnection.deltaInboundPacketsLost;\n\t\t\tthis.deltaInboundPacketsReceived += observedPeerConnection.deltaInboundPacketsReceived;\n\t\t\tthis.deltaOutboundPacketsSent += observedPeerConnection.deltaOutboundPacketsSent;\n\t\t\tthis.deltaReceivedAudioBytes += observedPeerConnection.deltaReceivedAudioBytes;\n\t\t\tthis.deltaReceivedVideoBytes += observedPeerConnection.deltaReceivedVideoBytes;\n\t\t\tthis.deltaSentAudioBytes += observedPeerConnection.deltaSentAudioBytes;\n\t\t\tthis.deltaSentVideoBytes += observedPeerConnection.deltaSentVideoBytes;\n\t\t\tthis.deltaTransportReceivedBytes += observedPeerConnection.deltaTransportReceivedBytes;\n\t\t\tthis.deltaTransportSentBytes += observedPeerConnection.deltaTransportSentBytes;\n\n\t\t\tthis.availableIncomingBitrate += observedPeerConnection.availableIncomingBitrate;\n\t\t\tthis.availableOutgoingBitrate += observedPeerConnection.availableOutgoingBitrate;\n\n\t\t\tthis.numberOfDataChannels += observedPeerConnection.observedDataChannels.size;\n\t\t\tthis.numberOfInbundTracks += observedPeerConnection.observedInboundTracks.size;\n\t\t\tthis.numberOfOutboundRtpStreams += observedPeerConnection.observedOutboundRtps.size;\n\t\t\tthis.numberOfOutboundTracks += observedPeerConnection.observedOutboundTracks.size;\n\t\t\tthis.numberOfInboundRtpStreams += observedPeerConnection.observedInboundRtps.size;\n\n\t\t\tif (observedPeerConnection.usingTURN) {\n\t\t\t\tthis.usingTURN = true;\n\t\t\t}\n\t\t\tif (observedPeerConnection.usingTCP) {\n\t\t\t\tthis.usingTCP = true;\n\t\t\t}\n\n\t\t\tif (observedPeerConnection.currentRttInMs) {\n\t\t\t\tif (this.currentMinRttInMs === undefined || observedPeerConnection.currentRttInMs < this.currentMinRttInMs) {\n\t\t\t\t\tthis.currentMinRttInMs = observedPeerConnection.currentRttInMs;\n\t\t\t\t}\n\t\t\t\tif (this.currentMaxRttInMs === undefined || observedPeerConnection.currentRttInMs > this.currentMaxRttInMs) {\n\t\t\t\t\tthis.currentMaxRttInMs = observedPeerConnection.currentRttInMs;\n\t\t\t\t}\n\t\t\t\tif (observedPeerConnection.currentRttInMs < 50) {\n\t\t\t\t\tthis.deltaRttLt50Measurements += 1;\n\t\t\t\t} else if (observedPeerConnection.currentRttInMs < 150) {\n\t\t\t\t\tthis.deltaRttLt150Measurements += 1;\n\t\t\t\t} else if (observedPeerConnection.currentRttInMs < 300) {\n\t\t\t\t\tthis.deltaRttLt300Measurements += 1;\n\t\t\t\t} else if (300 <= observedPeerConnection.currentRttInMs) {\n\t\t\t\t\tthis.deltaRttGtOrEq300Measurements += 1;\n\t\t\t\t}\n\n\t\t\t\tsumOfRtts += observedPeerConnection.currentRttInMs;\n\t\t\t\t++numberOfRttMeasurements;\n\t\t\t}\n\t\t}\n\n\t\tfor (const clientEvent of clientEventsPostBuffer) {\n\t\t\tthis._processClientEvent(clientEvent);\n\t\t}\n\n\t\tthis._closeReportedPeerConnections();\n\n\t\tif (latestPublicAddress !== undefined && latestPublicAddress !== this.publicAddress) {\n\t\t\tconst previousPublicAddress = this.publicAddress;\n\n\t\t\tthis.publicAddress = latestPublicAddress;\n\t\t\tthis._notify('client-public-address-changed', { ...this.eventScope, publicAddress: latestPublicAddress, previousPublicAddress });\n\t\t}\n\n\t\t// emit new attachments?\n\t\tthis.attachments = sample.attachments;\n\n\t\t// Guard the division: two samples accepted within the same millisecond make `elapsedInSeconds`\n\t\t// 0, which previously produced Infinity/NaN bitrates that then poisoned every aggregate above.\n\t\tif (0 < elapsedInSeconds) {\n\t\t\tthis.receivingAudioBitrate = (this.deltaReceivedAudioBytes * 8) / elapsedInSeconds;\n\t\t\tthis.receivingVideoBitrate = (this.deltaReceivedVideoBytes * 8) / elapsedInSeconds;\n\t\t\tthis.sendingAudioBitrate = (this.deltaSentAudioBytes * 8) / elapsedInSeconds;\n\t\t\tthis.sendingVideoBitrate = (this.deltaSentVideoBytes * 8) / elapsedInSeconds;\n\t\t} else {\n\t\t\tthis.receivingAudioBitrate = 0;\n\t\t\tthis.receivingVideoBitrate = 0;\n\t\t\tthis.sendingAudioBitrate = 0;\n\t\t\tthis.sendingVideoBitrate = 0;\n\t\t}\n\t\tthis.currentAvgRttInMs = 0 < numberOfRttMeasurements ? sumOfRtts / numberOfRttMeasurements : undefined;\n\n\t\tthis.calculatedScore.value = sample.score;\n\n\t\tthis.lastSampleTimestamp = sample.timestamp;\n\t\t// emit update\n\t\tconst elapsedTimeInMs = now - this.updated;\n\n\t\tthis.emit('update', sample, elapsedTimeInMs, context);\n\t\tthis._notify('client-updated', { ...this.eventScope, sample, elapsedTimeInMs, context });\n\t\tthis.updated = now;\n\n\t\t// if result changed after update\n\t\tif (this.calculatedScore.value) {\n\t\t\tthis.totalScoreSum += this.calculatedScore.value;\n\t\t\t++this.numberOfScoreMeasurements;\n\t\t}\n\n\t\tif (this.settings.closeClientIfIdleForMs !== undefined) {\n\t\t\tthis.closeTimer = setTimeout(() => {\n\t\t\t\tthis.close();\n\t\t\t}, this.settings.closeClientIfIdleForMs);\n\n\t\t\t// Do not let a monitoring timer be the reason a process stays alive. It still fires\n\t\t\t// normally while the application is running; it just stops holding the event loop open on\n\t\t\t// its own. Optional-called because some fake-timer implementations omit `unref`.\n\t\t\tthis.closeTimer.unref?.();\n\t\t}\n\n\t\t// Done processing this sample. Stop directing injections at it, then persist the FINAL,\n\t\t// injection-merged sample to the sink (this is why the sink write is here, not at the top).\n\t\tthis._activeSample = undefined;\n\t\t// A `PEER_CONNECTION_CLOSED` injected from a `client-updated` handler.\n\t\tthis._closeReportedPeerConnections();\n\t\ttry {\n\t\t\tthis.sink?.write(sample);\n\t\t} catch (err) {\n\t\t\tlogger.warn('ClientSampleSink.write failed for client %s: %o', this.clientId, err);\n\t\t}\n\t}\n\n\tpublic injectMetaData(metaData: ClientMetaData) {\n\t\tif (this.closed) return;\n\n\t\tif (this._activeSample) {\n\t\t\tif (!this._activeSample.clientMetaItems) this._activeSample.clientMetaItems = [];\n\t\t\tthis._activeSample.clientMetaItems.push(metaData);\n\t\t\tthis.addMetadata(metaData);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this._pendingInjections.clientMetaItems) this._pendingInjections.clientMetaItems = [];\n\t\tthis._pendingInjections.clientMetaItems.push(metaData);\n\t}\n\n\tpublic injectEvent(event: ClientEvent) {\n\t\tif (this.closed) return;\n\n\t\tif (this._activeSample) {\n\t\t\tif (!this._activeSample.clientEvents) this._activeSample.clientEvents = [];\n\t\t\tthis._activeSample.clientEvents.push(event);\n\t\t\tthis._processClientEvent(event);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this._pendingInjections.clientEvents) this._pendingInjections.clientEvents = [];\n\t\tthis._pendingInjections.clientEvents.push(event);\n\t}\n\n\tpublic injectIssue(issue: ClientIssue) {\n\t\tif (this.closed) return;\n\n\t\tif (this._activeSample) {\n\t\t\tif (!this._activeSample.clientIssues) this._activeSample.clientIssues = [];\n\t\t\tthis._activeSample.clientIssues.push(issue);\n\t\t\tthis.addIssue(issue);\n\t\t\t++this.deltaNumberOfIssues;\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this._pendingInjections.clientIssues) this._pendingInjections.clientIssues = [];\n\t\tthis._pendingInjections.clientIssues.push(issue);\n\t}\n\n\tpublic injectExtensionStat(stat: ExtensionStat) {\n\t\tif (this.closed) return;\n\n\t\tif (this._activeSample) {\n\t\t\tif (!this._activeSample.extensionStats) this._activeSample.extensionStats = [];\n\t\t\tthis._activeSample.extensionStats.push(stat);\n\t\t\tthis.addExtensionStats(stat);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this._pendingInjections.extensionStats) this._pendingInjections.extensionStats = [];\n\t\tthis._pendingInjections.extensionStats.push(stat);\n\t}\n\n\tpublic injectAttachment(attachments: Record<string, unknown>) {\n\t\tif (this.closed) return;\n\n\t\tif (this._activeSample) {\n\t\t\tif (!this._activeSample.attachments) this._activeSample.attachments = {};\n\t\t\tObject.assign(this._activeSample.attachments, attachments);\n\t\t\tthis.attachments = this._activeSample.attachments;\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this._pendingInjections.attachments) this._pendingInjections.attachments = {};\n\t\tObject.assign(this._pendingInjections.attachments, attachments);\n\t}\n\n\tpublic addMetadata(metadata: ClientMetaData) {\n\t\tif (this.closed) return;\n\n\t\tswitch (metadata.type) {\n\t\t\tcase ClientMetaTypes.BROWSER: {\n\t\t\t\tthis.browser = parseJsonAs(metadata.payload);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientMetaTypes.ENGINE: {\n\t\t\t\tthis.engine = parseJsonAs(metadata.payload);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientMetaTypes.PLATFORM: {\n\t\t\t\tthis.platform = parseJsonAs(metadata.payload);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientMetaTypes.OPERATION_SYSTEM: {\n\t\t\t\tthis.operationSystem = parseJsonAs(metadata.payload);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tthis._notify('client-metadata', { ...this.eventScope, metaData: metadata });\n\t}\n\n\t/**\n\t * Process one `clientIssues[]` entry.\n\t *\n\t * Entries come in two flavours (client-monitor-js >= 4.6.0):\n\t *\n\t * - a **raise** — opens an {@link ActiveClientIssue} under `issue.key` and emits `client-issue`;\n\t * - a **resolution** — `type` ends in `-resolved` and carries the same `key`; it closes the\n\t * matching active issue and emits `client-issue-resolved`.\n\t *\n\t * Keyless entries are one-shot: reported, never tracked. A re-raise of a key already active\n\t * refreshes the payload rather than opening a second interval.\n\t */\n\tpublic addIssue(issue: ClientIssue) {\n\t\tif (this.closed) return;\n\n\t\tif (isClientIssueResolutionEntry(issue)) {\n\t\t\treturn this._resolveIssue(issue);\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tif (issue.key) {\n\t\t\tconst payload = parseJsonObject(issue.payload);\n\t\t\tconst existing = this.activeIssues.get(issue.key);\n\n\t\t\tif (existing) {\n\t\t\t\t// A re-raise: same interval, fresher payload. Do not restart `raisedAt`.\n\t\t\t\texisting.payload = payload ?? existing.payload;\n\t\t\t} else {\n\t\t\t\tconst active: ActiveClientIssue = {\n\t\t\t\t\tkey: issue.key,\n\t\t\t\t\ttype: baseIssueType(issue.type),\n\t\t\t\t\tclientId: this.clientId,\n\t\t\t\t\tcallId: this.call.callId,\n\t\t\t\t\traisedAt: issue.timestamp ?? now,\n\t\t\t\t\tobservedAt: now,\n\t\t\t\t\tpayload,\n\t\t\t\t\tpeerConnectionId: typeof payload?.peerConnectionId === 'string' ? payload.peerConnectionId : undefined,\n\t\t\t\t\ttrackId: typeof payload?.trackId === 'string' ? payload.trackId : undefined,\n\t\t\t\t};\n\n\t\t\t\tthis.activeIssues.add(active);\n\t\t\t}\n\t\t}\n\n\t\tthis._notify('client-issue', { ...this.eventScope, issue });\n\t}\n\n\tpublic addExtensionStats(stats: ExtensionStat) {\n\t\tif (this.closed) return;\n\n\t\tthis._notify('client-extension-stats', { ...this.eventScope, extensionStats: stats });\n\t}\n\n\t/** Close the active issue a `<type>-resolved` entry refers to, and announce the finished interval. */\n\tprivate _resolveIssue(issue: ClientIssue, resolvedBy: ResolvedActiveClientIssue['resolvedBy'] = 'client') {\n\t\tconst now = Date.now();\n\t\tconst resolutionPayload = parseJsonObject(issue.payload);\n\t\tconst type = baseIssueType(issue.type);\n\t\t// The resolution carries `raisedAt` as a secondary join for consumers that don't store keys.\n\t\tconst raisedAt = typeof resolutionPayload?.raisedAt === 'number' ? resolutionPayload.raisedAt : undefined;\n\t\tconst active = issue.key ? this.activeIssues.get(issue.key) : undefined;\n\n\t\tif (active) {\n\t\t\tthis.activeIssues.remove(active.key);\n\t\t}\n\n\t\tconst resolvedAt = issue.timestamp ?? now;\n\t\tconst durationInMs = typeof resolutionPayload?.durationInMs === 'number'\n\t\t\t? resolutionPayload.durationInMs\n\t\t\t: Math.max(0, resolvedAt - (active?.raisedAt ?? raisedAt ?? resolvedAt));\n\n\t\tthis._notify('client-issue-resolved', {\n\t\t\t...this.eventScope,\n\t\t\tresolvedIssue: {\n\t\t\t\tkey: active?.key ?? issue.key ?? `${this.clientId}:${type}:${raisedAt ?? resolvedAt}`,\n\t\t\t\ttype,\n\t\t\t\tclientId: this.clientId,\n\t\t\t\tcallId: this.call.callId,\n\t\t\t\traisedAt: active?.raisedAt ?? raisedAt ?? resolvedAt,\n\t\t\t\tobservedAt: active?.observedAt ?? now,\n\t\t\t\tpayload: active?.payload,\n\t\t\t\tpeerConnectionId: active?.peerConnectionId,\n\t\t\t\ttrackId: active?.trackId,\n\t\t\t\tresolvedAt,\n\t\t\t\tobservedResolvedAt: now,\n\t\t\t\tdurationInMs,\n\t\t\t\tcomment: typeof resolutionPayload?.comment === 'string' ? resolutionPayload.comment : undefined,\n\t\t\t\tresolutionPayload,\n\t\t\t\tresolvedBy,\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate _processClientEvent(event: ClientEvent, postBuffer?: ClientEvent[]) {\n\t\t// An event deferred to `postBuffer` is processed again once the sample's peer connections are\n\t\t// applied; `client-event` is announced then, exactly once, rather than on both passes.\n\t\tconst deferredBefore = postBuffer?.length ?? 0;\n\n\t\tswitch (event.type) {\n\t\t\tcase ClientEventTypes.CLIENT_JOINED: {\n\t\t\t\tif (event.timestamp) {\n\t\t\t\t\tif (!this.joinedAt) {\n\t\t\t\t\t\tthis.joinedAt = event.timestamp;\n\t\t\t\t\t\tthis.emit('joined');\n\t\t\t\t\t\tthis._notify('client-joined', { ...this.eventScope });\n\t\t\t\t\t} else if (this.joinedAt < event.timestamp) {\n\t\t\t\t\t\tthis._notify('client-rejoined', { ...this.eventScope, timestamp: event.timestamp });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.joinedAt = event.timestamp;\n\t\t\t\t\t\tlogger.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlogger.debug('Client %s joined at %o', this.clientId, event);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.CLIENT_LEFT: {\n\t\t\t\tif (event.timestamp) {\n\t\t\t\t\tif (!this.leftAt) {\n\t\t\t\t\t\tthis.leftAt = event.timestamp;\n\t\t\t\t\t\tthis.emit('left');\n\t\t\t\t\t\tthis._notify('client-left', { ...this.eventScope });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn(`Client ${this.clientId} leftAt timestamp was already set`);\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t\tlogger.debug('Client %s left at %o', this.clientId, event);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.PEER_CONNECTION_OPENED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\n\t\t\t\t\tif (observedPeerConnection) {\n\t\t\t\t\t\tobservedPeerConnection.openedAt = event.timestamp;\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o', event);\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.PEER_CONNECTION_CLOSED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\n\t\t\t\t\tif (observedPeerConnection) {\n\t\t\t\t\t\tobservedPeerConnection.closedAt = event.timestamp;\n\t\t\t\t\t\tthis._closedPeerConnectionIds.add(observedPeerConnection.peerConnectionId);\n\t\t\t\t\t\tthis._peerConnectionsToClose.add(observedPeerConnection);\n\t\t\t\t\t\t// Outside of `accept()` there is no sample left to apply to it: close now.\n\t\t\t\t\t\tif (!this._activeSample) this._closeReportedPeerConnections();\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.MEDIA_TRACK_ADDED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\t\t\t\t\tconst observedTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId) ?? observedPeerConnection?.observedOutboundTracks.get(payload.trackId);\n\n\t\t\t\t\tif (observedTrack) {\n\t\t\t\t\t\tobservedTrack.addedAt = event.timestamp;\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.MEDIA_TRACK_REMOVED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\t\t\t\t\tconst observedTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId) ?? observedPeerConnection?.observedOutboundTracks.get(payload.trackId);\n\n\t\t\t\t\tif (observedTrack) {\n\t\t\t\t\t\tobservedTrack.removedAt = event.timestamp;\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.DATA_CHANNEL_OPEN: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.dataChannelId && typeof payload.dataChannelId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\t\t\t\t\tconst observedDataChannel = observedPeerConnection?.observedDataChannels.get(payload.dataChannelId);\n\n\t\t\t\t\tif (observedDataChannel) {\n\t\t\t\t\t\tobservedDataChannel.addedAt = event.timestamp;\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.DATA_CHANNEL_CLOSED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.dataChannelId && typeof payload.dataChannelId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\t\t\t\t\tconst observedDataChannel = observedPeerConnection?.observedDataChannels.get(payload.dataChannelId);\n\n\t\t\t\t\tif (observedDataChannel) {\n\t\t\t\t\t\tobservedDataChannel.removedAt = event.timestamp;\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.MEDIA_TRACK_MUTED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\t\t\t\t\tconst observedInboundTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId);\n\t\t\t\t\tconst observedOutboundTrack = observedPeerConnection?.observedOutboundTracks.get(payload.trackId);\n\n\t\t\t\t\tif (observedPeerConnection && observedInboundTrack) {\n\t\t\t\t\t\tobservedInboundTrack.muted = true;\n\t\t\t\t\t\tthis._notify('inbound-track-muted', { ...this.eventScope, observedPeerConnection, observedInboundTrack });\n\t\t\t\t\t} else if (observedPeerConnection && observedOutboundTrack) {\n\t\t\t\t\t\tobservedOutboundTrack.muted = true;\n\t\t\t\t\t\tthis._notify('outbound-track-muted', { ...this.eventScope, observedPeerConnection, observedOutboundTrack });\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\t// The track (or its peer connection) arrives in this same sample; retry once it is applied.\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.MEDIA_TRACK_UNMUTED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.trackId && typeof payload.trackId === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\t\t\t\t\tconst observedInboundTrack = observedPeerConnection?.observedInboundTracks.get(payload.trackId);\n\t\t\t\t\tconst observedOutboundTrack = observedPeerConnection?.observedOutboundTracks.get(payload.trackId);\n\n\t\t\t\t\tif (observedPeerConnection && observedInboundTrack) {\n\t\t\t\t\t\tobservedInboundTrack.muted = false;\n\t\t\t\t\t\tthis._notify('inbound-track-unmuted', { ...this.eventScope, observedPeerConnection, observedInboundTrack });\n\t\t\t\t\t} else if (observedPeerConnection && observedOutboundTrack) {\n\t\t\t\t\t\tobservedOutboundTrack.muted = false;\n\t\t\t\t\t\tthis._notify('outbound-track-unmuted', { ...this.eventScope, observedPeerConnection, observedOutboundTrack });\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\t// The track (or its peer connection) arrives in this same sample; retry once it is applied.\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.ICE_CONNECTION_STATE_CHANGED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.iceConnectionState && typeof payload.iceConnectionState === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\n\t\t\t\t\tif (observedPeerConnection) {\n\t\t\t\t\t\tobservedPeerConnection.iceConnectionState = payload.iceConnectionState as 'new' | 'checking' | 'connected' | 'completed' | 'failed' | 'disconnected' | 'closed';\n\t\t\t\t\t\tthis._notify('ice-connection-state-changed', { ...this.eventScope, observedPeerConnection, state: payload.iceConnectionState });\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.ICE_GATHERING_STATE_CHANGED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.iceGatheringState && typeof payload.iceGatheringState === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\n\t\t\t\t\tif (observedPeerConnection) {\n\t\t\t\t\t\tobservedPeerConnection.iceGatheringState = payload.iceGatheringState as 'new' | 'gathering' | 'complete';\n\t\t\t\t\t\tthis._notify('ice-gathering-state-changed', { ...this.eventScope, observedPeerConnection, state: payload.iceGatheringState });\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientEventTypes.PEER_CONNECTION_STATE_CHANGED: {\n\t\t\t\tconst payload = parseJsonAs<Record<string, unknown>>(event.payload);\n\n\t\t\t\tif (payload?.peerConnectionId && typeof payload.peerConnectionId === 'string' && payload?.peerConnectionState && typeof payload.peerConnectionState === 'string') {\n\t\t\t\t\tconst observedPeerConnection = this.observedPeerConnections.get(payload.peerConnectionId);\n\n\t\t\t\t\tif (observedPeerConnection) {\n\t\t\t\t\t\tobservedPeerConnection.connectionState = payload.peerConnectionState as 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';\n\t\t\t\t\t\tthis._notify('connection-state-changed', { ...this.eventScope, observedPeerConnection, state: payload.peerConnectionState });\n\t\t\t\t\t} else if (postBuffer) {\n\t\t\t\t\t\tpostBuffer.push(event);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.warn('Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o', event);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (postBuffer && deferredBefore < postBuffer.length) return;\n\n\t\tthis._notify('client-event', { ...this.eventScope, event });\n\t}\n\n\tprivate _closeReportedPeerConnections() {\n\t\tif (this._peerConnectionsToClose.size === 0) return;\n\n\t\tconst peerConnections = [ ...this._peerConnectionsToClose ];\n\n\t\tthis._peerConnectionsToClose.clear();\n\t\tfor (const observedPeerConnection of peerConnections) observedPeerConnection.close();\n\t}\n\n\tprivate _updatePeerConnection(sample: PeerConnectionSample, context?: AcceptContext): ObservedPeerConnection | undefined {\n\t\tlet observedPeerConnection = this.observedPeerConnections.get(sample.peerConnectionId);\n\n\t\tif (!observedPeerConnection) {\n\t\t\tif (this._closedPeerConnectionIds.has(sample.peerConnectionId)) {\n\t\t\t\tlogger.debug('Ignoring stats of peer connection %s, already closed by client %s', sample.peerConnectionId, this.clientId);\n\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tif (!sample.peerConnectionId) {\n\t\t\t\treturn (logger.warn(\n\t\t\t\t\t`ObservedClient received an invalid PeerConnectionSample (missing peerConnectionId field). ClientId: ${this.clientId}, CallId: ${this.call.callId}`,\n\t\t\t\t\tsample\n\t\t\t\t), void 0);\n\t\t\t}\n\n\t\t\tconst newObservedPeerConnection = new ObservedPeerConnection(sample.peerConnectionId, this);\n\n\t\t\tnewObservedPeerConnection.once('close', () => {\n\t\t\t\tthis.observedPeerConnections.delete(sample.peerConnectionId);\n\t\t\t});\n\t\t\tthis.observedPeerConnections.set(sample.peerConnectionId, newObservedPeerConnection);\n\n\t\t\tthis._notify('peer-connection-added', { ...this.eventScope, observedPeerConnection: newObservedPeerConnection });\n\n\t\t\tobservedPeerConnection = newObservedPeerConnection;\n\t\t}\n\n\t\tobservedPeerConnection.accept(sample, context);\n\n\t\treturn observedPeerConnection;\n\t}\n\n\tprivate _mergePendingInjections(sample: ClientSample): ClientSample {\n\t\tif (this.closed) return sample;\n\n\t\tif (this._pendingInjections.clientEvents) {\n\t\t\tif (!sample.clientEvents) sample.clientEvents = [];\n\t\t\tsample.clientEvents.push(...this._pendingInjections.clientEvents);\n\n\t\t\tthis._pendingInjections.clientEvents = undefined;\n\t\t}\n\n\t\tif (this._pendingInjections.clientIssues) {\n\t\t\tif (!sample.clientIssues) sample.clientIssues = [];\n\t\t\tsample.clientIssues.push(...this._pendingInjections.clientIssues);\n\n\t\t\tthis._pendingInjections.clientIssues = undefined;\n\t\t}\n\n\t\tif (this._pendingInjections.extensionStats) {\n\t\t\tif (!sample.extensionStats) sample.extensionStats = [];\n\t\t\tsample.extensionStats.push(...this._pendingInjections.extensionStats);\n\n\t\t\tthis._pendingInjections.extensionStats = undefined;\n\t\t}\n\n\t\tif (this._pendingInjections.attachments) {\n\t\t\tif (!sample.attachments) sample.attachments = {};\n\t\t\tObject.assign(sample.attachments, this._pendingInjections.attachments);\n\n\t\t\tthis._pendingInjections.attachments = undefined;\n\t\t}\n\n\t\tif (this._pendingInjections.clientMetaItems) {\n\t\t\tif (!sample.clientMetaItems) sample.clientMetaItems = [];\n\t\t\tsample.clientMetaItems.push(...this._pendingInjections.clientMetaItems);\n\n\t\t\tthis._pendingInjections.clientMetaItems = undefined;\n\t\t}\n\n\t\treturn sample;\n\t}\n\n\tprivate _flushPendingInjections() {\n\t\tconst hasPending = Boolean(\n\t\t\tthis._pendingInjections.clientEvents ||\n\t\t\tthis._pendingInjections.clientIssues ||\n\t\t\tthis._pendingInjections.extensionStats ||\n\t\t\tthis._pendingInjections.clientMetaItems ||\n\t\t\tthis._pendingInjections.attachments,\n\t\t);\n\n\t\tif (!hasPending) return;\n\n\t\tconst finalSample: ClientSample = { timestamp: Date.now() };\n\n\t\tthis._mergePendingInjections(finalSample);\n\n\t\tfor (const clientEvent of finalSample.clientEvents ?? []) this._processClientEvent(clientEvent);\n\t\tfor (const metaData of finalSample.clientMetaItems ?? []) this.addMetadata(metaData);\n\t\tfor (const issue of finalSample.clientIssues ?? []) this.addIssue(issue);\n\t\tfor (const extensionStat of finalSample.extensionStats ?? []) this.addExtensionStats(extensionStat);\n\t\tif (finalSample.attachments) this.attachments = { ...this.attachments, ...finalSample.attachments };\n\n\t\ttry {\n\t\t\tthis.sink?.write(finalSample);\n\t\t} catch (err) {\n\t\t\tlogger.warn('ClientSampleSink.write failed during injection flush for client %s: %o', this.clientId, err);\n\t\t}\n\t}\n\n\t/** Emit an Observer-bus event scoped to this client (or a peer connection under it). */\n\tprivate _notify<K extends keyof ObserverEvents>(type: K, ...args: ObserverEvents[K]): void {\n\t\tthis.call.observer.emit(type, ...args);\n\t}\n}\n","import { EventEmitter } from 'events';\nimport { ObservedClient } from './ObservedClient';\nimport { CertificateStats, CodecStats, DataChannelStats, IceCandidateStats, InboundRtpStats, InboundTrackSample, MediaPlayoutStats, MediaSourceStats, OutboundRtpStats, OutboundTrackSample, PeerConnectionSample, PeerConnectionTransportStats, RemoteInboundRtpStats, RemoteOutboundRtpStats } from './schema/ClientSample';\nimport { ObservedInboundRtp } from './ObservedInboundRtp';\nimport { createLogger } from './common/logger';\nimport { MediaKind } from './common/types';\nimport { ObservedOutboundRtp } from './ObservedOutboundRtp';\nimport { ObservedCertificate } from './ObservedCertificate';\nimport { ObservedCodec } from './ObservedCodec';\nimport { ObservedDataChannel } from './ObservedDataChannel';\nimport { ObservedIceCandidate } from './ObservedIceCandidate';\nimport { ObservedIceCandidatePair } from './ObservedIceCandidatePair';\nimport { ObservedIceTransport } from './ObservedIceTransport';\nimport { ObservedMediaSource } from './ObservedMediaSource';\nimport { ObservedPeerConnectionTransport } from './ObservedPeerConnectionTransport';\nimport { ObservedMediaPlayout } from './ObservedMediaPlayout';\nimport { ObservedRemoteInboundRtp } from './ObservedRemoteInboundRtp';\nimport { ObservedRemoteOutboundRtp } from './ObservedRemoteOutboundRtp';\nimport { ObservedInboundTrack } from './ObservedInboundTrack';\nimport { ObservedOutboundTrack } from './ObservedOutboundTrack';\nimport { CalculatedScore } from './scores/CalculatedScore';\nimport { ObservedTurnServer } from './ObservedTurnServer';\nimport { percentileOfSorted } from './utils/stats';\nimport { isPublicIpAddress } from './common/utils';\n\n/** Sorting in place is deliberate: these arrays are local per-tick scratch, so a copy is pure waste. */\nconst ascending = (a: number, b: number) => a - b;\n\nimport type { AcceptContext } from './Observer';\nimport type { ObserverEvents, ObservedPeerConnectionScope } from './ObserverEvents';\n\nconst logger = createLogger('ObservedPeerConnection');\n\n/**\n * The public address a *local* ICE candidate reveals, if any. For a `relay` candidate `address` is\n * the TURN server's relay address, so only `relatedAddress` (the client's mapped address as the TURN\n * server saw it) can be the client's.\n */\nfunction publicAddressOfLocalCandidate(candidate?: ObservedIceCandidate): string | undefined {\n\tif (!candidate) return undefined;\n\tif (candidate.candidateType !== 'relay' && isPublicIpAddress(candidate.address)) return candidate.address;\n\tif (isPublicIpAddress(candidate.relatedAddress)) return candidate.relatedAddress;\n\n\treturn undefined;\n}\n\n// Local lifecycle/coordination events only. All consumer-facing events are\n// emitted on the Observer bus (see ObserverEvents). `close` and the two track\n// `removed-*` events stay local because the parent ObservedClient wires to them\n// for teardown and track-report emission.\nexport type ObservedPeerConnectionEvents = {\n\t'removed-inbound-track': [ObservedInboundTrack];\n\t'removed-outbound-track': [ObservedOutboundTrack];\n\tclose: [];\n};\n\nexport declare interface ObservedPeerConnection {\n\ton<U extends keyof ObservedPeerConnectionEvents>(\n\t\tevent: U,\n\t\tlistener: (...args: ObservedPeerConnectionEvents[U]) => void\n\t): this;\n\toff<U extends keyof ObservedPeerConnectionEvents>(\n\t\tevent: U,\n\t\tlistener: (...args: ObservedPeerConnectionEvents[U]) => void\n\t): this;\n\tonce<U extends keyof ObservedPeerConnectionEvents>(\n\t\tevent: U,\n\t\tlistener: (...args: ObservedPeerConnectionEvents[U]) => void\n\t): this;\n\temit<U extends keyof ObservedPeerConnectionEvents>(event: U, ...args: ObservedPeerConnectionEvents[U]): boolean;\n}\n\nexport class ObservedPeerConnection extends EventEmitter {\n\tprivate _visited = true;\n\n\tpublic appData?: Record<string, unknown>;\n\tpublic readonly calculatedScore: CalculatedScore = {\n\t\tweight: 1,\n\t\tvalue: undefined,\n\t};\n\n\tpublic closed = false;\n\t// timestamp of the PEER_CONNECTION_OPENED event\n\tpublic openedAt?: number;\n\t// timestamp of the PEER_CONNECTION_CLOSED event\n\tpublic closedAt?: number;\n\tpublic updated = Date.now();\n\n\tpublic connectionState?: 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';\n\tpublic iceConnectionState?: 'new' | 'checking' | 'connected' | 'completed' | 'failed' | 'disconnected' | 'closed';\n\tpublic iceGatheringState?: 'new' | 'gathering' | 'complete';\n\n\tpublic availableIncomingBitrate = 0;\n\tpublic availableOutgoingBitrate = 0;\n\tpublic totalInboundPacketsLost = 0;\n\tpublic totalInboundPacketsReceived = 0;\n\tpublic totalOutboundPacketsSent = 0;\n\tpublic totalDataChannelBytesSent = 0;\n\tpublic totalDataChannelBytesReceived = 0;\n\tpublic totalDataChannelMessagesSent = 0;\n\tpublic totalDataChannelMessagesReceived = 0;\n\n\tpublic totalSentAudioBytes = 0;\n\tpublic totalSentVideoBytes = 0;\n\tpublic totalSentAudioPackets = 0;\n\tpublic totalSentVideoPackets = 0;\n\tpublic totalReceivedAudioPacktes = 0;\n\tpublic totalReceivedVideoPackets = 0;\n\tpublic totalReceivedAudioBytes = 0;\n\tpublic totalReceivedVideoBytes = 0;\n\n\tpublic deltaInboundPacketsLost = 0;\n\tpublic deltaInboundPacketsReceived = 0;\n\tpublic deltaOutboundPacketsSent = 0;\n\tpublic deltaDataChannelBytesSent = 0;\n\tpublic deltaDataChannelBytesReceived = 0;\n\tpublic deltaDataChannelMessagesSent = 0;\n\tpublic deltaDataChannelMessagesReceived = 0;\n\tpublic deltaInboundReceivedBytes = 0;\n\tpublic deltaOutboundSentBytes = 0;\n\n\tpublic deltaReceivedAudioBytes = 0;\n\tpublic deltaReceivedVideoBytes = 0;\n\tpublic deltaReceivedAudioPackets = 0;\n\tpublic deltaReceivedVideoPackets = 0;\n\tpublic deltaSentAudioBytes = 0;\n\tpublic deltaSentVideoBytes = 0;\n\tpublic deltaSentAudioPackets = 0;\n\tpublic deltaSentVideoPackets = 0;\n\tpublic deltaTransportSentBytes = 0;\n\tpublic deltaTransportReceivedBytes = 0;\n\n\tpublic receivingPacketsPerSecond = 0;\n\tpublic sendingPacketsPerSecond = 0;\n\tpublic sendingAudioBitrate = 0;\n\tpublic sendingVideoBitrate = 0;\n\tpublic receivingAudioBitrate = 0;\n\tpublic receivingVideoBitrate = 0;\n\n\t/**\n\t * Median round-trip time of the tick, in ms.\n\t *\n\t * Prefers {@link rtcpRttInMs} and falls back to {@link iceRttInMs}, so within one tick it always\n\t * reports **one** kind of round trip. It used to be the median of both mixed together, which was\n\t * a bug: the mixing ratio changed as streams came and went, so the value moved for reasons that\n\t * had nothing to do with the network.\n\t */\n\tpublic currentRttInMs?: number;\n\n\t/**\n\t * RTT measured by ICE/STUN consent checks, in ms — the trip to **whatever terminates ICE**. In an\n\t * SFU topology that is the SFU, so this is the client↔SFU leg, not client↔client.\n\t */\n\tpublic iceRttInMs?: number;\n\n\t/**\n\t * RTT reported by RTCP receiver reports, in ms — an **end-to-end** media-path round trip.\n\t *\n\t * Not the same trip as {@link iceRttInMs}; the difference between the two is roughly the far side\n\t * of the SFU (see {@link sfuHopRttInMs}).\n\t */\n\tpublic rtcpRttInMs?: number;\n\n\t/**\n\t * `rtcpRttInMs - iceRttInMs`, when both are known — an estimate of everything *past* the SFU.\n\t *\n\t * Useful for splitting \"this client's own last mile is slow\" (high `iceRttInMs`) from \"the path\n\t * beyond the SFU is slow\" (low ICE, high hop).\n\t */\n\tpublic sfuHopRttInMs?: number;\n\tpublic currentJitter?: number;\n\n\tpublic usingTCP = false;\n\tpublic usingTURN = false;\n\tpublic publicAddress?: string;\n\tpublic publicAddressUpdatedAt?: number;\n\n\tpublic observedTurnServer?: ObservedTurnServer;\n\tpublic readonly observedCertificates = new Map<string, ObservedCertificate>();\n\tpublic readonly observedCodecs = new Map<string, ObservedCodec>();\n\tpublic readonly observedDataChannels = new Map<string, ObservedDataChannel>();\n\tpublic readonly observedIceCandidates = new Map<string, ObservedIceCandidate>();\n\tpublic readonly observedIceCandidatesPair = new Map<string, ObservedIceCandidatePair>();\n\tpublic readonly observedIceTransports = new Map<string, ObservedIceTransport>();\n\tpublic readonly observedInboundRtps = new Map<number, ObservedInboundRtp>();\n\tpublic readonly observedInboundTracks = new Map<string, ObservedInboundTrack>();\n\tpublic readonly observedMediaPlayouts = new Map<string, ObservedMediaPlayout>();\n\tpublic readonly observedMediaSources = new Map<string, ObservedMediaSource>();\n\tpublic readonly observedOutboundRtps = new Map<number, ObservedOutboundRtp>();\n\tpublic readonly observedOutboundTracks = new Map<string, ObservedOutboundTrack>();\n\tpublic readonly observedPeerConnectionTransports = new Map<string, ObservedPeerConnectionTransport>();\n\tpublic readonly observedRemoteInboundRtps = new Map<number, ObservedRemoteInboundRtp>();\n\tpublic readonly observedRemoteOutboundRtps = new Map<number, ObservedRemoteOutboundRtp>();\n\n\tprivate readonly eventScope: ObservedPeerConnectionScope;\n\n\tpublic constructor(public readonly peerConnectionId: string, public readonly client: ObservedClient) {\n\t\tsuper();\n\t\tthis.setMaxListeners(Infinity);\n\t\tthis.eventScope = {\n\t\t\tobserver: client.call.observer,\n\t\t\tobservedCall: client.call,\n\t\t\tobservedClient: client,\n\t\t\tobservedPeerConnection: this,\n\t\t};\n\t}\n\n\tpublic get score() {\n\t\treturn this.calculatedScore.value;\n\t}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic get codecs() {\n\t\treturn [ ...this.observedCodecs.values() ];\n\t}\n\n\tpublic get inboundRtps() {\n\t\treturn [ ...this.observedInboundRtps.values() ];\n\t}\n\n\tpublic get remoteOutboundRtps() {\n\t\treturn [ ...this.observedRemoteOutboundRtps.values() ];\n\t}\n\n\tpublic get outboundRtps() {\n\t\treturn [ ...this.observedOutboundRtps.values() ];\n\t}\n\n\tpublic get remoteInboundRtps() {\n\t\treturn [ ...this.observedRemoteInboundRtps.values() ];\n\t}\n\n\tpublic get mediaSources() {\n\t\treturn [ ...this.observedMediaSources.values() ];\n\t}\n\n\tpublic get mediaPlayouts() {\n\t\treturn [ ...this.observedMediaPlayouts.values() ];\n\t}\n\n\tpublic get dataChannels() {\n\t\treturn [ ...this.observedDataChannels.values() ];\n\t}\n\n\tpublic get peerConnectionTransports() {\n\t\treturn [ ...this.observedPeerConnectionTransports.values() ];\n\t}\n\n\tpublic get iceTransports() {\n\t\treturn [ ...this.observedIceTransports.values() ];\n\t}\n\n\tpublic get iceCandidates() {\n\t\treturn [ ...this.observedIceCandidates.values() ];\n\t}\n\n\tpublic get iceCandidatePairs() {\n\t\treturn [ ...this.observedIceCandidatesPair.values() ];\n\t}\n\n\tpublic get certificates() {\n\t\treturn [ ...this.observedCertificates.values() ];\n\t}\n\n\tpublic get selectedIceCandidatePairs() {\n\t\treturn this.iceTransports.map((iceTransport) => iceTransport.getSelectedCandidatePair())\n\t\t\t.filter((pair) => pair !== undefined) as ObservedIceCandidatePair[];\n\t}\n\n\tpublic get selectedIceCandiadtePairForTurn() {\n\t\t// The ICE server `url` is only exposed on *local* candidates (it identifies the server\n\t\t// the candidate was obtained from); remote candidates never carry it. `turn` also\n\t\t// matches `turns:` (TURN over TLS).\n\t\treturn this.selectedIceCandidatePairs\n\t\t\t.filter((pair) => {\n\t\t\t\tconst localCandidate = pair.getLocalCandidate();\n\n\t\t\t\treturn localCandidate?.candidateType === 'relay' && localCandidate?.url?.startsWith('turn') === true;\n\t\t\t});\n\t}\n\n\tpublic close() {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\n\t\tthis.observedCertificates.forEach((observedCertificate) => this._notify('certificate-removed', { ...this.eventScope, observedCertificate }));\n\t\tthis.observedCodecs.forEach((observedCodec) => this._notify('codec-removed', { ...this.eventScope, observedCodec }));\n\t\tthis.observedDataChannels.forEach((observedDataChannel) => this._notify('data-channel-removed', { ...this.eventScope, observedDataChannel }));\n\t\tthis.observedIceCandidates.forEach((observedIceCandidate) => this._notify('ice-candidate-removed', { ...this.eventScope, observedIceCandidate }));\n\t\tthis.observedIceCandidatesPair.forEach((pair) => this._notify('ice-candidate-pair-removed', { ...this.eventScope, observedIceCandidatePair: pair }));\n\t\tthis.observedIceTransports.forEach((transport) => this._notify('ice-transport-removed', { ...this.eventScope, observedIceTransport: transport }));\n\t\tthis.observedInboundRtps.forEach((rtp) => this._notify('inbound-rtp-removed', { ...this.eventScope, observedInboundRtp: rtp }));\n\t\tthis.observedInboundTracks.forEach((track) => { this.emit('removed-inbound-track', track); this._notify('inbound-track-removed', { ...this.eventScope, observedInboundTrack: track }); });\n\t\tthis.observedMediaPlayouts.forEach((playout) => this._notify('media-playout-removed', { ...this.eventScope, observedMediaPlayout: playout }));\n\t\tthis.observedMediaSources.forEach((source) => this._notify('media-source-removed', { ...this.eventScope, observedMediaSource: source }));\n\t\tthis.observedOutboundRtps.forEach((rtp) => this._notify('outbound-rtp-removed', { ...this.eventScope, observedOutboundRtp: rtp }));\n\t\tthis.observedOutboundTracks.forEach((track) => { this.emit('removed-outbound-track', track); this._notify('outbound-track-removed', { ...this.eventScope, observedOutboundTrack: track }); });\n\t\tthis.observedPeerConnectionTransports.forEach((transport) => this._notify('peer-connection-transport-removed', { ...this.eventScope, observedPeerConnectionTransport: transport }));\n\t\tthis.observedRemoteInboundRtps.forEach((rtp) => this._notify('remote-inbound-rtp-removed', { ...this.eventScope, observedRemoteInboundRtp: rtp }));\n\t\tthis.observedRemoteOutboundRtps.forEach((rtp) => this._notify('remote-outbound-rtp-removed', { ...this.eventScope, observedRemoteOutboundRtp: rtp }));\n\n\t\tthis.observedCertificates.clear();\n\t\tthis.observedCodecs.clear();\n\t\tthis.observedDataChannels.clear();\n\t\tthis.observedIceCandidates.clear();\n\t\tthis.observedIceCandidatesPair.clear();\n\t\tthis.observedIceTransports.clear();\n\t\tthis.observedInboundRtps.clear();\n\t\tthis.observedInboundTracks.clear();\n\t\tthis.observedMediaPlayouts.clear();\n\t\tthis.observedMediaSources.clear();\n\t\tthis.observedOutboundRtps.clear();\n\t\tthis.observedOutboundTracks.clear();\n\t\tthis.observedPeerConnectionTransports.clear();\n\t\tthis.observedRemoteInboundRtps.clear();\n\t\tthis.observedRemoteOutboundRtps.clear();\n\n\t\tthis.client.call.observer.observedTURN.removePeerConnection(this);\n\n\t\tif (!this.closedAt) this.closedAt = Date.now();\n\n\t\tthis.emit('close');\n\t\tthis._notify('peer-connection-closed', { ...this.eventScope });\n\t}\n\n\tpublic accept(sample: PeerConnectionSample, context?: AcceptContext) {\n\t\tif (this.closed) return;\n\t\tthis._visited = true;\n\n\t\tif (context) this.appData = { ...this.appData, ...context };\n\n\t\tthis.availableIncomingBitrate = 0;\n\t\tthis.availableOutgoingBitrate = 0;\n\t\tthis.deltaInboundPacketsLost = 0;\n\t\tthis.deltaInboundPacketsReceived = 0;\n\t\tthis.deltaOutboundPacketsSent = 0;\n\t\tthis.deltaDataChannelBytesSent = 0;\n\t\tthis.deltaDataChannelBytesReceived = 0;\n\t\tthis.deltaDataChannelMessagesSent = 0;\n\t\tthis.deltaDataChannelMessagesReceived = 0;\n\t\tthis.deltaInboundReceivedBytes = 0;\n\t\tthis.deltaOutboundSentBytes = 0;\n\t\tthis.deltaReceivedAudioBytes = 0;\n\t\tthis.deltaReceivedVideoBytes = 0;\n\t\tthis.deltaReceivedAudioPackets = 0;\n\t\tthis.deltaReceivedVideoPackets = 0;\n\t\tthis.deltaSentAudioBytes = 0;\n\t\tthis.deltaSentVideoBytes = 0;\n\t\tthis.deltaSentAudioPackets = 0;\n\t\tthis.deltaSentVideoPackets = 0;\n\t\tthis.deltaTransportReceivedBytes = 0;\n\t\tthis.deltaTransportSentBytes = 0;\n\n\t\tthis.sendingAudioBitrate = 0;\n\t\tthis.sendingVideoBitrate = 0;\n\t\tthis.receivingAudioBitrate = 0;\n\t\tthis.receivingVideoBitrate = 0;\n\n\t\tconst now = Date.now();\n\t\tconst elapsedTimeInMs = now - this.updated;\n\t\tconst elapsedTimeInSec = elapsedTimeInMs / 1000;\n\t\t// Kept in two buckets: ICE/STUN and RTCP measure different round trips and must not be blended.\n\t\tconst iceRttMeasurementsInSec: number[] = [];\n\t\tconst rtcpRttMeasurementsInSec: number[] = [];\n\t\tconst jitterMeasurements: number[] = [];\n\n\t\tif (sample.certificates) {\n\t\t\tfor (const certificate of sample.certificates) {\n\t\t\t\tthis._updateCertificateStats(certificate);\n\t\t\t}\n\t\t}\n\t\tif (sample.codecs) {\n\t\t\tfor (const codec of sample.codecs) {\n\t\t\t\tthis._updateCodecStats(codec);\n\t\t\t}\n\t\t}\n\t\tif (sample.dataChannels) {\n\t\t\tfor (const dataChannel of sample.dataChannels) {\n\t\t\t\tconst observedDataChannel = this._updateDataChannelStats(dataChannel);\n\n\t\t\t\tif (!observedDataChannel) continue;\n\n\t\t\t\tthis.deltaDataChannelBytesSent += observedDataChannel.deltaBytesSent;\n\t\t\t\tthis.deltaDataChannelBytesReceived += observedDataChannel.deltaBytesReceived;\n\t\t\t\tthis.deltaDataChannelMessagesSent += observedDataChannel.deltaMessagesSent;\n\t\t\t\tthis.deltaDataChannelMessagesReceived += observedDataChannel.deltaMessagesReceived;\n\t\t\t}\n\t\t}\n\t\tif (sample.iceCandidates) {\n\t\t\tfor (const iceCandidate of sample.iceCandidates) {\n\t\t\t\tthis._updateIceCandidateStats(iceCandidate);\n\t\t\t}\n\t\t}\n\t\tif (sample.iceCandidatePairs) {\n\t\t\tfor (const iceCandidatePair of sample.iceCandidatePairs) {\n\t\t\t\tconst observedCandidatePair = this._updateIceCandidatePairStats(iceCandidatePair);\n\n\t\t\t\tif (!observedCandidatePair) continue;\n\n\t\t\t\tif (observedCandidatePair.currentRoundTripTime) {\n\t\t\t\t\t// ICE/STUN round trip — terminates at the SFU, kept apart from the RTCP one.\n\t\t\t\t\ticeRttMeasurementsInSec.push(observedCandidatePair.currentRoundTripTime);\n\t\t\t\t}\n\t\t\t\tif (observedCandidatePair.availableIncomingBitrate) {\n\t\t\t\t\tthis.availableIncomingBitrate += observedCandidatePair.availableIncomingBitrate;\n\t\t\t\t}\n\t\t\t\tif (observedCandidatePair.availableOutgoingBitrate) {\n\t\t\t\t\tthis.availableOutgoingBitrate += observedCandidatePair.availableOutgoingBitrate;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst iceTransportsWithChangedSelection: ObservedIceTransport[] = [];\n\n\t\tif (sample.iceTransports) {\n\t\t\tfor (const iceTransport of sample.iceTransports) {\n\t\t\t\tconst observedIceTransport = this._updateIceTransportStats(iceTransport);\n\n\t\t\t\tif (!observedIceTransport) continue;\n\n\t\t\t\tif (observedIceTransport.selectedCandidatePairChanged) {\n\t\t\t\t\ticeTransportsWithChangedSelection.push(observedIceTransport);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (sample.inboundRtps) {\n\t\t\tfor (const inboundRtp of sample.inboundRtps) {\n\t\t\t\tconst observedInboundRtp = this._updateInboundRtpStats(inboundRtp);\n\n\t\t\t\tif (!observedInboundRtp) continue;\n\n\t\t\t\tthis.deltaInboundPacketsLost += observedInboundRtp.deltaLostPackets;\n\t\t\t\tthis.deltaInboundPacketsReceived += observedInboundRtp.deltaReceivedPackets;\n\t\t\t\tthis.deltaInboundReceivedBytes += observedInboundRtp.deltaBytesReceived;\n\n\t\t\t\tswitch (inboundRtp.kind) {\n\t\t\t\t\tcase 'audio':\n\t\t\t\t\t\tthis.deltaReceivedAudioBytes += observedInboundRtp.deltaBytesReceived;\n\t\t\t\t\t\tthis.deltaReceivedAudioPackets += observedInboundRtp.deltaReceivedPackets;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'video':\n\t\t\t\t\t\tthis.deltaReceivedVideoBytes += observedInboundRtp.deltaBytesReceived;\n\t\t\t\t\t\tthis.deltaReceivedVideoPackets += observedInboundRtp.deltaReceivedPackets;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (observedInboundRtp.jitter) {\n\t\t\t\t\tjitterMeasurements.push(observedInboundRtp.jitter);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (sample.mediaPlayouts) {\n\t\t\tfor (const mediaPlayout of sample.mediaPlayouts) {\n\t\t\t\tthis._updateMediaPlayoutStats(mediaPlayout);\n\t\t\t}\n\t\t}\n\t\tif (sample.mediaSources) {\n\t\t\tfor (const mediaSource of sample.mediaSources) {\n\t\t\t\tthis._updateMediaSourceStats(mediaSource);\n\t\t\t}\n\t\t}\n\t\tif (sample.outboundRtps) {\n\t\t\tfor (const outboundRtp of sample.outboundRtps) {\n\t\t\t\tconst observedOutboundRtp = this._updateOutboundRtpStats(outboundRtp);\n\n\t\t\t\tif (!observedOutboundRtp) continue;\n\n\t\t\t\tthis.deltaOutboundPacketsSent += observedOutboundRtp.deltaPacketsSent ?? 0;\n\t\t\t\tthis.deltaOutboundSentBytes += observedOutboundRtp.deltaBytesSent ?? 0;\n\n\t\t\t\tswitch (outboundRtp.kind) {\n\t\t\t\t\tcase 'audio':\n\t\t\t\t\t\tthis.deltaSentAudioBytes += observedOutboundRtp.deltaBytesSent;\n\t\t\t\t\t\tthis.deltaSentAudioPackets += observedOutboundRtp.deltaPacketsSent;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'video':\n\t\t\t\t\t\tthis.deltaSentVideoBytes += observedOutboundRtp.deltaBytesSent;\n\t\t\t\t\t\tthis.deltaSentVideoPackets += observedOutboundRtp.deltaPacketsSent;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t\tif (sample.peerConnectionTransports) {\n\t\t\tfor (const peerConnectionTransport of sample.peerConnectionTransports) {\n\t\t\t\tconst observedTransport = this._updatePeerConnectionTransportStats(peerConnectionTransport);\n\n\t\t\t\tif (!observedTransport) continue;\n\n\t\t\t}\n\t\t}\n\t\tif (sample.remoteInboundRtps) {\n\t\t\tfor (const remoteInboundRtp of sample.remoteInboundRtps) {\n\t\t\t\tconst observedRemoteInboundRtp = this._updateRemoteInboundRtpStats(remoteInboundRtp);\n\n\t\t\t\tif (!observedRemoteInboundRtp) continue;\n\n\t\t\t\tif (observedRemoteInboundRtp.roundTripTime) {\n\t\t\t\t\t// RTCP round trip — end to end over the media path.\n\t\t\t\t\trtcpRttMeasurementsInSec.push(observedRemoteInboundRtp.roundTripTime);\n\t\t\t\t}\n\n\t\t\t\t// Surface receiver-report-derived metrics on the corresponding local outbound-rtp.\n\t\t\t\tconst observedOutboundRtp = observedRemoteInboundRtp.getOutboundRtp();\n\n\t\t\t\tif (observedOutboundRtp) {\n\t\t\t\t\tif (observedRemoteInboundRtp.roundTripTime !== undefined) {\n\t\t\t\t\t\tobservedOutboundRtp.remoteRttInMs = observedRemoteInboundRtp.roundTripTime * 1000;\n\t\t\t\t\t}\n\t\t\t\t\tif (observedRemoteInboundRtp.fractionLost !== undefined) {\n\t\t\t\t\t\tobservedOutboundRtp.remoteFractionLost = observedRemoteInboundRtp.fractionLost;\n\t\t\t\t\t}\n\t\t\t\t\tif (observedRemoteInboundRtp.jitter !== undefined) {\n\t\t\t\t\t\tobservedOutboundRtp.remoteJitter = observedRemoteInboundRtp.jitter;\n\t\t\t\t\t}\n\t\t\t\t\tif (observedRemoteInboundRtp.packetsLost !== undefined) {\n\t\t\t\t\t\tobservedOutboundRtp.remotePacketsLost = observedRemoteInboundRtp.packetsLost;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (sample.remoteOutboundRtps) {\n\t\t\tfor (const remoteOutboundRtp of sample.remoteOutboundRtps) {\n\t\t\t\tconst observedRemoteOutboundRtp = this._updateRemoteOutboundRtpStats(remoteOutboundRtp);\n\n\t\t\t\tif (!observedRemoteOutboundRtp) continue;\n\n\t\t\t\t// Surface sender-report-derived metrics on the corresponding local inbound-rtp.\n\t\t\t\tconst observedInboundRtp = observedRemoteOutboundRtp.getInboundRtp();\n\n\t\t\t\tif (observedInboundRtp) {\n\t\t\t\t\tif (observedRemoteOutboundRtp.roundTripTime !== undefined) {\n\t\t\t\t\t\tobservedInboundRtp.remoteRttInMs = observedRemoteOutboundRtp.roundTripTime * 1000;\n\t\t\t\t\t}\n\t\t\t\t\tif (observedRemoteOutboundRtp.bytesSent !== undefined) {\n\t\t\t\t\t\tobservedInboundRtp.remoteBytesSent = observedRemoteOutboundRtp.bytesSent;\n\t\t\t\t\t}\n\t\t\t\t\tif (observedRemoteOutboundRtp.packetsSent !== undefined) {\n\t\t\t\t\t\tobservedInboundRtp.remotePacketsSent = observedRemoteOutboundRtp.packetsSent;\n\t\t\t\t\t}\n\t\t\t\t\tif (observedRemoteOutboundRtp.remoteTimestamp !== undefined) {\n\t\t\t\t\t\tobservedInboundRtp.remoteTimestamp = observedRemoteOutboundRtp.remoteTimestamp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// tracks should be updated last as they are derived stats\n\t\t// and depends on base stats but they all received in the sample sample\n\t\tif (sample.inboundTracks) {\n\t\t\tfor (const inboundTrack of sample.inboundTracks) {\n\t\t\t\tthis._updateInboundTrackSample(inboundTrack);\n\t\t\t}\n\t\t}\n\t\tif (sample.outboundTracks) {\n\t\t\tfor (const outboundTrack of sample.outboundTracks) {\n\t\t\t\tthis._updateOutboundTrackSample(outboundTrack);\n\t\t\t}\n\t\t}\n\n\t\tthis.totalInboundPacketsLost += this.deltaInboundPacketsLost;\n\t\tthis.totalInboundPacketsReceived += this.deltaInboundPacketsReceived;\n\t\tthis.totalOutboundPacketsSent += this.deltaOutboundPacketsSent;\n\t\tthis.totalDataChannelBytesSent += this.deltaDataChannelBytesSent;\n\t\tthis.totalDataChannelBytesReceived += this.deltaDataChannelBytesReceived;\n\t\tthis.totalDataChannelMessagesSent += this.deltaDataChannelMessagesSent;\n\t\tthis.totalDataChannelMessagesReceived += this.deltaDataChannelMessagesReceived;\n\t\tthis.totalReceivedAudioBytes += this.deltaReceivedAudioBytes;\n\t\tthis.totalReceivedVideoBytes += this.deltaReceivedVideoBytes;\n\t\tthis.totalSentAudioBytes += this.deltaSentAudioBytes;\n\t\tthis.totalSentVideoBytes += this.deltaSentVideoBytes;\n\t\tthis.totalReceivedAudioPacktes += this.deltaReceivedAudioPackets;\n\t\tthis.totalReceivedVideoPackets += this.deltaReceivedVideoPackets;\n\t\tthis.totalSentAudioPackets += this.deltaSentAudioPackets;\n\t\tthis.totalSentVideoPackets += this.deltaSentVideoPackets;\n\n\t\tthis.receivingPacketsPerSecond = this.deltaInboundPacketsReceived / elapsedTimeInSec;\n\t\tthis.sendingPacketsPerSecond = this.deltaOutboundPacketsSent / elapsedTimeInSec;\n\t\tthis.sendingAudioBitrate = (this.deltaSentAudioBytes * 8) / elapsedTimeInSec;\n\t\tthis.sendingVideoBitrate = (this.deltaSentVideoBytes * 8) / elapsedTimeInSec;\n\t\tthis.receivingAudioBitrate = (this.deltaReceivedAudioBytes * 8) / elapsedTimeInSec;\n\t\tthis.receivingVideoBitrate = (this.deltaReceivedVideoBytes * 8) / elapsedTimeInSec;\n\n\t\tthis.iceRttInMs = 0 < iceRttMeasurementsInSec.length\n\t\t\t? percentileOfSorted(iceRttMeasurementsInSec.sort(ascending), 0.5) * 1000\n\t\t\t: undefined;\n\t\tthis.rtcpRttInMs = 0 < rtcpRttMeasurementsInSec.length\n\t\t\t? percentileOfSorted(rtcpRttMeasurementsInSec.sort(ascending), 0.5) * 1000\n\t\t\t: undefined;\n\n\t\t// Prefer the end-to-end (RTCP) trip, fall back to ICE — never a blend of the two.\n\t\tthis.currentRttInMs = this.rtcpRttInMs ?? this.iceRttInMs;\n\t\tthis.sfuHopRttInMs = this.rtcpRttInMs !== undefined && this.iceRttInMs !== undefined\n\t\t\t? Math.max(0, this.rtcpRttInMs - this.iceRttInMs)\n\t\t\t: undefined;\n\t\tif (jitterMeasurements.length > 0) {\n\t\t\tthis.currentJitter = percentileOfSorted(jitterMeasurements.sort(ascending), 0.5);\n\t\t} else {\n\t\t\tthis.currentJitter = undefined;\n\t\t}\n\t\tconst wasUsingTURN = this.usingTURN;\n\t\tconst selectedIceCandidatePairs = this.selectedIceCandidatePairs;\n\t\tconst selectedCandidatePairForTurn: ObservedIceCandidatePair[] = [];\n\n\t\tthis.usingTCP = false;\n\t\tthis.usingTURN = false;\n\n\t\tfor (const selectedCandidatePair of selectedIceCandidatePairs) {\n\t\t\tconst localCandidate = selectedCandidatePair.getLocalCandidate();\n\n\t\t\tif (localCandidate?.protocol === 'tcp') {\n\t\t\t\tthis.usingTCP = true;\n\t\t\t}\n\t\t\t// relay candidates are only obtained from TURN servers, so the local candidate's\n\t\t\t// type alone establishes TURN usage. The server `url` is only exposed on *local*\n\t\t\t// candidates (and not by every browser) — it is required only to attribute the\n\t\t\t// traffic to a concrete TURN server.\n\t\t\tif (localCandidate?.candidateType === 'relay') {\n\t\t\t\tthis.usingTURN = true;\n\t\t\t\tif (localCandidate.url?.startsWith('turn')) {\n\t\t\t\t\tselectedCandidatePairForTurn.push(selectedCandidatePair);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.deltaTransportReceivedBytes += selectedCandidatePair.deltaBytesReceived;\n\t\t\tthis.deltaTransportSentBytes += selectedCandidatePair.deltaBytesSent;\n\t\t}\n\n\t\tif (this.usingTURN) {\n\t\t\tif (!this.observedTurnServer) {\n\t\t\t\tthis.observedTurnServer = this.client.call.observer.observedTURN.addPeerConnection(this);\n\t\t\t}\n\t\t\tthis.observedTurnServer?.updateTurnUsage(...selectedCandidatePairForTurn);\n\t\t} else if (wasUsingTURN) {\n\t\t\tif (!this.usingTURN) {\n\t\t\t\tthis.client.call.observer.observedTURN.removePeerConnection(this);\n\t\t\t}\n\t\t}\n\n\t\t// Announced after the whole sample is applied, so a listener can resolve the new pair's\n\t\t// candidates (candidates and pairs are processed before transports, but a handler may also\n\t\t// want e.g. `publicAddress` or `usingTURN`, which are derived just above/below).\n\t\tconst publicAddress = this._resolvePublicAddress(selectedIceCandidatePairs);\n\t\tconst previousPublicAddress = this.publicAddress;\n\n\t\tif (publicAddress !== undefined && publicAddress !== previousPublicAddress) {\n\t\t\tthis.publicAddress = publicAddress;\n\t\t\tthis.publicAddressUpdatedAt = now;\n\t\t}\n\n\t\tfor (const observedIceTransport of iceTransportsWithChangedSelection) {\n\t\t\tthis._notify('selected-candidate-pair-changed', {\n\t\t\t\t...this.eventScope,\n\t\t\t\tobservedIceTransport,\n\t\t\t\tobservedIceCandidatePair: observedIceTransport.getSelectedCandidatePair(),\n\t\t\t\tpreviousCandidatePairId: observedIceTransport.previousSelectedCandidatePairId,\n\t\t\t});\n\t\t}\n\t\tif (publicAddress !== undefined && publicAddress !== previousPublicAddress) {\n\t\t\tthis._notify('peer-connection-public-address-changed', { ...this.eventScope, publicAddress, previousPublicAddress });\n\t\t}\n\n\t\tthis.calculatedScore.value = sample.score;\n\t\tthis.updated = now;\n\t\tthis._checkVisited();\n\n\t\tthis._notify('peer-connection-updated', { ...this.eventScope, context });\n\t}\n\n\tprivate _checkVisited() {\n\t\tfor (const certificate of [ ...this.observedCertificates.values() ]) {\n\t\t\tif (certificate.visited) continue;\n\n\t\t\tthis.observedCertificates.delete(certificate.id);\n\t\t\tthis._notify('certificate-removed', { ...this.eventScope, observedCertificate: certificate });\n\t\t}\n\t\tfor (const observedCodec of [ ...this.observedCodecs.values() ]) {\n\t\t\tif (observedCodec.visited) continue;\n\n\t\t\tthis.observedCodecs.delete(observedCodec.id);\n\t\t\tthis._notify('codec-removed', { ...this.eventScope, observedCodec });\n\t\t}\n\t\tfor (const observedDataChannel of [ ...this.observedDataChannels.values() ]) {\n\t\t\tif (observedDataChannel.visited) continue;\n\n\t\t\tthis.observedDataChannels.delete(observedDataChannel.id);\n\t\t\tthis._notify('data-channel-removed', { ...this.eventScope, observedDataChannel });\n\t\t}\n\t\tfor (const observedIceCandidate of [ ...this.observedIceCandidates.values() ]) {\n\t\t\tif (observedIceCandidate.visited) continue;\n\n\t\t\tthis.observedIceCandidates.delete(observedIceCandidate.id);\n\t\t\tthis._notify('ice-candidate-removed', { ...this.eventScope, observedIceCandidate });\n\t\t}\n\t\tfor (const observedIceCandidatePair of [ ...this.observedIceCandidatesPair.values() ]) {\n\t\t\tif (observedIceCandidatePair.visited) continue;\n\n\t\t\tthis.observedIceCandidatesPair.delete(observedIceCandidatePair.id);\n\t\t\tthis._notify('ice-candidate-pair-removed', { ...this.eventScope, observedIceCandidatePair });\n\t\t}\n\t\tfor (const observedIceTransport of [ ...this.observedIceTransports.values() ]) {\n\t\t\tif (observedIceTransport.visited) continue;\n\n\t\t\tthis.observedIceTransports.delete(observedIceTransport.id);\n\t\t\tthis._notify('ice-transport-removed', { ...this.eventScope, observedIceTransport });\n\t\t}\n\t\tfor (const observedInboundRtp of [ ...this.observedInboundRtps.values() ]) {\n\t\t\tif (observedInboundRtp.visited) continue;\n\n\t\t\tthis.observedInboundRtps.delete(observedInboundRtp.ssrc);\n\t\t\tthis._notify('inbound-rtp-removed', { ...this.eventScope, observedInboundRtp });\n\t\t}\n\t\tfor (const observedInboundTrack of [ ...this.observedInboundTracks.values() ]) {\n\t\t\tif (observedInboundTrack.visited) continue;\n\n\t\t\tthis.observedInboundTracks.delete(observedInboundTrack.id);\n\t\t\tthis.emit('removed-inbound-track', observedInboundTrack);\n\t\t\tthis._notify('inbound-track-removed', { ...this.eventScope, observedInboundTrack });\n\t\t}\n\t\tfor (const observedMediaPlayout of [ ...this.observedMediaPlayouts.values() ]) {\n\t\t\tif (observedMediaPlayout.visited) continue;\n\n\t\t\tthis.observedMediaPlayouts.delete(observedMediaPlayout.id);\n\t\t\tthis._notify('media-playout-removed', { ...this.eventScope, observedMediaPlayout });\n\t\t}\n\t\tfor (const observedMediaSource of [ ...this.observedMediaSources.values() ]) {\n\t\t\tif (observedMediaSource.visited) continue;\n\n\t\t\tthis.observedMediaSources.delete(observedMediaSource.id);\n\t\t\tthis._notify('media-source-removed', { ...this.eventScope, observedMediaSource });\n\t\t}\n\t\tfor (const observedOutboundRtp of [ ...this.observedOutboundRtps.values() ]) {\n\t\t\tif (observedOutboundRtp.visited) continue;\n\n\t\t\tthis.observedOutboundRtps.delete(observedOutboundRtp.ssrc);\n\t\t\tthis._notify('outbound-rtp-removed', { ...this.eventScope, observedOutboundRtp });\n\t\t}\n\t\tfor (const observedOutboundTrack of [ ...this.observedOutboundTracks.values() ]) {\n\t\t\tif (observedOutboundTrack.visited) continue;\n\n\t\t\tthis.observedOutboundTracks.delete(observedOutboundTrack.id);\n\t\t\tthis.emit('removed-outbound-track', observedOutboundTrack);\n\t\t\tthis._notify('outbound-track-removed', { ...this.eventScope, observedOutboundTrack });\n\t\t}\n\t\tfor (const observedPeerConnectionTransport of [ ...this.observedPeerConnectionTransports.values() ]) {\n\t\t\tif (observedPeerConnectionTransport.visited) continue;\n\n\t\t\tthis.observedPeerConnectionTransports.delete(observedPeerConnectionTransport.id);\n\t\t\tthis._notify('peer-connection-transport-removed', { ...this.eventScope, observedPeerConnectionTransport });\n\t\t}\n\t\tfor (const observedRemoteInboundRtp of [ ...this.observedRemoteInboundRtps.values() ]) {\n\t\t\tif (observedRemoteInboundRtp.visited) continue;\n\n\t\t\tthis.observedRemoteInboundRtps.delete(observedRemoteInboundRtp.ssrc);\n\t\t\tthis._notify('remote-inbound-rtp-removed', { ...this.eventScope, observedRemoteInboundRtp });\n\t\t}\n\t\tfor (const observedRemoteOutboundRtp of [ ...this.observedRemoteOutboundRtps.values() ]) {\n\t\t\tif (observedRemoteOutboundRtp.visited) continue;\n\n\t\t\tthis.observedRemoteOutboundRtps.delete(observedRemoteOutboundRtp.ssrc);\n\t\t\tthis._notify('remote-outbound-rtp-removed', { ...this.eventScope, observedRemoteOutboundRtp });\n\t\t}\n\t}\n\n\tprivate _updateCertificateStats(stats: CertificateStats) {\n\t\tlet observedCertificate = this.observedCertificates.get(stats.id);\n\n\t\tif (!observedCertificate) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.fingerprint) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid CertificateStats (missing timestamp OR id OR fingerprint field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedCertificate = new ObservedCertificate(stats.timestamp, stats.id, this);\n\n\t\t\tobservedCertificate.update(stats);\n\n\t\t\tthis.observedCertificates.set(stats.id, observedCertificate);\n\t\t\tthis._notify('certificate-added', { ...this.eventScope, observedCertificate });\n\t\t} else {\n\t\t\tobservedCertificate.update(stats);\n\t\t}\n\t\tthis._notify('certificate-updated', { ...this.eventScope, observedCertificate });\n\n\t\treturn observedCertificate;\n\t}\n\n\tprivate _updateCodecStats(stats: CodecStats) {\n\t\tlet observedCodec = this.observedCodecs.get(stats.id);\n\n\t\tif (!observedCodec) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.mimeType) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid CodecStats (missing timestamp OR id OR mimeType field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedCodec = new ObservedCodec(stats.timestamp, stats.id, stats.mimeType, this);\n\n\t\t\tobservedCodec.update(stats);\n\n\t\t\tthis.observedCodecs.set(stats.id, observedCodec);\n\t\t\tthis._notify('codec-added', { ...this.eventScope, observedCodec });\n\t\t} else {\n\t\t\tobservedCodec.update(stats);\n\t\t}\n\t\tthis._notify('codec-updated', { ...this.eventScope, observedCodec });\n\n\t\treturn observedCodec;\n\t}\n\n\tprivate _updateDataChannelStats(stats: DataChannelStats) {\n\t\tlet observedDataChannel = this.observedDataChannels.get(stats.id);\n\n\t\tif (!observedDataChannel) {\n\t\t\tif (!stats.timestamp || !stats.id) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid DataChannelStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedDataChannel = new ObservedDataChannel(stats.timestamp, stats.id, this);\n\n\t\t\tobservedDataChannel.update(stats);\n\n\t\t\tthis.observedDataChannels.set(stats.id, observedDataChannel);\n\t\t\tthis._notify('data-channel-added', { ...this.eventScope, observedDataChannel });\n\t\t} else {\n\t\t\tobservedDataChannel.update(stats);\n\t\t}\n\t\tthis._notify('data-channel-updated', { ...this.eventScope, observedDataChannel });\n\n\t\treturn observedDataChannel;\n\t}\n\n\tprivate _updateIceCandidateStats(stats: IceCandidateStats) {\n\t\tlet observedIceCandidate = this.observedIceCandidates.get(stats.id);\n\n\t\tif (!observedIceCandidate) {\n\t\t\tif (!stats.timestamp || !stats.id) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedIceCandidate = new ObservedIceCandidate(stats.timestamp, stats.id, this);\n\n\t\t\tobservedIceCandidate.update(stats);\n\n\t\t\tthis.observedIceCandidates.set(stats.id, observedIceCandidate);\n\t\t\tthis._notify('ice-candidate-added', { ...this.eventScope, observedIceCandidate });\n\t\t} else {\n\t\t\tobservedIceCandidate.update(stats);\n\t\t}\n\t\tthis._notify('ice-candidate-updated', { ...this.eventScope, observedIceCandidate });\n\n\t\treturn observedIceCandidate;\n\t}\n\n\tprivate _updateIceCandidatePairStats(stats: IceCandidateStats) {\n\t\tlet observedIceCandidatePair = this.observedIceCandidatesPair.get(stats.id);\n\n\t\tif (!observedIceCandidatePair) {\n\t\t\tif (!stats.timestamp || !stats.id) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedIceCandidatePair = new ObservedIceCandidatePair(stats.timestamp, stats.id, this);\n\n\t\t\tobservedIceCandidatePair.update(stats);\n\n\t\t\tthis.observedIceCandidatesPair.set(stats.id, observedIceCandidatePair);\n\t\t\tthis._notify('ice-candidate-pair-added', { ...this.eventScope, observedIceCandidatePair });\n\t\t} else {\n\t\t\tobservedIceCandidatePair.update(stats);\n\t\t}\n\t\tthis._notify('ice-candidate-pair-updated', { ...this.eventScope, observedIceCandidatePair });\n\n\t\treturn observedIceCandidatePair;\n\t}\n\n\tprivate _updateIceTransportStats(stats: IceCandidateStats) {\n\t\tlet observedIceTransport = this.observedIceTransports.get(stats.id);\n\n\t\tif (!observedIceTransport) {\n\t\t\tif (!stats.timestamp || !stats.id) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedIceTransport = new ObservedIceTransport(stats.timestamp, stats.id, this);\n\n\t\t\tobservedIceTransport.update(stats);\n\n\t\t\tthis.observedIceTransports.set(stats.id, observedIceTransport);\n\t\t\tthis._notify('ice-transport-added', { ...this.eventScope, observedIceTransport });\n\t\t} else {\n\t\t\tobservedIceTransport.update(stats);\n\t\t}\n\t\tthis._notify('ice-transport-updated', { ...this.eventScope, observedIceTransport });\n\n\t\treturn observedIceTransport;\n\t}\n\n\tprivate _updateInboundRtpStats(stats: InboundRtpStats) {\n\t\tlet observedInboundRtp = this.observedInboundRtps.get(stats.ssrc);\n\n\t\tif (!observedInboundRtp) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind || !stats.trackIdentifier) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR ssrc OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedInboundRtp = new ObservedInboundRtp(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.ssrc,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tstats.trackIdentifier,\n\t\t\t\tthis\n\t\t\t);\n\n\t\t\tobservedInboundRtp.update(stats);\n\n\t\t\tthis.observedInboundRtps.set(stats.ssrc, observedInboundRtp);\n\t\t\tthis._notify('inbound-rtp-added', { ...this.eventScope, observedInboundRtp });\n\t\t} else {\n\t\t\tobservedInboundRtp.update(stats);\n\t\t}\n\t\tthis._notify('inbound-rtp-updated', { ...this.eventScope, observedInboundRtp });\n\n\t\treturn observedInboundRtp;\n\t}\n\n\tprivate _updateInboundTrackSample(stats: InboundTrackSample) {\n\t\tlet observedInboundTrack = this.observedInboundTracks.get(stats.id);\n\n\t\tif (!observedInboundTrack) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid InboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst inboundRtp = [ ...this.observedInboundRtps.values() ].find((inbRtp) => inbRtp.trackIdentifier === stats.id);\n\t\t\tconst mediaPlayout = inboundRtp ?\n\t\t\t\t[ ...this.observedMediaPlayouts.values() ].find((mp) => mp.id === inboundRtp.playoutId) : undefined;\n\n\t\t\tobservedInboundTrack = new ObservedInboundTrack(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis,\n\t\t\t\tinboundRtp,\n\t\t\t\tmediaPlayout,\n\t\t\t);\n\n\t\t\tobservedInboundTrack.update(stats);\n\n\t\t\tthis.observedInboundTracks.set(stats.id, observedInboundTrack);\n\t\t\tthis._notify('inbound-track-added', { ...this.eventScope, observedInboundTrack });\n\t\t} else {\n\t\t\tobservedInboundTrack.update(stats);\n\t\t}\n\t\tthis._notify('inbound-track-updated', { ...this.eventScope, observedInboundTrack });\n\n\t\treturn observedInboundTrack;\n\t}\n\n\tprivate _updateMediaPlayoutStats(stats: MediaPlayoutStats) {\n\t\tlet observedMediaPlayout = this.observedMediaPlayouts.get(stats.id);\n\n\t\tif (!observedMediaPlayout) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedMediaPlayout = new ObservedMediaPlayout(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis\n\t\t\t);\n\n\t\t\tobservedMediaPlayout.update(stats);\n\n\t\t\tthis.observedMediaPlayouts.set(stats.id, observedMediaPlayout);\n\t\t\tthis._notify('media-playout-added', { ...this.eventScope, observedMediaPlayout });\n\t\t} else {\n\t\t\tobservedMediaPlayout.update(stats);\n\t\t}\n\t\tthis._notify('media-playout-updated', { ...this.eventScope, observedMediaPlayout });\n\n\t\treturn observedMediaPlayout;\n\t}\n\n\tprivate _updateMediaSourceStats(stats: MediaSourceStats) {\n\t\tlet observedMediaSource = this.observedMediaSources.get(stats.id);\n\n\t\tif (!observedMediaSource) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedMediaSource = new ObservedMediaSource(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis\n\t\t\t);\n\n\t\t\tobservedMediaSource.update(stats);\n\n\t\t\tthis.observedMediaSources.set(stats.id, observedMediaSource);\n\t\t\tthis._notify('media-source-added', { ...this.eventScope, observedMediaSource });\n\t\t} else {\n\t\t\tobservedMediaSource.update(stats);\n\t\t}\n\t\tthis._notify('media-source-updated', { ...this.eventScope, observedMediaSource });\n\n\t\treturn observedMediaSource;\n\t}\n\n\tprivate _updateOutboundRtpStats(stats: OutboundRtpStats) {\n\t\tlet observedOutboundRtp = this.observedOutboundRtps.get(stats.ssrc);\n\n\t\tif (!observedOutboundRtp) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid OutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedOutboundRtp = new ObservedOutboundRtp(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.ssrc,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis\n\t\t\t);\n\n\t\t\tobservedOutboundRtp.update(stats);\n\n\t\t\tthis.observedOutboundRtps.set(stats.ssrc, observedOutboundRtp);\n\t\t\tthis._notify('outbound-rtp-added', { ...this.eventScope, observedOutboundRtp });\n\t\t} else {\n\t\t\tobservedOutboundRtp.update(stats);\n\t\t}\n\t\tthis._notify('outbound-rtp-updated', { ...this.eventScope, observedOutboundRtp });\n\n\t\treturn observedOutboundRtp;\n\t}\n\n\tprivate _updateOutboundTrackSample(stats: OutboundTrackSample) {\n\t\tlet observedOutboundTrack = this.observedOutboundTracks.get(stats.id);\n\n\t\tif (!observedOutboundTrack) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid OutboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst observedMediaSource = [ ...this.observedMediaSources.values() ].find((mediaSource) => mediaSource.trackIdentifier === stats.id);\n\t\t\tconst outboundRtps = observedMediaSource\n\t\t\t\t? [ ...this.observedOutboundRtps.values() ].filter((outboundRtp) => outboundRtp.mediaSourceId === observedMediaSource?.id) : undefined;\n\n\t\t\tobservedOutboundTrack = new ObservedOutboundTrack(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis,\n\t\t\t\toutboundRtps,\n\t\t\t\tobservedMediaSource,\n\t\t\t);\n\n\t\t\tobservedOutboundTrack.update(stats);\n\n\t\t\tthis.observedOutboundTracks.set(stats.id, observedOutboundTrack);\n\t\t\tthis._notify('outbound-track-added', { ...this.eventScope, observedOutboundTrack });\n\t\t} else {\n\t\t\tobservedOutboundTrack.update(stats);\n\t\t}\n\t\tthis._notify('outbound-track-updated', { ...this.eventScope, observedOutboundTrack });\n\n\t\treturn observedOutboundTrack;\n\t}\n\n\tprivate _updatePeerConnectionTransportStats(stats: PeerConnectionTransportStats) {\n\t\tlet observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);\n\n\t\tif (!observedPeerConnectionTransport) {\n\t\t\tif (!stats.timestamp || !stats.id) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid PeerConnectionTransportStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedPeerConnectionTransport = new ObservedPeerConnectionTransport(stats.timestamp, stats.id, this);\n\n\t\t\tobservedPeerConnectionTransport.update(stats);\n\n\t\t\tthis.observedPeerConnectionTransports.set(stats.id, observedPeerConnectionTransport);\n\t\t\tthis._notify('peer-connection-transport-added', { ...this.eventScope, observedPeerConnectionTransport });\n\t\t} else {\n\t\t\tobservedPeerConnectionTransport.update(stats);\n\t\t}\n\t\tthis._notify('peer-connection-transport-updated', { ...this.eventScope, observedPeerConnectionTransport });\n\n\t\treturn observedPeerConnectionTransport;\n\t}\n\n\tprivate _updateRemoteInboundRtpStats(stats: RemoteInboundRtpStats) {\n\t\tlet observedRemoteInboundRtp = this.observedRemoteInboundRtps.get(stats.ssrc);\n\n\t\tif (!observedRemoteInboundRtp) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid RemoteInboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedRemoteInboundRtp = new ObservedRemoteInboundRtp(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.ssrc,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis\n\t\t\t);\n\n\t\t\tobservedRemoteInboundRtp.update(stats);\n\n\t\t\tthis.observedRemoteInboundRtps.set(stats.ssrc, observedRemoteInboundRtp);\n\t\t\tthis._notify('remote-inbound-rtp-added', { ...this.eventScope, observedRemoteInboundRtp });\n\t\t} else {\n\t\t\tobservedRemoteInboundRtp.update(stats);\n\t\t}\n\t\tthis._notify('remote-inbound-rtp-updated', { ...this.eventScope, observedRemoteInboundRtp });\n\n\t\treturn observedRemoteInboundRtp;\n\t}\n\n\tprivate _updateRemoteOutboundRtpStats(stats: RemoteOutboundRtpStats) {\n\t\tlet observedRemoteOutboundRtp = this.observedRemoteOutboundRtps.get(stats.ssrc);\n\n\t\tif (!observedRemoteOutboundRtp) {\n\t\t\tif (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {\n\t\t\t\treturn logger.warn(\n\t\t\t\t\t`ObservedPeerConnection received an invalid RemoteOutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,\n\t\t\t\t\tstats\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tobservedRemoteOutboundRtp = new ObservedRemoteOutboundRtp(\n\t\t\t\tstats.timestamp,\n\t\t\t\tstats.id,\n\t\t\t\tstats.ssrc,\n\t\t\t\tstats.kind as MediaKind,\n\t\t\t\tthis\n\t\t\t);\n\n\t\t\tobservedRemoteOutboundRtp.update(stats);\n\n\t\t\tthis.observedRemoteOutboundRtps.set(stats.ssrc, observedRemoteOutboundRtp);\n\t\t\tthis._notify('remote-outbound-rtp-added', { ...this.eventScope, observedRemoteOutboundRtp });\n\t\t} else {\n\t\t\tobservedRemoteOutboundRtp.update(stats);\n\t\t}\n\t\tthis._notify('remote-outbound-rtp-updated', { ...this.eventScope, observedRemoteOutboundRtp });\n\n\t\treturn observedRemoteOutboundRtp;\n\t}\n\n\t/** See `publicAddress`. Cheap on the common path: the fallback scan runs only when the selected pairs yield nothing. */\n\tprivate _resolvePublicAddress(selectedIceCandidatePairs: ObservedIceCandidatePair[]): string | undefined {\n\t\tfor (const pair of selectedIceCandidatePairs) {\n\t\t\tconst address = publicAddressOfLocalCandidate(pair.getLocalCandidate());\n\n\t\t\tif (address) return address;\n\t\t}\n\t\tfor (const pair of this.observedIceCandidatesPair.values()) {\n\t\t\tconst address = publicAddressOfLocalCandidate(pair.getLocalCandidate());\n\n\t\t\tif (address) return address;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/** Emit an Observer-bus event scoped to this peer connection. */\n\tprivate _notify<K extends keyof ObserverEvents>(type: K, ...args: ObserverEvents[K]): void {\n\t\tthis.client.call.observer.emit(type, ...args);\n\t}\n}\n","/**\n * Small statistics helpers used by the aggregators and detectors.\n *\n * Rationale: a mean is a poor summary for call telemetry — one participant with a 1500 ms RTT\n * skews the average for nine healthy ones. Detectors should reason with medians, high percentiles\n * and \"affected ratios\" instead.\n */\n\n/** A distribution summary of a numeric sample set. */\nexport type StatsSummary = {\n\tcount: number;\n\tmin: number;\n\tmax: number;\n\tmean: number;\n\tmedian: number;\n\tp25: number;\n\tp75: number;\n\tp95: number;\n};\n\n/**\n * The p-th percentile (0..1) using linear interpolation between closest ranks.\n * Returns `undefined` for an empty input.\n */\nexport function percentile(values: number[], p: number): number | undefined {\n\tif (values.length === 0) return undefined;\n\n\treturn percentileOfSorted(values.slice().sort(ascending), p);\n}\n\n/** The median (50th percentile). `undefined` for an empty input. */\nexport function median(values: number[]): number | undefined {\n\treturn percentile(values, 0.5);\n}\n\n/**\n * Median absolute deviation: `median(|value - median(values)|)`. A robust alternative to standard\n * deviation for describing how spread out `values` are — one wild outlier shifts a mean-based\n * deviation a lot, but barely moves a median. `undefined` for an empty input.\n */\nexport function medianAbsoluteDeviation(values: number[]): number | undefined {\n\tconst centre = median(values);\n\n\tif (centre === undefined) return undefined;\n\n\treturn median(values.map((value) => Math.abs(value - centre)));\n}\n\n/**\n * A robust z-score: how many (MAD-based) standard deviations `value` sits above/below the median of\n * `baseline`.\n *\n * Uses the median and MAD instead of the mean and standard deviation so a handful of baseline\n * outliers can't inflate the \"normal\" spread and mask a genuine spike — the same reasoning behind\n * {@link summarize}'s percentiles applies here to a single scalar spread. `1.4826` is the constant\n * that makes MAD estimate the standard deviation of a normal distribution, so the result reads on\n * the same scale as a classic z-score.\n *\n * A classic z-score divides by zero once every baseline value is identical (`MAD === 0`). Here:\n * `value` strictly above that constant baseline reads as `Infinity` — a spike with no precedent\n * whatsoever, however small; `value` at or below it reads as `0`, indistinguishable from the (flat)\n * baseline rather than a division error.\n *\n * ### Worked example\n *\n * A baseline of \"share of clients reporting congestion\", one entry per 10 s bucket, on a healthy\n * fleet: `[0.02, 0.01, 0.03, 0.04, 0.02]`. Median `0.02`, MAD `0.01`, so the scale is\n * `1.4826 * 0.01 ≈ 0.0148`.\n *\n * ```ts\n * robustZScore(0.03, baseline); // ≈ 0.67 — an ordinary bucket\n * robustZScore(0.25, baseline); // ≈ 15.5 — a quarter of the fleet at once; nothing like it before\n * ```\n *\n * Now add one bad bucket to the *baseline* — `[0.02, 0.01, 0.03, 0.04, 0.02, 0.40]`. A mean/stddev\n * z-score would absorb it: the mean climbs to `0.087` and the stddev to `~0.14`, so a genuine `0.25`\n * spike scores about `1.2` and looks unremarkable. **One past incident would hide the next one.**\n * Median and MAD barely move (median `0.025`, MAD `0.01`), so `0.25` still scores `≈15.2`. That\n * resistance is the entire reason for this function.\n *\n * The `Infinity` case is not an edge case in practice — it is a fleet that has been perfectly quiet:\n *\n * ```ts\n * robustZScore(0.10, [ 0, 0, 0, 0, 0 ]); // Infinity — first congestion ever seen\n * robustZScore(0, [ 0, 0, 0, 0, 0 ]); // 0 — still nothing happening\n * ```\n *\n * `Infinity` clears any finite threshold, which is intended: \"we have never seen this\" *is* the\n * strongest possible statistical statement. It is also why a caller must gate on practical\n * significance too — see `SfuCongestionDetector`, which additionally requires a minimum number of\n * affected clients, so a single client on a quiet fleet cannot page anyone.\n *\n * `undefined` when `baseline` is empty — there is nothing to compare against.\n */\nexport function robustZScore(value: number, baseline: number[]): number | undefined {\n\tconst centre = median(baseline);\n\n\tif (centre === undefined) return undefined;\n\n\tconst mad = medianAbsoluteDeviation(baseline) ?? 0;\n\n\tif (mad === 0) return centre < value ? Infinity : 0;\n\n\treturn (value - centre) / (1.4826 * mad);\n}\n\n/** Summarize a numeric sample set. Returns `undefined` for an empty input. */\nexport function summarize(values: number[]): StatsSummary | undefined {\n\tconst count = values.length;\n\n\tif (count === 0) return undefined;\n\n\t// Sort once and read everything off the sorted copy. Calling `percentile` three times would\n\t// copy-and-sort three times, which on the detector hot path meant thousands of redundant sorts\n\t// per tick — the aggregator produces one summary per metric per published track, every tick.\n\tconst sorted = values.slice().sort(ascending);\n\tlet sum = 0;\n\n\tfor (let i = 0; i < count; i++) sum += sorted[i];\n\n\treturn {\n\t\tcount,\n\t\tmin: sorted[0],\n\t\tmax: sorted[count - 1],\n\t\tmean: sum / count,\n\t\tp25: percentileOfSorted(sorted, 0.25),\n\t\tmedian: percentileOfSorted(sorted, 0.5),\n\t\tp75: percentileOfSorted(sorted, 0.75),\n\t\tp95: percentileOfSorted(sorted, 0.95),\n\t};\n}\n\nconst ascending = (a: number, b: number) => a - b;\n\n/**\n * Linear-interpolated percentile of an **already ascending** array. The building block behind\n * {@link percentile} and {@link summarize}; exported so callers computing several percentiles over\n * the same data can sort once themselves.\n *\n * Passing an unsorted array yields a meaningless number rather than an error — sort first.\n */\nexport function percentileOfSorted(sorted: number[], p: number): number {\n\tif (sorted.length === 1) return sorted[0];\n\n\tconst clamped = Math.min(1, Math.max(0, p));\n\tconst position = clamped * (sorted.length - 1);\n\tconst lowerIndex = Math.floor(position);\n\tconst upperIndex = Math.ceil(position);\n\n\tif (lowerIndex === upperIndex) return sorted[lowerIndex];\n\n\tconst weight = position - lowerIndex;\n\n\treturn (sorted[lowerIndex] * (1 - weight)) + (sorted[upperIndex] * weight);\n}\n\n/**\n * A counter-reset-safe delta: the increase of a cumulative counter between two observations.\n *\n * Returns `0` when the counter went backwards (reset / SSRC reuse) or when either side is missing.\n * NOTE the guard is `>=` on **defined** values — a previous value of `0` is a perfectly valid\n * baseline, so `0 -> 5` correctly yields `5` (using a truthiness check here silently drops the\n * first interval of every counter, which is exactly when the first loss/freeze event happens).\n */\nexport function counterDelta(previous: number | undefined, current: number | undefined): number {\n\tif (previous === undefined || current === undefined) return 0;\n\tif (current < previous) return 0;\n\n\treturn current - previous;\n}\n\n/**\n * Pearson correlation of two equal-length series, clamped to `0..1`.\n *\n * Negative or undefined relationships read as `0`, because every caller here asks \"does A follow B?\"\n * — an inverse relationship is not a weaker yes, it is a no.\n */\nexport function correlation(xs: number[], ys: number[]): number {\n\tconst n = Math.min(xs.length, ys.length);\n\n\tif (n < 2) return 0;\n\n\tlet meanX = 0;\n\tlet meanY = 0;\n\n\tfor (let i = 0; i < n; i++) {\n\t\tmeanX += xs[i];\n\t\tmeanY += ys[i];\n\t}\n\tmeanX /= n;\n\tmeanY /= n;\n\n\tlet covariance = 0;\n\tlet varianceX = 0;\n\tlet varianceY = 0;\n\n\tfor (let i = 0; i < n; i++) {\n\t\tconst dx = xs[i] - meanX;\n\t\tconst dy = ys[i] - meanY;\n\n\t\tcovariance += dx * dy;\n\t\tvarianceX += dx * dx;\n\t\tvarianceY += dy * dy;\n\t}\n\n\tif (varianceX === 0 || varianceY === 0) return 0;\n\n\treturn Math.max(0, covariance / Math.sqrt(varianceX * varianceY));\n}\n\n/** Per-step result of {@link pageHinkley}. */\nexport type PageHinkleyResult = {\n\n\t/**\n\t * The Page-Hinkley statistic after each observation, same length as the input — one value per\n\t * `values[i]`, so `statistic[i]` is \"how far the cumulative deviation has grown above its own\n\t * historical low, using only `values[0..i]`\". Never negative (it's a gap to a *minimum*), and\n\t * `0` for as long as the process looks stable.\n\t */\n\tstatistic: number[];\n\n\t/**\n\t * Index of the **first** observation whose statistic exceeded `lambda`, else `undefined`.\n\t *\n\t * This is a one-shot latch over the call's whole input: once found, later observations are not\n\t * re-checked, even if the statistic subsequently falls back down (e.g. after a single transient\n\t * spike — see the class doc example). \"Is it *still* elevated right now\" is a different\n\t * question, answered by looking at the *tail* of {@link statistic}, or — for a live stream — by\n\t * re-running this over a recent window each time (which is what `TrendTester` does) rather than\n\t * over the whole history once.\n\t */\n\tchangePointIndex?: number;\n\n\t/** `true` when {@link changePointIndex} is defined. */\n\tchangeDetected: boolean;\n};\n\n/**\n * Page-Hinkley test: sequential (online) detection of a sustained **increase** in the mean of\n * `values` — \"has this metric settled onto a durably higher level\", as opposed to \"did one sample\n * come in high\". A single noisy point should not read as a regression; ten points that are all a\n * bit higher than before should.\n *\n * ### How it works\n *\n * At step `i`, three numbers are tracked:\n *\n * 1. `mean` — the running average of `values[0..i]` (**not** a fixed baseline — it is recomputed\n * from everything seen so far, including `values[i]` itself, which is what makes this\n * *adaptive*: after a real shift, `mean` keeps drifting up to meet the new level, and the\n * signal below fades back out on its own rather than staying triggered forever).\n * 2. `cumulative` — running sum of `(values[i] - mean - delta)`. Subtracting `mean` centres each\n * term on \"surprise relative to what we've seen so far\"; subtracting `delta` on top means a\n * small positive surprise still nets to a *negative* contribution, so it doesn't accumulate.\n * 3. `runningMinimum` — the lowest `cumulative` has ever been.\n *\n * The **Page-Hinkley statistic** is `cumulative - runningMinimum`: how far the running sum has\n * climbed above its own historical floor. Pure noise pulls `cumulative` up and down around a flat\n * trend, so the gap to `runningMinimum` stays small. A sustained increase pushes `cumulative`\n * mostly one direction — up — so `runningMinimum` stops updating and the gap grows every step,\n * crossing `lambda` once the shift is large/long enough to be sure it isn't noise. That first\n * crossing is {@link PageHinkleyResult.changePointIndex}.\n *\n * ### Parameters\n *\n * - `delta` — the **drift tolerance** (in the same units as `values`, e.g. ms of RTT): how much of\n * a step-to-step increase is written off as noise rather than counted towards the cumulative sum.\n * `0` means even a razor-thin, consistent upward creep eventually accumulates enough to trigger;\n * raising it requires each observation to clear that bar above the running mean before it\n * contributes anything (see the class doc's hand-worked example — the same jump that triggers\n * with `delta: 0` is completely absorbed at `delta: 10`).\n * - `lambda` — the **detection threshold** the statistic must exceed. It is in \"surprise units\" (a\n * sum of deviations, not a single observation's units), so there's no shortcut for picking it\n * other than trying it against representative data — see the RTT examples in `stats.spec.ts` for\n * a worked comparison of a low vs. a high `lambda` on the same series. Raising it delays\n * detection but makes a false positive from a lucky run of noise less likely.\n *\n * O(n) — one pass, unlike {@link mannKendall}'s O(n²). To detect a **decrease** instead of an\n * increase, negate `values` before calling (or negate the result's meaning if you'd rather).\n */\nexport function pageHinkley(values: number[], delta = 0, lambda = 50): PageHinkleyResult {\n\tconst statistic: number[] = [];\n\tlet changePointIndex: number | undefined;\n\n\tlet sum = 0;\n\tlet cumulative = 0;\n\tlet runningMinimum = 0;\n\n\tfor (let i = 0; i < values.length; i++) {\n\t\tsum += values[i];\n\t\tconst mean = sum / (i + 1);\n\n\t\tcumulative += values[i] - mean - delta;\n\t\tif (cumulative < runningMinimum) runningMinimum = cumulative;\n\n\t\tconst ph = cumulative - runningMinimum;\n\n\t\tstatistic.push(ph);\n\t\tif (changePointIndex === undefined && lambda < ph) changePointIndex = i;\n\t}\n\n\treturn { statistic, changePointIndex, changeDetected: changePointIndex !== undefined };\n}\n\n/** Result of {@link mannKendall}. */\nexport type MannKendallResult = {\n\n\t/** Sum of pairwise signs (`sign(values[j] - values[i])` for every `i < j`). */\n\ts: number;\n\n\t/** Variance of {@link s}, corrected for tied values. */\n\tvariance: number;\n\n\t/** Standard-normal score derived from `s`. `0` when `s` is `0` — no evidence either way. */\n\tz: number;\n\n\t/** Two-tailed p-value for the null hypothesis \"no monotonic trend\". */\n\tpValue: number;\n\n\t/** `'increasing'` / `'decreasing'` when significant at `alpha`, else `'no-trend'`. */\n\ttrend: 'increasing' | 'decreasing' | 'no-trend';\n};\n\n/**\n * Mann-Kendall trend test: a non-parametric test for a **monotonic** trend in `values`, without\n * assuming a distribution or a constant rate of change — it only asks \"are later values\n * consistently larger (or smaller) than earlier ones more often than chance would allow\".\n *\n * Every pair `i < j` votes `+1` (`values[j] > values[i]`), `-1` (`values[j] < values[i]`) or `0`\n * (tie); `s` is the sum of those votes. Under the null hypothesis of no trend, `s` is\n * approximately normal with mean `0` and a known variance (corrected here for tied values), which\n * turns `s` into a Z score and a two-tailed p-value. `trend` is only `'increasing'` / `'decreasing'`\n * when that p-value clears `alpha` — a handful of mostly-ascending points is exactly what noise\n * looks like half the time, and this is what keeps that from reading as a trend.\n *\n * O(n²) (every pair is compared); fine for the small, per-tick sample counts detectors work with,\n * not for large historical series.\n */\nexport function mannKendall(values: number[], alpha = 0.05): MannKendallResult {\n\tconst n = values.length;\n\n\tif (n < 2) return { s: 0, variance: 0, z: 0, pValue: 1, trend: 'no-trend' };\n\n\tlet s = 0;\n\n\tfor (let i = 0; i < n - 1; i++) {\n\t\tfor (let j = i + 1; j < n; j++) {\n\t\t\ts += Math.sign(values[j] - values[i]);\n\t\t}\n\t}\n\n\t// Tie correction: ties reduce the number of informative comparisons, so they shrink the\n\t// variance relative to the tie-free formula `n(n-1)(2n+5)/18`.\n\tconst tieGroupSizes = new Map<number, number>();\n\n\tfor (const value of values) tieGroupSizes.set(value, (tieGroupSizes.get(value) ?? 0) + 1);\n\n\tlet tieCorrection = 0;\n\n\tfor (const groupSize of tieGroupSizes.values()) {\n\t\tif (1 < groupSize) tieCorrection += groupSize * (groupSize - 1) * ((2 * groupSize) + 5);\n\t}\n\n\tconst variance = ((n * (n - 1) * ((2 * n) + 5)) - tieCorrection) / 18;\n\n\treturn { s, variance, ...mannKendallVerdict(s, variance, alpha) };\n}\n\n/**\n * Turns a Mann-Kendall `s` statistic and its `variance` into a Z score, p-value and verdict — the\n * back half of {@link mannKendall}, split out so an incremental caller that maintains `s` /\n * `variance` itself (e.g. over a sliding window, correcting for evicted points rather than\n * recomputing every pair from scratch) doesn't have to reimplement the normal approximation.\n */\nexport function mannKendallVerdict(s: number, variance: number, alpha = 0.05): Pick<MannKendallResult, 'z' | 'pValue' | 'trend'> {\n\tlet z = 0;\n\n\tif (variance > 0) {\n\t\tif (s > 0) z = (s - 1) / Math.sqrt(variance);\n\t\telse if (s < 0) z = (s + 1) / Math.sqrt(variance);\n\t}\n\n\tconst pValue = 2 * (1 - standardNormalCdf(Math.abs(z)));\n\tconst trend = z !== 0 && pValue < alpha ? (z > 0 ? 'increasing' : 'decreasing') : 'no-trend';\n\n\treturn { z, pValue, trend };\n}\n\n/** Standard normal CDF via the Abramowitz & Stegun erf approximation (max error ~1.5e-7). */\nfunction standardNormalCdf(x: number): number {\n\treturn 0.5 * (1 + erf(x / Math.SQRT2));\n}\n\nfunction erf(x: number): number {\n\tconst sign = x < 0 ? -1 : 1;\n\tconst ax = Math.abs(x);\n\n\tconst a1 = 0.254829592;\n\tconst a2 = -0.284496736;\n\tconst a3 = 1.421413741;\n\tconst a4 = -1.453152027;\n\tconst a5 = 1.061405429;\n\tconst p = 0.3275911;\n\n\tconst t = 1 / (1 + (p * ax));\n\tconst y = 1 - (((((((((a5 * t) + a4) * t) + a3) * t) + a2) * t) + a1) * t * Math.exp(-(ax * ax)));\n\n\treturn sign * y;\n}\n","import { MediaKind } from './common/types';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { InboundRtpStats } from './schema/ClientSample';\nimport { counterDelta } from './utils/stats';\n\nexport class ObservedInboundRtp implements InboundRtpStats {\n\tpublic appData?: Record<string, unknown>;\n\n\tprivate _visited = false;\n\n\ttransportId?: string | undefined;\n\tcodecId?: string | undefined;\n\tpacketsReceived?: number | undefined;\n\tpacketsLost?: number | undefined;\n\tmid?: string | undefined;\n\tremoteId?: string | undefined;\n\tframesDecoded?: number | undefined;\n\tkeyFramesDecoded?: number | undefined;\n\tframesRendered?: number | undefined;\n\tframesDropped?: number | undefined;\n\tframeWidth?: number | undefined;\n\tframeHeight?: number | undefined;\n\tframesPerSecond?: number | undefined;\n\tqpSum?: number | undefined;\n\ttotalDecodeTime?: number | undefined;\n\ttotalInterFrameDelay?: number | undefined;\n\ttotalSquaredInterFrameDelay?: number | undefined;\n\tpauseCount?: number | undefined;\n\ttotalPausesDuration?: number | undefined;\n\tfreezeCount?: number | undefined;\n\ttotalFreezesDuration?: number | undefined;\n\tlastPacketReceivedTimestamp?: number | undefined;\n\theaderBytesReceived?: number | undefined;\n\tpacketsDiscarded?: number | undefined;\n\tfecBytesReceived?: number | undefined;\n\tfecPacketsReceived?: number | undefined;\n\tfecPacketsDiscarded?: number | undefined;\n\tbytesReceived?: number | undefined;\n\tnackCount?: number | undefined;\n\tfirCount?: number | undefined;\n\tpliCount?: number | undefined;\n\ttotalProcessingDelay?: number | undefined;\n\testimatedPlayoutTimestamp?: number | undefined;\n\tjitterBufferDelay?: number | undefined;\n\tjitterBufferTargetDelay?: number | undefined;\n\tjitterBufferEmittedCount?: number | undefined;\n\tjitterBufferMinimumDelay?: number | undefined;\n\ttotalSamplesReceived?: number | undefined;\n\tconcealedSamples?: number | undefined;\n\tsilentConcealedSamples?: number | undefined;\n\tconcealmentEvents?: number | undefined;\n\tinsertedSamplesForDeceleration?: number | undefined;\n\tremovedSamplesForAcceleration?: number | undefined;\n\taudioLevel?: number | undefined;\n\ttotalAudioEnergy?: number | undefined;\n\ttotalSamplesDuration?: number | undefined;\n\tframesReceived?: number | undefined;\n\tdecoderImplementation?: string | undefined;\n\tplayoutId?: string | undefined;\n\tpowerEfficientDecoder?: boolean | undefined;\n\tframesAssembledFromMultiplePackets?: number | undefined;\n\ttotalAssemblyTime?: number | undefined;\n\tretransmittedPacketsReceived?: number | undefined;\n\tretransmittedBytesReceived?: number | undefined;\n\trtxSsrc?: number | undefined;\n\tfecSsrc?: number | undefined;\n\ttotalCorruptionProbability?: number | undefined;\n\ttotalSquaredCorruptionProbability?: number | undefined;\n\tcorruptionMeasurements?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\tjitter?: number | undefined;\n\t\n\tpublic bitrate = 0;\n\tpublic fractionLost?: number;\n\tpublic bitPerPixel = 0;\n\n\tpublic deltaLostPackets = 0;\n\tpublic deltaReceivedPackets = 0;\n\tpublic deltaBytesReceived = 0;\n\tpublic deltaReceivedSamples = 0;\n\tpublic deltaSilentConcealedSamples = 0;\n\n\t// Per-tick, counter-reset-safe deltas of the cumulative RTCStats counters. These are what\n\t// detectors need (\"how many freezes/PLIs in THIS tick\"), as opposed to the lifetime totals.\n\tpublic deltaConcealedSamples = 0;\n\tpublic deltaConcealmentEvents = 0;\n\tpublic deltaFreezeCount = 0;\n\tpublic deltaFreezesDuration = 0;\n\tpublic deltaPliCount = 0;\n\tpublic deltaNackCount = 0;\n\tpublic deltaFirCount = 0;\n\tpublic deltaPacketsDiscarded = 0;\n\tpublic deltaFramesDecoded = 0;\n\tpublic deltaFramesReceived = 0;\n\tpublic deltaFramesRendered = 0;\n\tpublic deltaFramesDropped = 0;\n\tpublic deltaKeyFramesDecoded = 0;\n\tpublic deltaDecodeTime = 0;\n\tpublic deltaJitterBufferDelay = 0;\n\tpublic deltaJitterBufferEmittedCount = 0;\n\tpublic deltaRetransmittedPacketsReceived = 0;\n\tpublic deltaFecPacketsReceived = 0;\n\tpublic deltaFecPacketsDiscarded = 0;\n\tpublic deltaPausesDuration = 0;\n\n\t/**\n\t * Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from\n\t * the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to\n\t * read those two counters.\n\t */\n\tpublic jitterBufferDelayInMs?: number;\n\n\t/** Fraction of the samples received in this tick that were concealed (0..1). */\n\tpublic concealmentRatio?: number;\n\n\t/** Fraction of the frames received in this tick that were dropped before rendering (0..1). */\n\tpublic framesDroppedRatio?: number;\n\n\t/**\n\t * `true` when the codec or decoder implementation changed in this tick.\n\t *\n\t * Chrome resets `packetsReceived`/`bytesReceived` on an SSRC when the codec switches\n\t * (crbug.com/webrtc/5361, open since 2015), which shows up as a sawtooth spike or a negative\n\t * rate. Every delta in this tick is therefore suppressed to `0` rather than reported as traffic\n\t * — otherwise a room-wide codec rollout produces a synchronized fake-degradation alert across\n\t * every participant at once.\n\t */\n\tpublic counterResetBoundary = false;\n\n\t// Derived from the corresponding remote-outbound-rtp (sender report), when present.\n\tpublic remoteRttInMs?: number;\n\tpublic remoteBytesSent?: number;\n\tpublic remotePacketsSent?: number;\n\tpublic remoteTimestamp?: number;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic ssrc: number,\n\t\tpublic kind: MediaKind,\n\t\tpublic trackIdentifier: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getRemoteOutboundRtp() {\n\t\treturn this._peerConnection.observedRemoteOutboundRtps.get(this.ssrc);\n\t}\n\n\tpublic getIceTransport() {\n\t\treturn this._peerConnection.observedIceTransports.get(this.transportId ?? '');\n\t}\n\n\tpublic getCodec() {\n\t\treturn this._peerConnection.observedCodecs.get(this.codecId ?? '');\n\t}\n\n\tpublic getMediaPlayout() {\n\t\treturn this._peerConnection.observedMediaPlayouts.get(this.playoutId ?? '');\n\t}\n\n\tpublic getTrack() {\n\t\treturn this._peerConnection.observedInboundTracks.get(this.trackIdentifier);\n\t}\n\n\tpublic update(stats: InboundRtpStats) {\n\t\tthis._visited = true;\n\t\tthis.deltaBytesReceived = 0;\n\t\tthis.deltaLostPackets = 0;\n\t\tthis.deltaReceivedPackets = 0;\n\t\tthis.deltaReceivedSamples = 0;\n\t\tthis.deltaSilentConcealedSamples = 0;\n\t\tthis.remoteRttInMs = undefined;\n\t\tthis.remoteBytesSent = undefined;\n\t\tthis.remotePacketsSent = undefined;\n\t\tthis.remoteTimestamp = undefined;\n\t\tthis.bitrate = 0;\n\t\tthis.fractionLost = undefined;\n\t\tthis.bitPerPixel = 0;\n\n\t\tthis.deltaConcealedSamples = 0;\n\t\tthis.deltaConcealmentEvents = 0;\n\t\tthis.deltaFreezeCount = 0;\n\t\tthis.deltaFreezesDuration = 0;\n\t\tthis.deltaPausesDuration = 0;\n\t\tthis.deltaPliCount = 0;\n\t\tthis.deltaNackCount = 0;\n\t\tthis.deltaFirCount = 0;\n\t\tthis.deltaPacketsDiscarded = 0;\n\t\tthis.deltaFramesDecoded = 0;\n\t\tthis.deltaFramesReceived = 0;\n\t\tthis.deltaFramesRendered = 0;\n\t\tthis.deltaFramesDropped = 0;\n\t\tthis.deltaKeyFramesDecoded = 0;\n\t\tthis.deltaDecodeTime = 0;\n\t\tthis.deltaJitterBufferDelay = 0;\n\t\tthis.deltaJitterBufferEmittedCount = 0;\n\t\tthis.deltaRetransmittedPacketsReceived = 0;\n\t\tthis.deltaFecPacketsReceived = 0;\n\t\tthis.deltaFecPacketsDiscarded = 0;\n\t\tthis.jitterBufferDelayInMs = undefined;\n\t\tthis.concealmentRatio = undefined;\n\t\tthis.framesDroppedRatio = undefined;\n\n\t\tconst elapsedTimeInMs = stats.timestamp - this.timestamp;\n\n\t\t// crbug.com/webrtc/5361: a codec switch resets the cumulative counters on the SSRC. Treat the\n\t\t// tick as a boundary and derive nothing from it, instead of reporting a phantom spike.\n\t\tthis.counterResetBoundary = (this.codecId !== undefined && stats.codecId !== undefined && this.codecId !== stats.codecId)\n\t\t\t|| (this.decoderImplementation !== undefined && stats.decoderImplementation !== undefined\n\t\t\t\t&& this.decoderImplementation !== stats.decoderImplementation);\n\n\t\tif (0 < elapsedTimeInMs && !this.counterResetBoundary) {\n\t\t\tconst elapsedInSeconds = elapsedTimeInMs / 1000;\n\n\t\t\t// All deltas are counter-reset-safe and treat a previous value of `0` as a valid baseline\n\t\t\t// (a truthiness guard here silently drops the first interval of every counter).\n\t\t\tthis.deltaBytesReceived = counterDelta(this.bytesReceived, stats.bytesReceived);\n\t\t\tthis.deltaLostPackets = counterDelta(this.packetsLost, stats.packetsLost);\n\t\t\tthis.deltaReceivedPackets = counterDelta(this.packetsReceived, stats.packetsReceived);\n\t\t\tthis.deltaReceivedSamples = counterDelta(this.totalSamplesReceived, stats.totalSamplesReceived);\n\t\t\tthis.deltaSilentConcealedSamples = counterDelta(this.silentConcealedSamples, stats.silentConcealedSamples);\n\n\t\t\tthis.deltaConcealedSamples = counterDelta(this.concealedSamples, stats.concealedSamples);\n\t\t\tthis.deltaConcealmentEvents = counterDelta(this.concealmentEvents, stats.concealmentEvents);\n\t\t\tthis.deltaFreezeCount = counterDelta(this.freezeCount, stats.freezeCount);\n\t\t\tthis.deltaFreezesDuration = counterDelta(this.totalFreezesDuration, stats.totalFreezesDuration);\n\t\t\tthis.deltaPausesDuration = counterDelta(this.totalPausesDuration, stats.totalPausesDuration);\n\t\t\tthis.deltaPliCount = counterDelta(this.pliCount, stats.pliCount);\n\t\t\tthis.deltaNackCount = counterDelta(this.nackCount, stats.nackCount);\n\t\t\tthis.deltaFirCount = counterDelta(this.firCount, stats.firCount);\n\t\t\tthis.deltaPacketsDiscarded = counterDelta(this.packetsDiscarded, stats.packetsDiscarded);\n\t\t\tthis.deltaFramesDecoded = counterDelta(this.framesDecoded, stats.framesDecoded);\n\t\t\tthis.deltaFramesReceived = counterDelta(this.framesReceived, stats.framesReceived);\n\t\t\tthis.deltaFramesRendered = counterDelta(this.framesRendered, stats.framesRendered);\n\t\t\tthis.deltaFramesDropped = counterDelta(this.framesDropped, stats.framesDropped);\n\t\t\tthis.deltaKeyFramesDecoded = counterDelta(this.keyFramesDecoded, stats.keyFramesDecoded);\n\t\t\tthis.deltaDecodeTime = counterDelta(this.totalDecodeTime, stats.totalDecodeTime);\n\t\t\tthis.deltaJitterBufferDelay = counterDelta(this.jitterBufferDelay, stats.jitterBufferDelay);\n\t\t\tthis.deltaJitterBufferEmittedCount = counterDelta(this.jitterBufferEmittedCount, stats.jitterBufferEmittedCount);\n\t\t\tthis.deltaRetransmittedPacketsReceived = counterDelta(this.retransmittedPacketsReceived, stats.retransmittedPacketsReceived);\n\t\t\tthis.deltaFecPacketsReceived = counterDelta(this.fecPacketsReceived, stats.fecPacketsReceived);\n\t\t\tthis.deltaFecPacketsDiscarded = counterDelta(this.fecPacketsDiscarded, stats.fecPacketsDiscarded);\n\n\t\t\t// bits per second (matches ObservedOutboundRtp.bitrate and the client-level bitrates).\n\t\t\tthis.bitrate = (this.deltaBytesReceived * 8) / elapsedInSeconds;\n\n\t\t\t// bits per pixel of the frames decoded in this tick.\n\t\t\tconst pixels = (stats.frameWidth ?? 0) * (stats.frameHeight ?? 0);\n\n\t\t\tif (0 < pixels && 0 < this.deltaFramesReceived) {\n\t\t\t\tthis.bitPerPixel = (this.deltaBytesReceived * 8) / (this.deltaFramesReceived * pixels);\n\t\t\t}\n\n\t\t\t// Report 0 (not `undefined`) when there was traffic but no loss, so detectors can tell\n\t\t\t// \"healthy\" apart from \"no data\".\n\t\t\tif (0 < this.deltaReceivedPackets || 0 < this.deltaLostPackets) {\n\t\t\t\tthis.fractionLost = this.deltaLostPackets / (this.deltaLostPackets + this.deltaReceivedPackets);\n\t\t\t}\n\t\t\tif (0 < this.deltaJitterBufferEmittedCount) {\n\t\t\t\tthis.jitterBufferDelayInMs = (this.deltaJitterBufferDelay / this.deltaJitterBufferEmittedCount) * 1000;\n\t\t\t}\n\t\t\tif (0 < this.deltaReceivedSamples) {\n\t\t\t\tthis.concealmentRatio = this.deltaConcealedSamples / this.deltaReceivedSamples;\n\t\t\t}\n\t\t\tif (0 < this.deltaFramesReceived) {\n\t\t\t\tthis.framesDroppedRatio = this.deltaFramesDropped / this.deltaFramesReceived;\n\t\t\t}\n\t\t}\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.codecId = stats.codecId;\n\t\tthis.packetsReceived = stats.packetsReceived;\n\t\tthis.packetsLost = stats.packetsLost;\n\t\tthis.mid = stats.mid;\n\t\tthis.remoteId = stats.remoteId;\n\t\tthis.framesDecoded = stats.framesDecoded;\n\t\tthis.keyFramesDecoded = stats.keyFramesDecoded;\n\t\tthis.framesRendered = stats.framesRendered;\n\t\tthis.framesDropped = stats.framesDropped;\n\t\tthis.frameWidth = stats.frameWidth;\n\t\tthis.frameHeight = stats.frameHeight;\n\t\tthis.framesPerSecond = stats.framesPerSecond;\n\t\tthis.qpSum = stats.qpSum;\n\t\tthis.totalDecodeTime = stats.totalDecodeTime;\n\t\tthis.totalInterFrameDelay = stats.totalInterFrameDelay;\n\t\tthis.totalSquaredInterFrameDelay = stats.totalSquaredInterFrameDelay;\n\t\tthis.pauseCount = stats.pauseCount;\n\t\tthis.totalPausesDuration = stats.totalPausesDuration;\n\t\tthis.freezeCount = stats.freezeCount;\n\t\tthis.totalFreezesDuration = stats.totalFreezesDuration;\n\t\tthis.lastPacketReceivedTimestamp = stats.lastPacketReceivedTimestamp;\n\t\tthis.headerBytesReceived = stats.headerBytesReceived;\n\t\tthis.packetsDiscarded = stats.packetsDiscarded;\n\t\tthis.fecBytesReceived = stats.fecBytesReceived;\n\t\tthis.fecPacketsReceived = stats.fecPacketsReceived;\n\t\tthis.fecPacketsDiscarded = stats.fecPacketsDiscarded;\n\t\tthis.bytesReceived = stats.bytesReceived;\n\t\tthis.nackCount = stats.nackCount;\n\t\tthis.firCount = stats.firCount;\n\t\tthis.pliCount = stats.pliCount;\n\t\tthis.totalProcessingDelay = stats.totalProcessingDelay;\n\t\tthis.estimatedPlayoutTimestamp = stats.estimatedPlayoutTimestamp;\n\t\tthis.jitterBufferDelay = stats.jitterBufferDelay;\n\t\tthis.jitterBufferTargetDelay = stats.jitterBufferTargetDelay;\n\t\tthis.jitterBufferEmittedCount = stats.jitterBufferEmittedCount;\n\t\tthis.jitterBufferMinimumDelay = stats.jitterBufferMinimumDelay;\n\t\tthis.totalSamplesReceived = stats.totalSamplesReceived;\n\t\tthis.concealedSamples = stats.concealedSamples;\n\t\tthis.silentConcealedSamples = stats.silentConcealedSamples;\n\t\tthis.concealmentEvents = stats.concealmentEvents;\n\t\t// Previously reset to `undefined` at the top of update() but never assigned, so `jitter`\n\t\t// was permanently undefined on every inbound RTP.\n\t\tthis.jitter = stats.jitter;\n\t\tthis.framesReceived = stats.framesReceived;\n\t\tthis.retransmittedPacketsReceived = stats.retransmittedPacketsReceived;\n\t\tthis.retransmittedBytesReceived = stats.retransmittedBytesReceived;\n\t}\n}\n","import { MediaKind } from './common/types';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { OutboundRtpStats, QualityLimitationDurations } from './schema/ClientSample';\nimport { counterDelta } from './utils/stats';\n\nexport class ObservedOutboundRtp implements OutboundRtpStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\ttransportId?: string | undefined;\n\tcodecId?: string | undefined;\n\tpacketsSent?: number | undefined;\n\tbytesSent?: number | undefined;\n\tmid?: string | undefined;\n\tmediaSourceId?: string | undefined;\n\tremoteId?: string | undefined;\n\trid?: string | undefined;\n\theaderBytesSent?: number | undefined;\n\tretransmittedPacketsSent?: number | undefined;\n\tretransmittedBytesSent?: number | undefined;\n\trtxSsrc?: number | undefined;\n\ttargetBitrate?: number | undefined;\n\ttotalEncodedBytesTarget?: number | undefined;\n\tframeWidth?: number | undefined;\n\tframeHeight?: number | undefined;\n\tframesPerSecond?: number | undefined;\n\tframesSent?: number | undefined;\n\thugeFramesSent?: number | undefined;\n\tframesEncoded?: number | undefined;\n\tkeyFramesEncoded?: number | undefined;\n\tqpSum?: number | undefined;\n\ttotalEncodeTime?: number | undefined;\n\ttotalPacketSendDelay?: number | undefined;\n\tqualityLimitationReason?: string | undefined;\n\tqualityLimitationResolutionChanges?: number | undefined;\n\tnackCount?: number | undefined;\n\tfirCount?: number | undefined;\n\tpliCount?: number | undefined;\n\tencoderImplementation?: string | undefined;\n\tpowerEfficientEncoder?: boolean | undefined;\n\tactive?: boolean | undefined;\n\tscalabilityMode?: string | undefined;\n\tqualityLimitationDurations?: QualityLimitationDurations | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\t// derived fields\n\tpublic bitrate = 0;\n\tpublic payloadBitrate = 0;\n\tpublic packetRate = 0;\n\tpublic bitPerPixel = 0;\n\t\n\tpublic deltaPacketsSent = 0;\n\tpublic deltaBytesSent = 0;\n\n\t// Per-tick, counter-reset-safe deltas of the cumulative RTCStats counters.\n\tpublic deltaFramesSent = 0;\n\tpublic deltaFramesEncoded = 0;\n\tpublic deltaKeyFramesEncoded = 0;\n\tpublic deltaNackCount = 0;\n\tpublic deltaPliCount = 0;\n\tpublic deltaFirCount = 0;\n\tpublic deltaRetransmittedPacketsSent = 0;\n\tpublic deltaRetransmittedBytesSent = 0;\n\tpublic deltaEncodeTime = 0;\n\tpublic deltaQualityLimitationResolutionChanges = 0;\n\n\t/**\n\t * `true` when the codec, encoder implementation or scalability mode changed in this tick.\n\t *\n\t * Chrome resets `packetsSent`/`bytesSent` on the SSRC when the codec switches\n\t * (crbug.com/webrtc/5361), producing sawtooth spikes and negative bitrates. All deltas for the\n\t * tick are suppressed so a codec change is never mistaken for a traffic event.\n\t */\n\tpublic counterResetBoundary = false;\n\n\t// Derived from the corresponding remote-inbound-rtp (receiver report), when present.\n\tpublic remoteRttInMs?: number;\n\tpublic remoteFractionLost?: number;\n\tpublic remoteJitter?: number;\n\tpublic remotePacketsLost?: number;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic ssrc: number,\n\t\tpublic kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection,\n\t) {\n\t}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\t\n\t\tthis._visited = false;\n\t\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getRemoteInboundRtp() {\n\t\treturn this._peerConnection.observedRemoteInboundRtps.get(this.ssrc);\n\t}\n\n\tpublic getCodec() {\n\t\treturn this._peerConnection.observedCodecs.get(this.codecId ?? '');\n\t}\n\n\tpublic getMediaSource() {\n\t\treturn this._peerConnection.observedMediaSources.get(this.mediaSourceId ?? '');\n\t}\n\n\tpublic getTrack() {\n\t\treturn this.getMediaSource()?.getTrack();\n\t}\n\n\tpublic update(stats: OutboundRtpStats) {\n\t\tthis._visited = true;\n\t\tthis.bitPerPixel = 0;\n\t\tthis.bitrate = 0;\n\t\tthis.payloadBitrate = 0;\n\t\tthis.packetRate = 0;\n\t\tthis.deltaPacketsSent = 0;\n\t\tthis.deltaBytesSent = 0;\n\t\tthis.deltaFramesSent = 0;\n\t\tthis.deltaFramesEncoded = 0;\n\t\tthis.deltaKeyFramesEncoded = 0;\n\t\tthis.deltaNackCount = 0;\n\t\tthis.deltaPliCount = 0;\n\t\tthis.deltaFirCount = 0;\n\t\tthis.deltaRetransmittedPacketsSent = 0;\n\t\tthis.deltaRetransmittedBytesSent = 0;\n\t\tthis.deltaEncodeTime = 0;\n\t\tthis.deltaQualityLimitationResolutionChanges = 0;\n\t\tthis.remoteRttInMs = undefined;\n\t\tthis.remoteFractionLost = undefined;\n\t\tthis.remoteJitter = undefined;\n\t\tthis.remotePacketsLost = undefined;\n\n\t\tconst elapsedTimeInMs = stats.timestamp - this.timestamp;\n\n\t\t// crbug.com/webrtc/5361: the cumulative counters reset on a codec/encoder switch.\n\t\tconst changed = (a?: string, b?: string) => a !== undefined && b !== undefined && a !== b;\n\n\t\tthis.counterResetBoundary = changed(this.codecId, stats.codecId)\n\t\t\t|| changed(this.encoderImplementation, stats.encoderImplementation)\n\t\t\t|| changed(this.scalabilityMode, stats.scalabilityMode);\n\n\t\tif (elapsedTimeInMs && !this.counterResetBoundary) {\n\t\t\t// Guard against counter resets / SSRC reuse: only accept a non-negative delta.\n\t\t\tif (stats.packetsSent !== undefined && this.packetsSent !== undefined && stats.packetsSent >= this.packetsSent) {\n\t\t\t\tthis.deltaPacketsSent = stats.packetsSent - this.packetsSent;\n\t\t\t\tthis.packetRate = this.deltaPacketsSent / (elapsedTimeInMs / 1000);\n\t\t\t}\n\t\t\tif (stats.bytesSent !== undefined && this.bytesSent !== undefined && stats.bytesSent >= this.bytesSent) {\n\t\t\t\tthis.deltaBytesSent = stats.bytesSent - this.bytesSent;\n\t\t\t\tthis.bitrate = (this.deltaBytesSent * 8) / (elapsedTimeInMs / 1000);\n\t\t\t}\n\t\t\t// NOTE `??` binds looser than `-`, so the previous expression parsed as\n\t\t\t// `deltaBytesSent ?? (0 - headerDelta)` and always yielded `deltaBytesSent` — i.e.\n\t\t\t// payloadBitrate silently equalled bitrate and never subtracted the header bytes.\n\t\t\tconst deltaHeaderBytesSent = counterDelta(this.headerBytesSent, stats.headerBytesSent);\n\n\t\t\tthis.payloadBitrate = (Math.max(0, this.deltaBytesSent - deltaHeaderBytesSent) * 8) / (elapsedTimeInMs / 1000);\n\n\t\t\tthis.deltaFramesSent = counterDelta(this.framesSent, stats.framesSent);\n\t\t\tthis.deltaFramesEncoded = counterDelta(this.framesEncoded, stats.framesEncoded);\n\t\t\tthis.deltaKeyFramesEncoded = counterDelta(this.keyFramesEncoded, stats.keyFramesEncoded);\n\t\t\tthis.deltaNackCount = counterDelta(this.nackCount, stats.nackCount);\n\t\t\tthis.deltaPliCount = counterDelta(this.pliCount, stats.pliCount);\n\t\t\tthis.deltaFirCount = counterDelta(this.firCount, stats.firCount);\n\t\t\tthis.deltaRetransmittedPacketsSent = counterDelta(this.retransmittedPacketsSent, stats.retransmittedPacketsSent);\n\t\t\tthis.deltaRetransmittedBytesSent = counterDelta(this.retransmittedBytesSent, stats.retransmittedBytesSent);\n\t\t\tthis.deltaEncodeTime = counterDelta(this.totalEncodeTime, stats.totalEncodeTime);\n\t\t\tthis.deltaQualityLimitationResolutionChanges = counterDelta(\n\t\t\t\tthis.qualityLimitationResolutionChanges, stats.qualityLimitationResolutionChanges,\n\t\t\t);\n\n\t\t\t// bits per pixel of the frames sent in this tick. (The previous expression subtracted in\n\t\t\t// reverse — `previous - current` — so it was always negative, and divided by frames, not pixels.)\n\t\t\tconst pixels = (stats.frameWidth ?? 0) * (stats.frameHeight ?? 0);\n\n\t\t\tif (0 < pixels && 0 < this.deltaFramesSent) {\n\t\t\t\tthis.bitPerPixel = (this.deltaBytesSent * 8) / (this.deltaFramesSent * pixels);\n\t\t\t}\n\t\t}\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.codecId = stats.codecId;\n\t\tthis.packetsSent = stats.packetsSent;\n\t\tthis.bytesSent = stats.bytesSent;\n\t\tthis.mid = stats.mid;\n\t\tthis.mediaSourceId = stats.mediaSourceId;\n\t\tthis.remoteId = stats.remoteId;\n\t\tthis.rid = stats.rid;\n\t\tthis.headerBytesSent = stats.headerBytesSent;\n\t\tthis.retransmittedPacketsSent = stats.retransmittedPacketsSent;\n\t\tthis.retransmittedBytesSent = stats.retransmittedBytesSent;\n\t\tthis.rtxSsrc = stats.rtxSsrc;\n\t\tthis.targetBitrate = stats.targetBitrate;\n\t\tthis.totalEncodedBytesTarget = stats.totalEncodedBytesTarget;\n\t\tthis.frameWidth = stats.frameWidth;\n\t\tthis.frameHeight = stats.frameHeight;\n\t\tthis.framesPerSecond = stats.framesPerSecond;\n\t\tthis.framesSent = stats.framesSent;\n\t\tthis.hugeFramesSent = stats.hugeFramesSent;\n\t\tthis.framesEncoded = stats.framesEncoded;\n\t\tthis.keyFramesEncoded = stats.keyFramesEncoded;\n\t\tthis.qpSum = stats.qpSum;\n\t\tthis.totalEncodeTime = stats.totalEncodeTime;\n\t\tthis.totalPacketSendDelay = stats.totalPacketSendDelay;\n\t\tthis.qualityLimitationReason = stats.qualityLimitationReason;\n\t\tthis.qualityLimitationResolutionChanges = stats.qualityLimitationResolutionChanges;\n\t\tthis.nackCount = stats.nackCount;\n\t\tthis.firCount = stats.firCount;\n\t\tthis.pliCount = stats.pliCount;\n\t\tthis.encoderImplementation = stats.encoderImplementation;\n\t\tthis.powerEfficientEncoder = stats.powerEfficientEncoder;\n\t\tthis.active = stats.active;\n\t\tthis.scalabilityMode = stats.scalabilityMode;\n\t\tthis.qualityLimitationDurations = stats.qualityLimitationDurations;\n\t\tthis.attachments = stats.attachments;\n\t}\n}","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { CertificateStats } from './schema/ClientSample';\n\nexport class ObservedCertificate implements CertificateStats {\n\tpublic appData?: Record<string, unknown>;\n\n\tprivate _visited = false;\n\n\tfingerprint?: string;\n\tfingerprintAlgorithm?: string;\n\tbase64Certificate?: string;\n\tissuerCertificateId?: string;\n\tattachments?: Record<string, unknown>;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic update(stats: CertificateStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.fingerprint = stats.fingerprint;\n\t\tthis.fingerprintAlgorithm = stats.fingerprintAlgorithm;\n\t\tthis.base64Certificate = stats.base64Certificate;\n\t\tthis.issuerCertificateId = stats.issuerCertificateId;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { CodecStats } from './schema/ClientSample';\n\nexport class ObservedCodec implements CodecStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\tpayloadType?: number | undefined;\n\ttransportId?: string | undefined;\n\tclockRate?: number | undefined;\n\tchannels?: number | undefined;\n\tsdpFmtpLine?: string | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic mimeType: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getIceTransport() {\n\t\treturn this._peerConnection.observedIceTransports.get(this.transportId ?? '');\n\t}\n\n\tpublic update(stats: CodecStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.payloadType = stats.payloadType;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.clockRate = stats.clockRate;\n\t\tthis.channels = stats.channels;\n\t\tthis.sdpFmtpLine = stats.sdpFmtpLine;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { DataChannelStats } from './schema/ClientSample';\n\nexport type ObservedDataChannelState = 'connecting' | 'open' | 'closing' | 'closed';\n\nexport class ObservedDataChannel implements DataChannelStats\t{\n\tprivate _visited = false;\n\t\n\tlabel?: string | undefined;\n\tprotocol?: string | undefined;\n\tdataChannelIdentifier?: number | undefined;\n\tstate?: string | undefined;\n\tmessagesSent?: number | undefined;\n\tbytesSent?: number | undefined;\n\tmessagesReceived?: number | undefined;\n\tbytesReceived?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic addedAt?: number | undefined;\n\tpublic removedAt?: number | undefined;\n\t\n\tpublic deltaBytesSent = 0;\n\tpublic deltaBytesReceived = 0;\n\tpublic deltaMessagesSent = 0;\n\tpublic deltaMessagesReceived = 0;\n\n\tpublic appData?: Record<string, unknown>;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection,\n\t) {\n\t}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\t\n\t\tthis._visited = false;\n\t\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic update(stats: DataChannelStats) {\n\t\tthis._visited = true;\n\n\t\tif (this.messagesSent && stats.messagesSent && stats.messagesSent >= this.messagesSent) {\n\t\t\tthis.deltaMessagesSent = stats.messagesSent - this.messagesSent;\n\t\t} else {\n\t\t\tthis.deltaMessagesSent = 0;\n\t\t}\n\n\t\tif (this.messagesReceived && stats.messagesReceived && stats.messagesReceived >= this.messagesReceived) {\n\t\t\tthis.deltaMessagesReceived = stats.messagesReceived - this.messagesReceived;\n\t\t} else {\n\t\t\tthis.deltaMessagesReceived = 0;\n\t\t}\n\n\t\tif (this.bytesSent && stats.bytesSent && stats.bytesSent >= this.bytesSent) {\n\t\t\tthis.deltaBytesSent = stats.bytesSent - this.bytesSent;\n\t\t} else {\n\t\t\tthis.deltaBytesSent = 0;\n\t\t}\n\n\t\tif (this.bytesReceived && stats.bytesReceived && stats.bytesReceived >= this.bytesReceived) {\n\t\t\tthis.deltaBytesReceived = stats.bytesReceived - this.bytesReceived;\n\t\t} else {\n\t\t\tthis.deltaBytesReceived = 0;\n\t\t}\n\n\t\tthis.label = stats.label;\n\t\tthis.protocol = stats.protocol;\n\t\tthis.dataChannelIdentifier = stats.dataChannelIdentifier;\n\t\tthis.state = stats.state;\n\t\tthis.messagesSent = stats.messagesSent;\n\t\tthis.bytesSent = stats.bytesSent;\n\t\tthis.messagesReceived = stats.messagesReceived;\n\t\tthis.bytesReceived = stats.bytesReceived;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { IceCandidateStats } from './schema/ClientSample';\n\nexport class ObservedIceCandidate implements IceCandidateStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\ttransportId?: string | undefined;\n\taddress?: string | undefined;\n\tport?: number | undefined;\n\tprotocol?: string | undefined;\n\tcandidateType?: string | undefined;\n\tpriority?: number | undefined;\n\turl?: string | undefined;\n\trelayProtocol?: string | undefined;\n\tfoundation?: string | undefined;\n\trelatedAddress?: string | undefined;\n\trelatedPort?: number | undefined;\n\tusernameFragment?: string | undefined;\n\ttcpType?: string | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getIceTransport() {\n\t\treturn this._peerConnection.observedIceTransports.get(this.transportId ?? '');\n\t}\n\n\tpublic update(stats: IceCandidateStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.address = stats.address;\n\t\tthis.port = stats.port;\n\t\tthis.protocol = stats.protocol;\n\t\tthis.candidateType = stats.candidateType;\n\t\tthis.priority = stats.priority;\n\t\tthis.url = stats.url;\n\t\tthis.relayProtocol = stats.relayProtocol;\n\t\tthis.foundation = stats.foundation;\n\t\tthis.relatedAddress = stats.relatedAddress;\n\t\tthis.relatedPort = stats.relatedPort;\n\t\tthis.usernameFragment = stats.usernameFragment;\n\t\tthis.tcpType = stats.tcpType;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { IceCandidatePairStats } from './schema/ClientSample';\n\nexport class ObservedIceCandidatePair implements IceCandidatePairStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\ttransportId?: string | undefined;\n\tlocalCandidateId?: string | undefined;\n\tremoteCandidateId?: string | undefined;\n\tstate?: 'new' | 'in-progress' | 'waiting' | 'failed' | 'succeeded' | undefined;\n\tnominated?: boolean | undefined;\n\tpacketsSent?: number | undefined;\n\tpacketsReceived?: number | undefined;\n\tbytesSent?: number | undefined;\n\tbytesReceived?: number | undefined;\n\tlastPacketSentTimestamp?: number | undefined;\n\tlastPacketReceivedTimestamp?: number | undefined;\n\ttotalRoundTripTime?: number | undefined;\n\tcurrentRoundTripTime?: number | undefined;\n\tavailableOutgoingBitrate?: number | undefined;\n\tavailableIncomingBitrate?: number | undefined;\n\trequestsReceived?: number | undefined;\n\trequestsSent?: number | undefined;\n\tresponsesReceived?: number | undefined;\n\tresponsesSent?: number | undefined;\n\tconsentRequestsSent?: number | undefined;\n\tpacketsDiscardedOnSend?: number | undefined;\n\tbytesDiscardedOnSend?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic deltaBytesSent = 0;\n\tpublic deltaBytesReceived = 0;\n\tpublic deltaPacketsSent = 0;\n\tpublic deltaPacketsReceived = 0;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection,\n\t) {\n\t}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\t\n\t\tthis._visited = false;\n\t\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getIceTransport() {\n\t\treturn this._peerConnection.observedIceTransports.get(this.transportId ?? '');\n\t}\n\n\tpublic getLocalCandidate() {\n\t\treturn this._peerConnection.observedIceCandidates.get(this.localCandidateId ?? '');\n\t}\n\n\tpublic getRemoteCandidate() {\n\t\treturn this._peerConnection.observedIceCandidates.get(this.remoteCandidateId ?? '');\n\t}\n\n\tpublic update(stats: IceCandidatePairStats) {\n\t\tthis._visited = true;\n\n\t\tif (this.packetsSent && stats.packetsSent && stats.packetsSent >= this.packetsSent) {\n\t\t\tthis.deltaPacketsSent = stats.packetsSent - this.packetsSent;\n\t\t} else {\n\t\t\tthis.deltaPacketsSent = 0;\n\t\t}\n\t\tif (this.packetsReceived && stats.packetsReceived && stats.packetsReceived >= this.packetsReceived) {\n\t\t\tthis.deltaPacketsReceived = stats.packetsReceived - this.packetsReceived;\n\t\t} else {\n\t\t\tthis.deltaPacketsReceived = 0;\n\t\t}\n\t\tif (this.bytesSent && stats.bytesSent && stats.bytesSent >= this.bytesSent) {\n\t\t\tthis.deltaBytesSent = stats.bytesSent - this.bytesSent;\n\t\t} else {\n\t\t\tthis.deltaBytesSent = 0;\n\t\t}\n\t\tif (this.bytesReceived && stats.bytesReceived && stats.bytesReceived >= this.bytesReceived) {\n\t\t\tthis.deltaBytesReceived = stats.bytesReceived - this.bytesReceived;\n\t\t} else {\n\t\t\tthis.deltaBytesReceived = 0;\n\t\t}\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.localCandidateId = stats.localCandidateId;\n\t\tthis.remoteCandidateId = stats.remoteCandidateId;\n\t\tthis.state = this._convertState(stats.state);\n\t\tthis.nominated = stats.nominated;\n\t\tthis.packetsSent = stats.packetsSent;\n\t\tthis.packetsReceived = stats.packetsReceived;\n\t\tthis.bytesSent = stats.bytesSent;\n\t\tthis.bytesReceived = stats.bytesReceived;\n\t\tthis.lastPacketSentTimestamp = stats.lastPacketSentTimestamp;\n\t\tthis.lastPacketReceivedTimestamp = stats.lastPacketReceivedTimestamp;\n\t\tthis.totalRoundTripTime = stats.totalRoundTripTime;\n\t\tthis.currentRoundTripTime = stats.currentRoundTripTime;\n\t\tthis.availableOutgoingBitrate = stats.availableOutgoingBitrate;\n\t\tthis.availableIncomingBitrate = stats.availableIncomingBitrate;\n\t\tthis.requestsReceived = stats.requestsReceived;\n\t\tthis.requestsSent = stats.requestsSent;\n\t\tthis.responsesReceived = stats.responsesReceived;\n\t\tthis.responsesSent = stats.responsesSent;\n\t\tthis.consentRequestsSent = stats.consentRequestsSent;\n\t\tthis.packetsDiscardedOnSend = stats.packetsDiscardedOnSend;\n\t\tthis.bytesDiscardedOnSend = stats.bytesDiscardedOnSend;\n\t\tthis.attachments = stats.attachments;\n\t}\n\n\tprivate _convertState(state: string | undefined) {\n\t\tswitch (state) {\n\t\t\tcase 'new':\n\t\t\tcase 'in-progress':\n\t\t\tcase 'waiting':\n\t\t\tcase 'failed':\n\t\t\tcase 'succeeded':\n\t\t\t\treturn state;\n\t\t\tcase 'cancelled':\n\t\t\t\treturn 'failed';\n\t\t\tcase 'inprogress':\n\t\t\t\treturn 'in-progress';\n\t\t\tdefault:\n\t\t\t\treturn undefined;\n\t\t}\n\t}\n}","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { IceTransportStats } from './schema/ClientSample';\n\nexport class ObservedIceTransport implements IceTransportStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\tpacketsSent?: number | undefined;\n\tpacketsReceived?: number | undefined;\n\tbytesSent?: number | undefined;\n\tbytesReceived?: number | undefined;\n\ticeRole?: string | undefined;\n\ticeLocalUsernameFragment?: string | undefined;\n\tdtlsState?: string | undefined;\n\ticeState?: string | undefined;\n\tselectedCandidatePairId?: string | undefined;\n\tlocalCertificateId?: string | undefined;\n\tremoteCertificateId?: string | undefined;\n\ttlsVersion?: string | undefined;\n\tdtlsCipher?: string | undefined;\n\tdtlsRole?: string | undefined;\n\tsrtpCipher?: string | undefined;\n\tselectedCandidatePairChanges?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic deltaPacketsReceived = 0;\n\tpublic deltaPacketsSent = 0;\n\tpublic deltaBytesReceived = 0;\n\tpublic deltaBytesSent = 0;\n\n\t/**\n\t * `true` when the last `update()` changed `selectedCandidatePairId` (including the first\n\t * selection, and a selection being dropped). Per tick: reset by every `update()`.\n\t */\n\tpublic selectedCandidatePairChanged = false;\n\n\t/** The `selectedCandidatePairId` before the last change, if there was one. */\n\tpublic previousSelectedCandidatePairId?: string;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getSelectedCandidatePair() {\n\t\treturn this._peerConnection.observedIceCandidatesPair.get(this.selectedCandidatePairId ?? '');\n\t}\n\n\tpublic update(stats: IceTransportStats) {\n\t\tthis._visited = true;\n\n\t\tif (this.bytesReceived !== undefined && stats.bytesReceived !== undefined && this.bytesReceived <= stats.bytesReceived) {\n\t\t\tthis.deltaBytesReceived = stats.bytesReceived - this.bytesReceived;\n\t\t} else {\n\t\t\tthis.deltaBytesReceived = 0;\n\t\t}\n\t\t\n\t\tif (this.bytesSent !== undefined && stats.bytesSent !== undefined && this.bytesSent <= stats.bytesSent) {\n\t\t\tthis.deltaBytesSent = stats.bytesSent - this.bytesSent;\n\t\t} else {\n\t\t\tthis.deltaBytesSent = 0;\n\t\t}\n\n\t\tif (this.packetsReceived !== undefined && stats.packetsReceived !== undefined && this.packetsReceived <= stats.packetsReceived) {\n\t\t\tthis.deltaPacketsReceived = stats.packetsReceived - this.packetsReceived;\n\t\t} else {\n\t\t\tthis.deltaPacketsReceived = 0;\n\t\t}\n\t\t\n\t\tif (this.packetsSent !== undefined && stats.packetsSent !== undefined && this.packetsSent <= stats.packetsSent) {\n\t\t\tthis.deltaPacketsSent = stats.packetsSent - this.packetsSent;\n\t\t} else {\n\t\t\tthis.deltaPacketsSent = 0;\n\t\t}\n\n\t\tthis.selectedCandidatePairChanged = this.selectedCandidatePairId !== stats.selectedCandidatePairId;\n\t\tif (this.selectedCandidatePairChanged) {\n\t\t\tthis.previousSelectedCandidatePairId = this.selectedCandidatePairId;\n\t\t}\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.packetsSent = stats.packetsSent;\n\t\tthis.packetsReceived = stats.packetsReceived;\n\t\tthis.bytesSent = stats.bytesSent;\n\t\tthis.bytesReceived = stats.bytesReceived;\n\t\tthis.iceRole = stats.iceRole;\n\t\tthis.iceLocalUsernameFragment = stats.iceLocalUsernameFragment;\n\t\tthis.dtlsState = stats.dtlsState;\n\t\tthis.iceState = stats.iceState;\n\t\tthis.selectedCandidatePairId = stats.selectedCandidatePairId;\n\t\tthis.localCertificateId = stats.localCertificateId;\n\t\tthis.remoteCertificateId = stats.remoteCertificateId;\n\t\tthis.tlsVersion = stats.tlsVersion;\n\t\tthis.dtlsCipher = stats.dtlsCipher;\n\t\tthis.dtlsRole = stats.dtlsRole;\n\t\tthis.srtpCipher = stats.srtpCipher;\n\t\tthis.selectedCandidatePairChanges = stats.selectedCandidatePairChanges;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { MediaKind } from './common/types';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { MediaSourceStats } from './schema/ClientSample';\n\nexport class ObservedMediaSource implements MediaSourceStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\ttrackIdentifier?: string | undefined;\n\taudioLevel?: number | undefined;\n\ttotalAudioEnergy?: number | undefined;\n\ttotalSamplesDuration?: number | undefined;\n\techoReturnLoss?: number | undefined;\n\techoReturnLossEnhancement?: number | undefined;\n\twidth?: number | undefined;\n\theight?: number | undefined;\n\tframes?: number | undefined;\n\tframesPerSecond?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getTrack() {\n\t\treturn this._peerConnection.observedOutboundTracks.get(this.trackIdentifier ?? '');\n\t}\n\n\tpublic update(stats: MediaSourceStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.trackIdentifier = stats.trackIdentifier;\n\t\tthis.audioLevel = stats.audioLevel;\n\t\tthis.totalAudioEnergy = stats.totalAudioEnergy;\n\t\tthis.totalSamplesDuration = stats.totalSamplesDuration;\n\t\tthis.echoReturnLoss = stats.echoReturnLoss;\n\t\tthis.echoReturnLossEnhancement = stats.echoReturnLossEnhancement;\n\t\tthis.width = stats.width;\n\t\tthis.height = stats.height;\n\t\tthis.frames = stats.frames;\n\t\tthis.framesPerSecond = stats.framesPerSecond;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { PeerConnectionTransportStats } from './schema/ClientSample';\n\nexport class ObservedPeerConnectionTransport implements PeerConnectionTransportStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\tdataChannelsOpened?: number | undefined;\n\tdataChannelsClosed?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic update(stats: PeerConnectionTransportStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.dataChannelsOpened = stats.dataChannelsOpened;\n\t\tthis.dataChannelsClosed = stats.dataChannelsClosed;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { MediaKind } from './common/types';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { MediaPlayoutStats } from './schema/ClientSample';\n\nexport class ObservedMediaPlayout implements MediaPlayoutStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\tsynthesizedSamplesDuration?: number | undefined;\n\tsynthesizedSamplesEvents?: number | undefined;\n\ttotalSamplesDuration?: number | undefined;\n\ttotalPlayoutDelay?: number | undefined;\n\ttotalSamplesCount?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic update(stats: MediaPlayoutStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.synthesizedSamplesDuration = stats.synthesizedSamplesDuration;\n\t\tthis.synthesizedSamplesEvents = stats.synthesizedSamplesEvents;\n\t\tthis.totalSamplesDuration = stats.totalSamplesDuration;\n\t\tthis.totalPlayoutDelay = stats.totalPlayoutDelay;\n\t\tthis.totalSamplesCount = stats.totalSamplesCount;\n\t\tthis.attachments = stats.attachments;\n\t\t\n\t}\n}\n","import { MediaKind } from './common/types';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { RemoteInboundRtpStats } from './schema/ClientSample';\n\nexport class ObservedRemoteInboundRtp implements RemoteInboundRtpStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\ttransportId?: string | undefined;\n\tcodecId?: string | undefined;\n\tpacketsReceived?: number | undefined;\n\tpacketsLost?: number | undefined;\n\tjitter?: number | undefined;\n\tlocalId?: string | undefined;\n\troundTripTime?: number | undefined;\n\ttotalRoundTripTime?: number | undefined;\n\tfractionLost?: number | undefined;\n\troundTripTimeMeasurements?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic ssrc: number,\n\t\tpublic kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getOutboundRtp() {\n\t\treturn this._peerConnection.observedOutboundRtps.get(this.ssrc);\n\t}\n\n\tpublic getCodec() {\n\t\treturn this._peerConnection.observedCodecs.get(this.codecId ?? '');\n\t}\n\n\tpublic update(stats: RemoteInboundRtpStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.codecId = stats.codecId;\n\t\tthis.packetsReceived = stats.packetsReceived;\n\t\tthis.packetsLost = stats.packetsLost;\n\t\tthis.jitter = stats.jitter;\n\t\tthis.localId = stats.localId;\n\t\tthis.roundTripTime = stats.roundTripTime;\n\t\tthis.totalRoundTripTime = stats.totalRoundTripTime;\n\t\tthis.fractionLost = stats.fractionLost;\n\t\tthis.roundTripTimeMeasurements = stats.roundTripTimeMeasurements;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { MediaKind } from './common/types';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { RemoteOutboundRtpStats } from './schema/ClientSample';\n\nexport class ObservedRemoteOutboundRtp implements RemoteOutboundRtpStats {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\ttransportId?: string | undefined;\n\tcodecId?: string | undefined;\n\tpacketsSent?: number | undefined;\n\tbytesSent?: number | undefined;\n\tlocalId?: string | undefined;\n\tremoteTimestamp?: number | undefined;\n\treportsSent?: number | undefined;\n\troundTripTime?: number | undefined;\n\ttotalRoundTripTime?: number | undefined;\n\troundTripTimeMeasurements?: number | undefined;\n\tattachments?: Record<string, unknown> | undefined;\n\n\tpublic constructor(\n\t\tpublic timestamp: number,\n\t\tpublic id: string,\n\t\tpublic ssrc: number,\n\t\tpublic kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection\n\t) {}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getInboundRtp() {\n\t\treturn this._peerConnection.observedInboundRtps.get(this.ssrc);\n\t}\n\n\tpublic getCodec() {\n\t\treturn this._peerConnection.observedCodecs.get(this.codecId ?? '');\n\t}\n\n\tpublic update(stats: RemoteOutboundRtpStats) {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.transportId = stats.transportId;\n\t\tthis.codecId = stats.codecId;\n\t\tthis.packetsSent = stats.packetsSent;\n\t\tthis.bytesSent = stats.bytesSent;\n\t\tthis.localId = stats.localId;\n\t\tthis.remoteTimestamp = stats.remoteTimestamp;\n\t\tthis.reportsSent = stats.reportsSent;\n\t\tthis.roundTripTime = stats.roundTripTime;\n\t\tthis.totalRoundTripTime = stats.totalRoundTripTime;\n\t\tthis.roundTripTimeMeasurements = stats.roundTripTimeMeasurements;\n\t\tthis.attachments = stats.attachments;\n\t}\n}\n","import { CalculatedScore } from './scores/CalculatedScore';\nimport { MediaKind } from './common/types';\nimport { InboundTrackSample } from './schema/ClientSample';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { ObservedInboundRtp } from './ObservedInboundRtp';\nimport { ObservedMediaPlayout } from './ObservedMediaPlayout';\nimport { ObservedOutboundTrack } from './ObservedOutboundTrack';\n\nexport class ObservedInboundTrack implements InboundTrackSample {\n\tpublic readonly calculatedScore: CalculatedScore = {\n\t\tweight: 1,\n\t\tvalue: undefined,\n\t};\n\tpublic appData?: Record<string, unknown>;\n\tpublic remoteOutboundTrack?: ObservedOutboundTrack | undefined;\n\n\tprivate _visited = false;\n\n\tpublic addedAt?: number | undefined;\n\tpublic removedAt?: number | undefined;\n\n\tpublic muted?: boolean;\n\tpublic attachments?: Record<string, unknown> | undefined;\n\n\tpublic degradationReasons: string[] = [];\n\n\tpublic get degraded() {\n\t\treturn this.degradationReasons.length > 0;\n\t}\n\n\tconstructor(\n\t\tpublic timestamp: number,\n\t\tpublic readonly id: string,\n\t\tpublic readonly kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection,\n\t\tprivate readonly _inboundRtp?: ObservedInboundRtp,\n\t\tprivate readonly _mediaPlayout?: ObservedMediaPlayout,\n\t) {\n\t\t// no-op\n\t}\n\n\tpublic get score() {\n\t\treturn this.calculatedScore.value;\n\t}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getInboundRtp() {\n\t\treturn this._inboundRtp;\n\t}\n\n\tpublic getMediaPlayout() {\n\t\treturn this._mediaPlayout;\n\t}\n\n\tpublic update(stats: InboundTrackSample): void {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.calculatedScore.value = stats.score;\n\t\tthis.attachments = stats.attachments;\n\n\t\tthis.checkDegradation();\n\t}\n\n\tprivate checkDegradation() {\n\t\tconst thresholds = this._peerConnection.client.call.observer.config.inboundTrackDegradationThresholds;\n\n\t\tthis.degradationReasons.length = 0;\n\t\tif (!thresholds) return;\n\n\t\tconst reasons: string[] = [];\n\n\t\tif (thresholds.deltaFreezeCount < (this._inboundRtp?.deltaFreezeCount ?? 0)) {\n\t\t\treasons.push('freezes');\n\t\t}\n\t\tif (thresholds.framesDroppedRatio < (this._inboundRtp?.framesDroppedRatio ?? 0)) {\n\t\t\treasons.push('frames-dropped');\n\t\t}\n\t\tif (thresholds.jitterBufferDelayInMs < (this._inboundRtp?.jitterBufferDelayInMs ?? 0)) {\n\t\t\treasons.push('jitter-buffer-delay');\n\t\t}\n\t\tif (thresholds.concealmentRatio < (this._inboundRtp?.concealmentRatio ?? 0)) {\n\t\t\treasons.push('concealment');\n\t\t}\n\t\tif (thresholds.rttInMs < (this._inboundRtp?.remoteRttInMs ?? 0)) {\n\t\t\treasons.push('rtt');\n\t\t}\n\n\t\tthis.degradationReasons = reasons;\n\t}\n}","export type DeferredPromise<T> = {\n\tpromise: Promise<T>;\n\tresolve: (value: T | PromiseLike<T>) => void;\n\treject: (reason?: any) => void;\n};\n\nexport function createDeferredPromise<T>(): DeferredPromise<T> {\n\tlet resolve: (value: T | PromiseLike<T>) => void;\n\tlet reject: (reason?: any) => void;\n\n\tconst promise = new Promise<T>((res, rej) => {\n\t\tresolve = res;\n\t\treject = rej;\n\t});\n\n\treturn { promise, resolve: resolve!, reject: reject! };\n}\n\n/** Collect finite numbers only; `undefined`/`NaN`/`Infinity` must not enter a percentile summary. */\nexport function pushFinite(target: number[], value: number | undefined): void {\n\tif (typeof value === 'number' && Number.isFinite(value)) target.push(value);\n}\n\n/**\n * Best-effort read of a wire payload. Returns `undefined` for empty input or malformed JSON rather\n * than throwing — every caller here is reading data that arrived over the wire, where a bad value is\n * data to ignore rather than an exception to propagate.\n *\n * Payloads arrive in two shapes. Schema 3.5.0 and later deliver them as records, ready to use;\n * samples from clients on earlier schema versions carry the same payload as a JSON string. An object\n * passes through untouched, a string is parsed — so every reader handles both wire generations\n * without knowing which one it is looking at.\n *\n * Since schema 3.7.0 a record's values are free-form JSON rather than primitives, so a payload may\n * nest objects and arrays to any depth. That widening costs the readers here nothing — they already\n * treat every value as `unknown` and narrow with a `typeof` guard before use — but it is the reason\n * a value must never be read bare: `payload.role` is `unknown`, not `string`.\n *\n * The single payload entry point; {@link parseJsonObject} adds the \"and it must be an object\"\n * guarantee on top, which is what every wire-payload reader actually wants.\n */\nexport function parseJsonAs<T>(value?: unknown): T | undefined {\n\tif (value === undefined || value === null || value === '') return undefined;\n\tif (typeof value === 'object') return value as T;\n\tif (typeof value !== 'string') return undefined;\n\n\ttry {\n\t\treturn JSON.parse(value) as T;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** `parseJsonAs`, narrowed to \"an object came back\" — the shape every payload reader wants. */\nexport function parseJsonObject(value?: unknown): Record<string, unknown> | undefined {\n\tconst parsed = parseJsonAs<unknown>(value);\n\n\treturn typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;\n}\n\n/**\n * Best-effort read of a wire `scoreReasons` field. Schema 3.6.0 carries it as a record of\n * reason key → the points that reason subtracted from the score; samples from clients on\n * earlier schema versions (3.5.x) carried the reason *keys only*, as a `string[]`. A legacy\n * array is folded into the record shape with a magnitude of `0` — the key still conveys the\n * reason, and `0` keeps any downstream summing of contributions honest about the magnitude\n * the old wire never carried. Record entries whose value is not a finite number are dropped.\n * An empty result collapses to `undefined`, matching the client's \"omitted when there is\n * nothing to explain\"; anything unreadable is data to ignore rather than an exception to\n * propagate.\n */\nexport function normalizeScoreReasons(value?: unknown): Record<string, number> | undefined {\n\tif (value === undefined || value === null) return undefined;\n\n\tconst result: Record<string, number> = {};\n\n\tif (Array.isArray(value)) {\n\t\tfor (const entry of value) {\n\t\t\tif (typeof entry !== 'string' || entry.length === 0) continue;\n\n\t\t\tresult[entry] = 0;\n\t\t}\n\t} else if (typeof value === 'object') {\n\t\tfor (const [ key, magnitude ] of Object.entries(value as Record<string, unknown>)) {\n\t\t\tif (typeof magnitude !== 'number' || !Number.isFinite(magnitude)) continue;\n\n\t\t\tresult[key] = magnitude;\n\t\t}\n\t} else {\n\t\treturn undefined;\n\t}\n\n\treturn Object.keys(result).length === 0 ? undefined : result;\n}\n\nfunction isPublicIpv4Address(address: string): boolean {\n\tconst parts = address.split('.');\n\n\tif (parts.length !== 4) return false;\n\n\tconst octets: number[] = [];\n\n\tfor (const part of parts) {\n\t\tif (!/^\\d{1,3}$/.test(part)) return false;\n\n\t\tconst octet = Number(part);\n\n\t\tif (255 < octet) return false;\n\t\toctets.push(octet);\n\t}\n\n\tconst [ a, b, c ] = octets;\n\n\tif (a === 0) return false; // \"this\" network (incl. 0.0.0.0, the browsers' redacted address)\n\tif (a === 10) return false; // RFC 1918\n\tif (a === 100 && 64 <= b && b <= 127) return false; // RFC 6598 carrier-grade NAT\n\tif (a === 127) return false; // loopback\n\tif (a === 169 && b === 254) return false; // link-local\n\tif (a === 172 && 16 <= b && b <= 31) return false; // RFC 1918\n\tif (a === 192 && b === 0 && c === 0) return false; // IETF protocol assignments\n\tif (a === 192 && b === 168) return false; // RFC 1918\n\tif (a === 198 && (b === 18 || b === 19)) return false; // benchmarking\n\tif (224 <= a) return false; // multicast + reserved + broadcast\n\n\treturn true;\n}\n\nfunction isPublicIpv6Address(address: string): boolean {\n\tconst lower = address.toLowerCase().replace(/^\\[|\\]$/g, '')\n\t\t.split('%')[0];\n\n\tif (!lower.includes(':')) return false;\n\n\tconst mappedV4 = /^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(lower);\n\n\tif (mappedV4) return isPublicIpv4Address(mappedV4[1]);\n\tif (lower === '::' || lower === '::1') return false; // unspecified / loopback\n\tif (/^fe[89ab]/.test(lower)) return false; // fe80::/10 link-local\n\tif (/^f[cd]/.test(lower)) return false; // fc00::/7 unique local\n\tif (lower.startsWith('ff')) return false; // multicast\n\tif (lower.startsWith('2001:db8')) return false; // documentation\n\n\treturn true;\n}\n\n/**\n * `true` when `address` is an IP literal that is reachable on the public internet — i.e. not\n * private (RFC 1918 / ULA), loopback, link-local, carrier-grade NAT, multicast, unspecified, an mDNS\n * `.local` hostname (how browsers obfuscate host candidates) or empty.\n */\nexport function isPublicIpAddress(address?: string | null): boolean {\n\tif (!address) return false;\n\n\tconst trimmed = address.trim();\n\n\tif (!trimmed || trimmed.endsWith('.local')) return false;\n\tif (trimmed.includes(':')) return isPublicIpv6Address(trimmed);\n\n\treturn isPublicIpv4Address(trimmed);\n}\n","import { CalculatedScore } from './scores/CalculatedScore';\nimport { MediaKind } from './common/types';\nimport { OutboundTrackSample } from './schema/ClientSample';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { ObservedOutboundRtp } from './ObservedOutboundRtp';\nimport { ObservedMediaSource } from './ObservedMediaSource';\nimport { ObservedInboundTrack } from './ObservedInboundTrack';\nimport { pushFinite } from './common/utils';\nimport { StatsSummary, summarize } from './utils/stats';\nimport { Detectors } from '.';\n\n/** The per-receiver view the aggregator builds for one subscribed track. */\nexport type PublishedTrackReceivingDistributionEntry = {\n\tnumberOfReceivers: number;\n\tnumberOfHealthyReceivers: number;\n\tnumberOfDegradedReceivers: number;\n\n\t/** degradedReceivers / receivers (0..1); `0` when there are no receivers. */\n\tdegradedRatio: number;\n\n\t/** Distribution summaries across receivers (undefined when no receiver reported the metric). */\n\tbitrate?: StatsSummary;\n\tfractionLost?: StatsSummary;\n\tjitter?: StatsSummary;\n\trttInMs?: StatsSummary;\n\tjitterBufferDelayInMs?: StatsSummary;\n\tconcealmentRatio?: StatsSummary;\n\n\t/** Fan-out counters: how many receivers saw the symptom, and the total across them. */\n\tfreezes: { affectedReceivers: number, total: number };\n\tplis: { affectedReceivers: number, total: number };\n\tconcealment: { affectedReceivers: number };\n};\n\nexport class ObservedOutboundTrack implements OutboundTrackSample {\n\tprivate _visited = false;\n\tpublic appData?: Record<string, unknown>;\n\n\tpublic readonly remoteInboundTracks = new Set<ObservedInboundTrack>();\n\n\tpublic readonly detectors = new Detectors();\n\n\t// only set if the track is being received by at least one subscriber, otherwise undefined\n\t// which means the remote track resolver has to be set up to know if the track is unconsumed or not\n\tpublic receivingDistribution?: PublishedTrackReceivingDistributionEntry;\n\n\tpublic readonly calculatedScore: CalculatedScore = {\n\t\tweight: 1,\n\t\tvalue: undefined,\n\t};\n\n\tpublic addedAt?: number | undefined;\n\tpublic removedAt?: number | undefined;\n\n\tpublic muted?: boolean;\n\tpublic attachments?: Record<string, unknown> | undefined;\n\n\tpublic degradedReasons?: string[] | undefined;\n\tpublic bitrate?: number | undefined;\n\tpublic deltaPacketsSent?: number | undefined;\n\tpublic remoteFractionLost?: number | undefined;\n\tpublic remoteRttInMs?: number | undefined;\n\tpublic qualityLimitationReason?: string | undefined;\n\n\tconstructor(\n\t\tpublic timestamp: number,\n\t\tpublic readonly id: string,\n\t\tpublic readonly kind: MediaKind,\n\t\tprivate readonly _peerConnection: ObservedPeerConnection,\n\t\tprivate readonly _outboundRtps?: ObservedOutboundRtp[],\n\t\tprivate readonly _mediaSource?: ObservedMediaSource,\n\t) {\n\t\t// no-op\n\t}\n\n\tpublic get score() {\n\t\treturn this.calculatedScore.value;\n\t}\n\n\tpublic get visited() {\n\t\tconst visited = this._visited;\n\n\t\tthis._visited = false;\n\n\t\treturn visited;\n\t}\n\n\tpublic get degraded() {\n\t\treturn (this.degradedReasons?.length ?? 0) > 0;\n\t}\n\n\tpublic getPeerConnection() {\n\t\treturn this._peerConnection;\n\t}\n\n\tpublic getOutboundRtps() {\n\t\treturn this._outboundRtps;\n\t}\n\n\tpublic getMediaSource() {\n\t\treturn this._mediaSource;\n\t}\n\n\tpublic update(stats: OutboundTrackSample): void {\n\t\tthis._visited = true;\n\n\t\tthis.timestamp = stats.timestamp;\n\t\tthis.calculatedScore.value = stats.score;\n\t\tthis.attachments = stats.attachments;\n\n\t\t// update comes after outbound rtps are updated, so we can accumulate stuffs\n\t\tthis.bitrate = undefined;\n\t\tthis.deltaPacketsSent = undefined;\n\t\tthis.remoteFractionLost = undefined;\n\t\tthis.remoteRttInMs = undefined;\n\t\tthis.qualityLimitationReason = undefined;\n\n\t\tfor (const rtp of this._outboundRtps ?? []) {\n\t\t\tthis.bitrate = (this.bitrate ?? 0) + rtp.bitrate;\n\t\t\tthis.deltaPacketsSent = (this.deltaPacketsSent ?? 0) + rtp.deltaPacketsSent;\n\n\t\t\tif (rtp.remoteFractionLost !== undefined) {\n\t\t\t\tthis.remoteFractionLost = Math.max(this.remoteFractionLost ?? 0, rtp.remoteFractionLost);\n\t\t\t}\n\t\t\tif (rtp.remoteRttInMs !== undefined) {\n\t\t\t\tthis.remoteRttInMs = Math.max(this.remoteRttInMs ?? 0, rtp.remoteRttInMs);\n\t\t\t}\n\t\t\tif (rtp.qualityLimitationReason && rtp.qualityLimitationReason !== 'none') {\n\t\t\t\tthis.qualityLimitationReason = rtp.qualityLimitationReason;\n\t\t\t}\n\t\t}\n\n\t\tconst thresholds = this._peerConnection.client.call.observer.config.outboundTrackDegradationThresholds;\n\t\tconst reasons: string[] = [];\n\n\t\tif (thresholds && thresholds.fractionLost < (this.remoteFractionLost ?? 0)) reasons.push('remote-fraction-lost');\n\t\tif (thresholds && thresholds.rttInMs < (this.remoteRttInMs ?? 0)) reasons.push('remote-rtt');\n\t\tif (this.qualityLimitationReason) reasons.push(`quality-limited-${this.qualityLimitationReason}`);\n\t\tif (this.muted !== true && this.deltaPacketsSent === 0) reasons.push('no-packets-sent');\n\n\t\tthis.degradedReasons = reasons.length === 0 ? undefined : reasons;\n\n\t\tthis.receivingDistribution = this.createReceivingDistribution();\n\n\t\tthis.detectors.update();\n\t}\n\n\tprivate createReceivingDistribution(): PublishedTrackReceivingDistributionEntry | undefined {\n\t\tif (this.remoteInboundTracks.size === 0) return undefined;\n\n\t\t// One pass, filling the sample arrays the summaries need plus the fan-out counters. This used\n\t\t// to be a dozen separate `map`/`filter`/`reduce` traversals, each allocating an intermediate\n\t\t// array per metric per track per tick.\n\n\t\tlet freezeAffected = 0;\n\t\tlet freezeTotal = 0;\n\t\tlet pliAffected = 0;\n\t\tlet pliTotal = 0;\n\t\tlet concealmentAffected = 0;\n\t\tlet numberOfReceivers = 0;\n\t\tlet numberOfDegradedReceivers = 0;\n\t\tlet numberOfHealthyReceivers = 0;\n\t\tconst bitrates: number[] = [];\n\t\tconst fractionLosts: number[] = [];\n\t\tconst jitters: number[] = [];\n\t\tconst rtts: number[] = [];\n\t\tconst jitterBufferDelays: number[] = [];\n\t\tconst concealmentRatios: number[] = [];\n\n\t\tfor (const inboundTrack of this.remoteInboundTracks) {\n\t\t\tconst inboundRtp = inboundTrack.getInboundRtp();\n\t\t\tconst peerConnection = inboundTrack.getPeerConnection();\n\n\t\t\tif (!inboundRtp) continue;\n\n\t\t\tif (0 < inboundRtp.deltaFreezeCount) freezeAffected += 1;\n\t\t\tif (0 < inboundRtp.deltaPliCount) pliAffected += 1;\n\t\t\tif (0 < (inboundRtp.concealmentRatio ?? 0)) concealmentAffected += 1;\n\n\t\t\tif (inboundTrack.degraded) numberOfDegradedReceivers += 1;\n\t\t\telse numberOfHealthyReceivers += 1;\n\n\t\t\tpliTotal += inboundRtp.deltaPliCount;\n\t\t\tfreezeTotal += inboundRtp.deltaFreezeCount;\n\n\t\t\t++numberOfReceivers;\n\n\t\t\tpushFinite(bitrates, inboundRtp.bitrate);\n\t\t\tpushFinite(fractionLosts, inboundRtp.fractionLost);\n\t\t\tpushFinite(jitters, inboundRtp.jitter);\n\t\t\tpushFinite(rtts, peerConnection?.currentRttInMs);\n\t\t\tpushFinite(jitterBufferDelays, inboundRtp.jitterBufferDelayInMs);\n\t\t\tpushFinite(concealmentRatios, inboundRtp.concealmentRatio);\n\t\t}\n\n\t\treturn {\n\t\t\tnumberOfReceivers,\n\t\t\tnumberOfHealthyReceivers,\n\t\t\tnumberOfDegradedReceivers,\n\t\t\tdegradedRatio: numberOfReceivers === 0 ? 0 : numberOfDegradedReceivers / numberOfReceivers,\n\n\t\t\tbitrate: summarize(bitrates),\n\t\t\tfractionLost: summarize(fractionLosts),\n\t\t\tjitter: summarize(jitters),\n\t\t\trttInMs: summarize(rtts),\n\t\t\tjitterBufferDelayInMs: summarize(jitterBufferDelays),\n\t\t\tconcealmentRatio: summarize(concealmentRatios),\n\n\t\t\tfreezes: { affectedReceivers: freezeAffected, total: freezeTotal },\n\t\t\tplis: { affectedReceivers: pliAffected, total: pliTotal },\n\t\t\tconcealment: { affectedReceivers: concealmentAffected },\n\t\t};\n\t}\n}","/* eslint-disable no-shadow */\n\n/* ================================================================================================\n * W3C types, defined here rather than taken from the DOM lib\n *\n * These mirror types from the WebRTC and Media Capture specs. They used to come from TypeScript's\n * `dom` lib, which meant this **Node-only** library compiled with every browser global in scope:\n * `window`, `document`, `fetch`, `localStorage`, and — the one that actually bites — a `setTimeout`\n * typed to return `number` instead of Node's `Timeout`. Code referencing something that does not\n * exist at runtime would type-check happily and fail in production.\n *\n * The types themselves are still wanted: these payloads are what a browser client reports, so they\n * should read in the browser's vocabulary. So they are declared here, and `dom` is out of\n * `tsconfig.json`.\n *\n * The enumerations below are verbatim from the spec and stable. The three `MediaTrack*` shapes are\n * deliberately looser than the DOM originals: this library only ever *reads* them, parsed from JSON\n * that some client serialised, and a faithful transcription of `ConstrainULong` &co. would be a lot\n * of surface area to keep in sync for no gain on the reading side.\n * ============================================================================================== */\n\n/** Whether the candidate is for RTP or RTCP. */\nexport type RTCIceComponent = 'rtcp' | 'rtp';\n\n/** The transport protocol of the candidate. */\nexport type RTCIceProtocol = 'tcp' | 'udp';\n\n/** The TCP candidate type, when `protocol` is `'tcp'`. */\nexport type RTCIceTcpCandidateType = 'active' | 'passive' | 'so';\n\n/** How the candidate was obtained: host, peer/server reflexive, or relayed. */\nexport type RTCIceCandidateType = 'host' | 'prflx' | 'relay' | 'srflx';\n\n/**\n * A track's applied settings, as reported by `MediaStreamTrack.getSettings()`.\n *\n * The index signature is deliberate: browsers ship settings this list does not know about, and a\n * closed type would silently drop them on the way through.\n */\nexport type MediaTrackSettings = {\n\tdeviceId?: string;\n\tgroupId?: string;\n\twidth?: number;\n\theight?: number;\n\taspectRatio?: number;\n\tframeRate?: number;\n\tfacingMode?: string;\n\tresizeMode?: string;\n\tsampleRate?: number;\n\tsampleSize?: number;\n\tchannelCount?: number;\n\tlatency?: number;\n\tautoGainControl?: boolean;\n\techoCancellation?: boolean;\n\tnoiseSuppression?: boolean;\n\tdisplaySurface?: string;\n\t[key: string]: unknown;\n};\n\n/** What a track's source is capable of, as reported by `MediaStreamTrack.getCapabilities()`. */\nexport type MediaTrackCapabilities = Record<string, unknown>;\n\n/** The constraints a track was created with, as reported by `MediaStreamTrack.getConstraints()`. */\nexport type MediaTrackConstraints = Record<string, unknown>;\n\nexport enum ClientEventTypes {\n\tCLIENT_JOINED = 'CLIENT_JOINED',\n\tCLIENT_LEFT = 'CLIENT_LEFT',\n\tPEER_CONNECTION_OPENED = 'PEER_CONNECTION_OPENED',\n\tPEER_CONNECTION_CLOSED = 'PEER_CONNECTION_CLOSED',\n\tMEDIA_TRACK_ADDED = 'MEDIA_TRACK_ADDED',\n\tMEDIA_TRACK_REMOVED = 'MEDIA_TRACK_REMOVED',\n\tMEDIA_TRACK_RESUMED = 'MEDIA_TRACK_RESUMED',\n\tMEDIA_TRACK_MUTED = 'MEDIA_TRACK_MUTED',\n\tMEDIA_TRACK_UNMUTED = 'MEDIA_TRACK_UNMUTED',\n\tICE_GATHERING_STATE_CHANGED = 'ICE_GATHERING_STATE_CHANGED',\n\tPEER_CONNECTION_STATE_CHANGED = 'PEER_CONNECTION_STATE_CHANGED',\n\tICE_CONNECTION_STATE_CHANGED = 'ICE_CONNECTION_STATE_CHANGED',\n\tDATA_CHANNEL_OPEN = 'DATA_CHANNEL_OPEN',\n\tDATA_CHANNEL_CLOSED = 'DATA_CHANNEL_CLOSED',\n\tDATA_CHANNEL_ERROR = 'DATA_CHANNEL_ERROR',\n\tNEGOTIATION_NEEDED = 'NEGOTIATION_NEEDED',\n\tSIGNALING_STATE_CHANGE = 'SIGNALING_STATE_CHANGE',\n\t// ICE_GATHERING_STATE_CHANGE = 'ICE_GATHERING_STATE_CHANGE',\n\t// ICE_CONNECTION_STATE_CHANGE = 'ICE_CONNECTION_STATE_CHANGE',\n\tICE_CANDIDATE = 'ICE_CANDIDATE',\n\tICE_CANDIDATE_ERROR = 'ICE_CANDIDATE_ERROR',\n\t\n\t// mediasoup events\n\tPRODUCER_ADDED = 'PRODUCER_ADDED',\n\tPRODUCER_REMOVED = 'PRODUCER_REMOVED',\n\tPRODUCER_PAUSED = 'PRODUCER_PAUSED',\n\tPRODUCER_RESUMED = 'PRODUCER_RESUMED',\n\tCONSUMER_ADDED = 'CONSUMER_ADDED',\n\tCONSUMER_REMOVED = 'CONSUMER_REMOVED',\n\tCONSUMER_PAUSED = 'CONSUMER_PAUSED',\n\tCONSUMER_RESUMED = 'CONSUMER_RESUMED',\n\tDATA_PRODUCER_CREATED = 'DATA_PRODUCER_CREATED',\n\tDATA_PRODUCER_CLOSED = 'DATA_PRODUCER_CLOSED',\n\tDATA_CONSUMER_CREATED = 'DATA_CONSUMER_CREATED',\n\tDATA_CONSUMER_CLOSED = 'DATA_CONSUMER_CLOSED',\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface ClientJoinedEventPayload extends Record<string, unknown> {\n\t// empty\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface ClientLeftEventPayload extends Record<string, unknown> {\n}\n\nexport interface PeerConnectionOpenedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ticeConnectionState?: string;\n\ticeGatheringState?: string;\n\tsignalingState?: string;\n}\n\nexport interface PeerConnectionClosedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ticeConnectionState?: string;\n\ticeGatheringState?: string;\n\tsignalingState?: string;\n}\n\nexport interface MediaTrackAddedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ttrackId: string;\n\tkind: 'audio' | 'video';\n\tlabel?: string;\n\tmuted: boolean;\n\tenabled: boolean;\n\treadyState: string;\n\tcontentHint?: string;\n\tconstraints: MediaTrackConstraints,\n\tcapabilities: MediaTrackCapabilities,\n\tsettings: MediaTrackSettings,\n}\n\nexport interface MediaTrackRemovedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ttrackId: string;\n\tkind: 'audio' | 'video';\n\tlabel?: string;\n\tmuted: boolean;\n\tenabled: boolean;\n\treadyState: string;\n\tcontentHint?: string;\n}\n\nexport interface MediaTrackMutedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ttrackId: string;\n\tkind: 'audio' | 'video';\n\tlabel?: string;\n\tmuted: boolean;\n\tenabled: boolean;\n\treadyState: string;\n\tcontentHint?: string;\n}\n\nexport interface MediaTrackUnmutedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ttrackId: string;\n\tkind: 'audio' | 'video';\n\tlabel?: string;\n\tmuted: boolean;\n\tenabled: boolean;\n\treadyState: string;\n\tcontentHint?: string;\n}\n\nexport interface IceGatheringStateChangedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ticeGatheringState: string;\n}\n\nexport interface PeerConnectionStateChangedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tconnectionState: string;\n}\n\nexport interface IceConnectionStateChangedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\ticeConnectionState: string;\n}\n\nexport interface DataChannelErrorEventPayload extends Record<string, unknown> {\n\tlabel: string;\n\tpeerConnectionId: string;\n\treadyState: string;\n\tdataChannelId: string | number | null,\n\terror: string | null,\n}\n\nexport interface DataChannelOpenEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tlabel: string;\n\treadyState: string;\n\tdataChannelId: string | number | null,\n}\n\nexport interface DataChannelClosedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tlabel: string;\n\treadyState: string;\n\tdataChannelId: string | number | null,\n}\n\nexport interface NegotiationNeededEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n}\n\nexport interface IceCandidateEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/address) */\n\taddress?: string | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/candidate) */\n\tcandidate?: string;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/component) */\n\tcomponent?: RTCIceComponent | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/foundation) */\n\tfoundation?: string | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/port) */\n\tport?: number | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/priority) */\n\tpriority?: number | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/protocol) */\n\tprotocol?: RTCIceProtocol | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/relatedAddress) */\n\trelatedAddress?: string | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/relatedPort) */\n\trelatedPort?: number | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/sdpMLineIndex) */\n\tsdpMLineIndex?: number | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/sdpMid) */\n\tsdpMid?: string | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/tcpType) */\n\ttcpType?: RTCIceTcpCandidateType | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/type) */\n\ttype?: RTCIceCandidateType | null;\n\n\t/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/RTCIceCandidate/usernameFragment) */\n\tusernameFragment?: string | null;\n}\n\nexport interface IceCandidateErrorEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\terrorCode?: number;\n\terrorText?: string;\n\taddress?: string | null;\n\tport?: number | null;\n\turl?: string | null;\n}\n\nexport interface SignalingStateChangedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tsignalingState: string;\n}\n\nexport interface ProducerAddedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n}\n\nexport interface ProducerRemovedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n}\n\nexport interface ProducerPausedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n}\n\nexport interface ProducerResumedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n}\n\nexport interface ConsumerAddedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n\tconsumerId: string;\n\ttrackId: string;\n}\n\nexport interface ConsumerRemovedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n\tconsumerId: string;\n\ttrackId: string;\n}\n\nexport interface ConsumerPausedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n\tconsumerId: string;\n\ttrackId: string;\n}\n\nexport interface ConsumerResumedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tproducerId: string;\n\tconsumerId: string;\n\ttrackId: string;\n}\n\nexport interface DataProducerCreatedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tdataProducerId: string;\n}\n\nexport interface DataProducerClosedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tdataProducerId: string;\n}\n\nexport interface DataConsumerCreatedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tdataProducerId: string;\n\tdataConsumerId: string;\n}\n\nexport interface DataConsumerClosedEventPayload extends Record<string, unknown> {\n\tpeerConnectionId: string;\n\tdataProducerId: string;\n\tdataConsumerId: string;\n}","/* eslint-disable no-shadow */\n\nexport enum ClientMetaTypes {\n\tMEDIA_CONSTRAINT = 'MEDIA_CONSTRAINT',\n\tMEDIA_DEVICE = 'MEDIA_DEVICE',\n\tMEDIA_DEVICES_SUPPORTED_CONSTRAINTS = 'MEDIA_DEVICES_SUPPORTED_CONSTRAINTS',\n\tUSER_MEDIA_ERROR = 'USER_MEDIA_ERROR',\n\tLOCAL_SDP = 'LOCAL_SDP',\n\n\tOPERATION_SYSTEM = 'OPERATION_SYSTEM',\n\tENGINE = 'ENGINE',\n\tPLATFORM = 'PLATFORM',\n\tBROWSER = 'BROWSER',\n}\n\nexport type MediaDeviceInfo = {\n\tdeviceId: string;\n\tlabel: string;\n\tkind: string;\n\tgroupId: string;\n}\n\nexport type Browser = {\n\tname: string;\n\tversion: string;\n}\n\nexport type Platform = {\n\tname: string;\n\tversion: string;\n}\n\nexport type Engine = {\n\tname: string;\n\tversion: string;\n}\n\nexport type OperationSystem = {\n\tname: string;\n\tversion: string;\n}","import type { ClientIssue } from '../schema/ClientSample';\n\n/** The suffix client-monitor-js appends to the type of a resolution entry. */\nexport const RESOLVED_ISSUE_SUFFIX = '-resolved';\n\n/**\n * A **stateful** client issue the server currently believes to be open.\n *\n * client-monitor-js (>= 4.6.0, `sendResolvedIssuesToServer`) puts an issue's whole lifecycle on the\n * wire as two entries sharing a `key`:\n *\n * ```\n * raise: { type: 'stuck-decoder', key, payload, timestamp: raisedAt }\n * resolution: { type: 'stuck-decoder-resolved', key, payload: { raisedAt, comment, …final }, timestamp: resolvedAt }\n * ```\n *\n * The observer opens an `ActiveClientIssue` on the raise and closes it on the matching key, which\n * turns a stream of point-in-time symptom reports into **intervals**. That is what makes the\n * difference between \"several clients reported congestion in the last 10 seconds\" (a guess built on\n * an arbitrary window) and \"several clients are congested *right now, simultaneously*\" — the latter\n * being real evidence of a shared cause.\n *\n * Issues still active when the client monitor closes are auto-resolved by the client, so a clean\n * departure does not leak.\n */\nexport type ActiveClientIssue = {\n\n\t/** Identity shared by the raise and its resolution. Unique per client. */\n\tkey: string;\n\n\t/** The issue type **without** the `-resolved` suffix (e.g. `'congestion'`). */\n\ttype: string;\n\n\t/** The client that reported it. */\n\tclientId: string;\n\n\t/**\n\t * The call that client belongs to. The dimension that separates \"one bad meeting\" from \"our\n\t * infrastructure\": at observer scope, clients in *different* calls share nothing but the server.\n\t */\n\tcallId: string;\n\n\t/** When the client raised it (client clock). */\n\traisedAt: number;\n\n\t/** When the observer first saw it (server clock) — skew-free, use this for cross-client timing. */\n\tobservedAt: number;\n\n\t/** Parsed raise payload, when it was JSON. */\n\tpayload?: Record<string, unknown>;\n\n\t/** `payload.peerConnectionId`, when present — most client detectors report it. */\n\tpeerConnectionId?: string;\n\n\t/** `payload.trackId`, when present — the join key to a track (and thus to a publisher). */\n\ttrackId?: string;\n};\n\n/** A closed interval: an {@link ActiveClientIssue} plus how it ended. */\nexport type ResolvedActiveClientIssue = ActiveClientIssue & {\n\n\t/** When the client resolved it (client clock). */\n\tresolvedAt: number;\n\n\t/** Observer-clock resolution time. */\n\tobservedResolvedAt: number;\n\n\t/** `resolvedAt - raisedAt` as reported by the client, else derived from observer clocks. */\n\tdurationInMs: number;\n\n\t/** Free-form note passed to `resolveIssue`. */\n\tcomment?: string;\n\n\t/** Payload explicitly passed at resolution (the built-in detectors pass their final payload). */\n\tresolutionPayload?: Record<string, unknown>;\n\n\t/**\n\t * How the interval ended: the client said so, the observer expired it, or the client left\n\t * without resolving.\n\t */\n\tresolvedBy: 'client' | 'timeout' | 'client-closed';\n};\n\n/** `true` when the entry is a resolution companion rather than a raise. */\nexport function isClientIssueResolutionEntry(issue: ClientIssue): boolean {\n\treturn issue.type.endsWith(RESOLVED_ISSUE_SUFFIX);\n}\n\n/** Strip the `-resolved` suffix, so both entries of a lifecycle share one logical type. */\nexport function baseIssueType(type: string): string {\n\treturn type.endsWith(RESOLVED_ISSUE_SUFFIX) ? type.slice(0, -RESOLVED_ISSUE_SUFFIX.length) : type;\n}\n","import { ObservedCall } from '../ObservedCall';\n\nexport class DefaultCallScoreCalculator {\n\n\tpublic constructor(\n\t\tpublic readonly observedCall: ObservedCall,\n\t) {\n\t}\n\n\tpublic update() {\n\t\tlet totalScore = 0;\n\t\tlet totalWeight = 0;\n\n\t\tfor (const client of this.observedCall.observedClients.values()) {\n\t\t\tif (client.score === undefined) continue;\n\n\t\t\ttotalScore += client.score * client.calculatedScore.weight;\n\t\t\ttotalWeight += client.calculatedScore.weight;\n\t\t}\n\n\t\tthis.observedCall.calculatedScore.value = totalWeight ? totalScore / totalWeight : undefined;\n\t}\n}","import { createLogger } from '../common/logger';\nimport { Detector } from './Detector';\nimport { CallConcurrentIssueDetector, CallConcurrentIssueDetectorConfig } from './CallConcurrentIssueDetector';\nimport { ClientPopulationIssueDetector, ClientPopulationIssueDetectorConfig } from './ClientPopulationIssueDetector';\nimport { PublisherFaultCorroborationDetector, PublisherFaultCorroborationDetectorConfig } from './PublisherFaultCorroborationDetector';\nimport { ObserverConcurrentIssueDetector, ObserverConcurrentIssueDetectorConfig } from './ObserverConcurrentIssueDetector';\nimport { IssueFanOutDetector, IssueFanOutDetectorConfig } from './IssueFanOutDetector';\nimport { SfuCongestionDetector, SfuCongestionDetectorConfig } from './SfuCongestionDetector';\nimport { TrackDeliveryMismatchDetector, TrackDeliveryMismatchDetectorConfig } from './TrackDeliveryMismatchDetector';\nimport { TurnServerHealthDetector, TurnServerHealthDetectorConfig } from './TurnServerHealthDetector';\nimport { TurnServerOutageDetector, TurnServerOutageDetectorConfig } from './TurnServerOutageDetector';\nimport { UnconsumedTrackDetector, UnconsumedTrackDetectorConfig } from './UnconsumedTrackDetector';\n\nconst logger = createLogger('Detectors');\n\n/**\n * Detectors that reason **across calls**, created once on the observer.\n *\n * Adding one means: give the class a `static readonly NAME`, add its entry here, and add a `case` to\n * `Observer.addObserverDetector`. This map is what types the call site — the config is checked\n * against the right detector and an unknown name won't compile.\n */\nexport type AvailableObserverScopeDetectorsConfigs = {\n\t[SfuCongestionDetector.NAME]: SfuCongestionDetectorConfig;\n\t[ObserverConcurrentIssueDetector.NAME]: ObserverConcurrentIssueDetectorConfig;\n\t[ClientPopulationIssueDetector.NAME]: ClientPopulationIssueDetectorConfig;\n\t[TurnServerHealthDetector.NAME]: TurnServerHealthDetectorConfig;\n\t[TurnServerOutageDetector.NAME]: TurnServerOutageDetectorConfig;\n};\n\n/**\n * Detectors that reason **within one call**, created for every call the observer opens.\n *\n * Note there is no detector in both maps. \"Is this meeting in trouble?\" and \"is our infrastructure in\n * trouble?\" are different questions with different gates and different findings, so they are separate\n * classes — `CallConcurrentIssueDetector` and `ObserverConcurrentIssueDetector` — rather than one\n * class branching on what it was handed.\n */\nexport type AvailableCallScopeDetectorsConfigs = {\n\t[UnconsumedTrackDetector.NAME]: UnconsumedTrackDetectorConfig;\n\t[TrackDeliveryMismatchDetector.NAME]: TrackDeliveryMismatchDetectorConfig;\n\t[CallConcurrentIssueDetector.NAME]: CallConcurrentIssueDetectorConfig;\n\t[IssueFanOutDetector.NAME]: IssueFanOutDetectorConfig;\n\t[PublisherFaultCorroborationDetector.NAME]: PublisherFaultCorroborationDetectorConfig;\n};\n\nexport type AvailableDetectorsConfigs =\n\t| AvailableObserverScopeDetectorsConfigs\n\t| AvailableCallScopeDetectorsConfigs;\n\nexport class Detectors {\n\tprivate _detectors: Detector[];\n\n\tpublic constructor(...detectors: Detector[]) {\n\t\tthis._detectors = detectors;\n\t}\n\n\t/**\n\t * Every registered detector, in registration order.\n\t *\n\t * This is **the** way to get hold of an instance: `addDetector` / `addObserverDetector` are\n\t * chainable and return the owning entity, so the registry is where instances live. Read it to\n\t * inspect a detector's state, or to pick one out and {@link remove} it.\n\t *\n\t * A copy, not the live array — a caller iterating this while removing would otherwise skip\n\t * entries, and that is exactly what \"remove the ones that look like X\" does.\n\t */\n\tpublic get instances(): Detector[] {\n\t\treturn [ ...this._detectors ];\n\t}\n\n\t/** Iterate the registry directly: `for (const detector of call.detectors)`. */\n\tpublic [Symbol.iterator](): IterableIterator<Detector> {\n\t\treturn this.instances[Symbol.iterator]();\n\t}\n\n\t/** The names in registration order. Duplicates are meaningful — see {@link getAll}. */\n\tpublic get listOfNames() {\n\t\treturn this._detectors.map((d) => d.name);\n\t}\n\n\tpublic get size() {\n\t\treturn this._detectors.length;\n\t}\n\n\tpublic add(detector: Detector) {\n\t\tthis._detectors.push(detector);\n\t}\n\n\t/** The first detector registered under `name`. See {@link getAll} when several can share one. */\n\tpublic get(name: string): Detector | undefined {\n\t\treturn this._detectors.find((detector) => detector.name === name);\n\t}\n\n\t/**\n\t * Every detector registered under `name`.\n\t *\n\t * More than one is legitimate: `ClientPopulationIssueDetector` is meant to be added once per\n\t * `groupBy` axis, and two instances of it share a name.\n\t */\n\tpublic getAll(name: string): Detector[] {\n\t\treturn this._detectors.filter((detector) => detector.name === name);\n\t}\n\n\tpublic has(name: string): boolean {\n\t\treturn this._detectors.some((detector) => detector.name === name);\n\t}\n\n\t/** Remove one specific instance. Returns `false` if it was not registered here. */\n\tpublic remove(detector: Detector): boolean {\n\t\tconst remaining = this._detectors.filter((candidate) => candidate !== detector);\n\n\t\tif (remaining.length === this._detectors.length) return false;\n\n\t\tthis._detectors = remaining;\n\t\tthis._close(detector);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Remove **every** detector registered under `name`, returning how many were removed.\n\t *\n\t * All of them rather than the first, because a name can legitimately be registered more than once\n\t * (see {@link getAll}) and \"remove the `client-population-issue-detector`\" cannot sensibly mean\n\t * \"remove whichever axis happens to be first in the array\". Removing all of them is the only\n\t * behaviour that leaves the registry in a state the caller can predict from the name alone.\n\t *\n\t * Each removed detector gets `close()`, so trackers unsubscribe from the issue registry, bus\n\t * listeners drop, and timers clear — a detector removed without closing keeps being fed issues\n\t * forever.\n\t */\n\tpublic removeByName(name: string): number {\n\t\tconst removed = this._detectors.filter((detector) => detector.name === name);\n\n\t\tif (removed.length === 0) return 0;\n\n\t\tthis._detectors = this._detectors.filter((detector) => detector.name !== name);\n\t\tfor (const detector of removed) this._close(detector);\n\n\t\treturn removed.length;\n\t}\n\n\tpublic update() {\n\t\tfor (const detector of this._detectors) {\n\t\t\ttry {\n\t\t\t\tdetector.update();\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn(`Error updating detector ${detector?.name}`, err);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic clear() {\n\t\tconst detectors = this._detectors;\n\n\t\tthis._detectors = [];\n\t\tfor (const detector of detectors) this._close(detector);\n\t}\n\n\tprivate _close(detector: Detector) {\n\t\ttry {\n\t\t\tdetector.close?.();\n\t\t} catch (err) {\n\t\t\tlogger.warn(`Error closing detector ${detector?.name}`, err);\n\t\t}\n\t}\n}\n","import type { ActiveClientIssue } from './ActiveClientIssue';\nimport type { ActiveIssueTracker } from './ActiveIssueTracker';\n\n/**\n * One client's currently **open** stateful issues, keyed by `ClientIssue.key`.\n *\n * The server-side mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0):\n * a raise opens an entry, the matching `<type>-resolved` closes it, and the client's own close\n * force-resolves whatever is left. That turns point-in-time symptom reports into **intervals**,\n * which is what lets detectors ask \"are these clients broken *at the same time*\" rather than \"did\n * they both report something recently\".\n *\n * Keyed by `key` rather than by type on purpose: one client can have several issues of the same type\n * open at once (one per track), and they resolve independently.\n *\n * Every change is forwarded to the call's `ActiveIssuesRegistry`, which forwards to the observer's —\n * so the client owns the storage and the wider scopes get their views maintained as it happens.\n */\nexport class ObservedClientIssueRegistry {\n\tprivate readonly issues = new Map<string, ActiveClientIssue>();\n\n\tpublic constructor(\n\t\tprivate readonly registry?: ActiveIssueTracker,\n\t) {\n\t}\n\n\tpublic get size(): number {\n\t\treturn this.issues.size;\n\t}\n\n\tpublic keys(): IterableIterator<string> {\n\t\treturn this.issues.keys();\n\t}\n\n\tpublic values(): IterableIterator<ActiveClientIssue> {\n\t\treturn this.issues.values();\n\t}\n\n\tpublic get(key: string): ActiveClientIssue | undefined {\n\t\treturn this.issues.get(key);\n\t}\n\n\tpublic add(issue: ActiveClientIssue): this {\n\t\tconst existing = this.issues.get(issue.key);\n\n\t\t// Replacing a live key without retiring the old object would strand it in every upstream\n\t\t// registry and every subscribed tracker — nothing else holds the key to remove it later.\n\t\tif (existing && existing !== issue) this.registry?.delete(existing);\n\n\t\tthis.issues.set(issue.key, issue);\n\t\tthis.registry?.add(issue);\n\n\t\treturn this;\n\t}\n\n\tpublic remove(key: string): ActiveClientIssue | undefined {\n\t\tconst issue = this.issues.get(key);\n\n\t\tif (!issue) return undefined;\n\n\t\tthis.issues.delete(key);\n\t\tthis.registry?.delete(issue);\n\n\t\treturn issue;\n\t}\n\n\tpublic clear(): void {\n\t\t// Snapshot the keys: `remove` mutates the map, and this is the teardown path — it has to visit\n\t\t// every entry exactly once, or the upstream registries keep issues for a client that is gone.\n\t\tfor (const key of [ ...this.issues.keys() ]) this.remove(key);\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { ObservedCall } from '../ObservedCall';\n\nexport const UnconsumedTrackTypes = {\n\t/** A track is being published to the SFU that nobody is subscribed to — pure wasted uplink. */\n\tunconsumedPublishedTrack: 'UNCONSUMED_PUBLISHED_TRACK',\n} as const;\n\nexport type UnconsumedTrackDetectorConfig = {\n\n\t/**\n\t * How long a track must stay unconsumed **while still sending** before it is reported (ms).\n\t * Default `30_000`.\n\t *\n\t * This is the main guard against a false alarm, because a gap between publishing and the first\n\t * subscription is completely normal at join time — and again after every renegotiation. Sensible\n\t * range `15_000`–`120_000`. Too low and you report every join; too high and you tolerate wasted\n\t * uplink for longer than you need to. Waste is not an outage, so err high.\n\t */\n\tminUnconsumedDurationInMs: number;\n\n\t/**\n\t * Ignore tracks sending below this bitrate (**bits per second**). Default `50_000` (50 kbps).\n\t *\n\t * The point of the detector is wasted bandwidth, and a track trickling keep-alive packets wastes\n\t * none worth an alert. Typical `20_000`–`100_000`: muted or paused tracks sit near zero, a real\n\t * video track is hundreds of kbps. Set it to `0` to report every unconsumed track regardless of\n\t * cost.\n\t */\n\tminBitrate: number;\n\n\t/**\n\t * Re-arm time per track (ms). Default `300_000`.\n\t *\n\t * Long on purpose: an unconsumed track usually *stays* unconsumed, so a short cooldown means a\n\t * steady drip of the same finding for the life of the call. Typical `300_000`–`900_000`.\n\t */\n\tcooldownMs: number;\n};\n\n/**\n * Finds tracks that are **published but consumed by nobody** — uplink and SFU ingress spent on media\n * that is never forwarded anywhere.\n *\n * This is the one detector that reads the resolver's *silence* as the signal: an outbound track with\n * an empty `remoteInboundTracks` set, still pushing packets. It reads `call.unconsumedOutboundTracks`,\n * which the resolver maintains as tracks gain and lose subscribers, so a healthy call costs one\n * `size === 0` check rather than a walk over every published track. The usual causes are a participant\n * publishing while everyone has them hidden or muted-in-UI, a simulcast layer no viewer's bandwidth\n * ever selects, or an application that forgot to stop a track after the last subscriber left.\n *\n * It is deliberately slow to fire: `minUnconsumedDurationInMs` must elapse with the track still\n * sending, because a brief gap between publishing and the first subscription is completely normal at\n * join time.\n *\n * ### Careful: this detector is only sound with a resolver\n *\n * \"No subscribers\" and \"no resolver configured\" produce the identical observation — an empty link\n * set. Without a `RemoteTrackResolver` this would report *every* published track in the call as\n * unconsumed, so it checks `call.remoteTrackResolver` at runtime and does nothing without one.\n */\nexport class UnconsumedTrackDetector implements Detector {\n\tpublic static readonly NAME = 'unconsumed-track-detector';\n\tpublic readonly name = UnconsumedTrackDetector.NAME;\n\n\tpublic readonly config: UnconsumedTrackDetectorConfig;\n\n\t/** trackId -> when it was first seen sending with no subscribers. */\n\tprivate readonly _unconsumedSince = new Map<string, number>();\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\tpublic constructor(\n\t\tprivate readonly call: ObservedCall,\n\t\tconfig: Partial<UnconsumedTrackDetectorConfig> = {},\n\t) {\n\t\tthis.config = {\n\t\t\tcooldownMs: 300_000,\n\t\t\tminUnconsumedDurationInMs: 30_000,\n\t\t\tminBitrate: 50_000,\n\t\t\t...config,\n\t\t};\n\t}\n\n\tpublic update(): void {\n\t\t// Without links, \"no subscribers\" is unknowable — never guess.\n\t\tif (!this.call.remoteTrackResolver) return;\n\n\t\tconst unconsumed = this.call.unconsumedOutboundTracks;\n\n\t\t// The common case: every published track has a subscriber, so there is nothing to look at.\n\t\t// The set is maintained by the resolver at the two moments the answer can change (a track\n\t\t// gains its first subscriber, or loses its last), so this costs nothing on a healthy call\n\t\t// rather than a walk over every published track in it.\n\t\tif (unconsumed.size === 0) {\n\t\t\tif (0 < this._unconsumedSince.size) this._unconsumedSince.clear();\n\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst seen = new Set<string>();\n\n\t\tfor (const outboundTrack of unconsumed) {\n\t\t\tconst rtps = outboundTrack.getOutboundRtps() ?? [];\n\t\t\tlet bitrate = 0;\n\t\t\tlet sending = false;\n\n\t\t\tfor (let i = 0; i < rtps.length; i++) {\n\t\t\t\tbitrate += rtps[i].bitrate;\n\t\t\t\tif (0 < rtps[i].deltaPacketsSent) sending = true;\n\t\t\t}\n\n\t\t\tseen.add(outboundTrack.id);\n\n\t\t\tif (!sending || bitrate < this.config.minBitrate) {\n\t\t\t\tthis._unconsumedSince.delete(outboundTrack.id);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst since = this._unconsumedSince.get(outboundTrack.id) ?? now;\n\n\t\t\tthis._unconsumedSince.set(outboundTrack.id, since);\n\n\t\t\tconst unconsumedForMs = now - since;\n\n\t\t\tif (unconsumedForMs < this.config.minUnconsumedDurationInMs) continue;\n\t\t\tif (now - (this._lastRaisedAt.get(outboundTrack.id) ?? 0) < this.config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(outboundTrack.id, now);\n\n\t\t\tconst peerConnection = outboundTrack.getPeerConnection();\n\n\t\t\tthis.call.addIssue({\n\t\t\t\ttype: UnconsumedTrackTypes.unconsumedPublishedTrack,\n\t\t\t\ttimestamp: now,\n\t\t\t\tpayload: {\n\t\t\t\t\ttrackId: outboundTrack.id,\n\t\t\t\t\tkind: outboundTrack.kind,\n\t\t\t\t\tpublisherClientId: peerConnection?.client.clientId,\n\t\t\t\t\tpeerConnectionId: peerConnection?.peerConnectionId,\n\t\t\t\t\tbitrate,\n\t\t\t\t\tunconsumedForMs,\n\t\t\t\t\t// what the waste costs, roughly, if it continues\n\t\t\t\t\twastedBytesPerSecond: bitrate / 8,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tfor (const trackId of [ ...this._unconsumedSince.keys() ]) {\n\t\t\tif (!seen.has(trackId)) this._unconsumedSince.delete(trackId);\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._unconsumedSince.clear();\n\t\tthis._lastRaisedAt.clear();\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { ObservedCall } from '../ObservedCall';\nimport { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\nimport { ActiveClientIssue } from '../issues/ActiveClientIssue';\n\nexport const TrackDeliveryMismatchTypes = {\n\t/**\n\t * The source is sending, but **none** of its subscribers are receiving → the media is being lost\n\t * between the publisher and the receivers. In an SFU that means the forwarding path.\n\t */\n\tpublishedTrackNotDelivered: 'PUBLISHED_TRACK_NOT_DELIVERED',\n\n\t/**\n\t * The source is sending and most subscribers are fine, but **some** are dry → those consumers are\n\t * broken individually (in mediasoup, the usual mitigation is recreating the consumer).\n\t */\n\treceiverTrackNotDelivered: 'RECEIVER_TRACK_NOT_DELIVERED',\n\n\t/**\n\t * The source itself stopped producing, so its subscribers being dry is expected and **not** an\n\t * SFU fault. Reported so the other two verdicts can be trusted as *not* being this.\n\t */\n\tpublisherTrackDry: 'PUBLISHER_TRACK_DRY',\n} as const;\n\nexport type TrackDeliveryMismatchDetectorConfig = {\n\n\t/**\n\t * The receiver-side issue type meaning \"no media arriving\". Default `'dry-inbound-track'`, which is\n\t * what client-monitor-js raises. Only change it if you raise your own equivalent.\n\t */\n\tdryInboundIssueType: string;\n\n\t/**\n\t * The publisher-side issue type meaning \"not producing\". Default `'dry-outbound-track'`, as raised\n\t * by client-monitor-js. The pairing of these two types is the whole detector: their **disagreement**\n\t * is the finding.\n\t */\n\tdryOutboundIssueType: string;\n\n\t/**\n\t * Subscribers required before \"all of them\" means anything. Default `2`.\n\t *\n\t * With one subscriber, \"every receiver is dry\" is a single client's report and carries no more\n\t * weight than the client issue already does. Sensible range `2`–`4`.\n\t */\n\tminReceivers: number;\n\n\t/**\n\t * Fraction of subscribers that must be dry to call it a whole-track delivery failure. Default `1`.\n\t *\n\t * `1` — literally all of them — on purpose. The inference here is sharp: the publisher says it is\n\t * sending and *every* receiver says nothing arrives, so the fault is between them, in the SFU's\n\t * forwarding. Lowering it to `0.8` admits mixed evidence, where some receivers do get the media, and\n\t * the conclusion no longer follows: that is a per-receiver problem and `IssueFanOutDetector`'s\n\t * question. Do not lower it without deciding what the finding then means.\n\t */\n\tallReceiversRatio: number;\n\n\t/** Re-arm time per (track, verdict) in ms. Default `60_000`. Typical `30_000`–`300_000`. */\n\tcooldownMs: number;\n\n};\n\ntype DeliveryItem = {\n\tpublisherClientId: string,\n\tpublisherSending: boolean,\n\tpublisherDryIssue: boolean,\n\tpublisherBitrate: number,\n\tnumberOfSubscribers: number,\n\tnumberOfDrySubscribers: number,\n}\n\n/**\n * Answers **\"is the media actually getting through?\"** by joining the two ends of a published track.\n *\n * A dry track is the clearest possible symptom — no bytes are arriving — but on its own it is\n * ambiguous, and the ambiguity is precisely what a single endpoint cannot resolve. A receiver seeing\n * silence cannot tell whether the camera was switched off, the SFU stopped forwarding, or its own\n * consumer wedged. All three look identical from the browser.\n *\n * With the publisher↔subscriber links this becomes a three-way decision:\n *\n * | publisher | subscribers | verdict |\n * |---|---|---|\n * | sending | **all** dry | `PUBLISHED_TRACK_NOT_DELIVERED` — the SFU/forwarding path |\n * | sending | **some** dry | `RECEIVER_TRACK_NOT_DELIVERED` — those consumers (recreate them) |\n * | dry | any dry | `PUBLISHER_TRACK_DRY` — the source stopped; not an SFU fault |\n *\n * The publisher side is judged from **both** signals available: its own `dry-outbound-track` issue\n * when the client reports one, and — as the fallback, and the corroboration when it does not — the\n * observed outbound RTP (`deltaPacketsSent`). That combination is what makes the first row\n * trustworthy: the server can state that packets demonstrably left the publisher during the same\n * interval in which every receiver got nothing.\n *\n * This is the \"SFU forwarding mismatch\" check, and notably it needs **no** mediasoup instrumentation\n * — the client's own dry-track verdicts plus the resolver links are sufficient.\n */\nexport class TrackDeliveryMismatchDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'track-delivery-mismatch-detector' as const;\n\tpublic readonly name = TrackDeliveryMismatchDetector.NAME;\n\n\tprivate readonly _config: TrackDeliveryMismatchDetectorConfig;\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\tprivate readonly dryOutboundTracks = new Set<string>();\n\tprivate readonly dryInboundTracks = new Set<string>();\n\n\tpublic constructor(\n\t\tprivate readonly call: ObservedCall,\n\t\tconfig: Partial<TrackDeliveryMismatchDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tdryOutboundIssueType: 'dry-outbound-track',\n\t\t\tdryInboundIssueType: 'dry-inbound-track',\n\t\t\tminReceivers: 2,\n\t\t\tallReceiversRatio: 1,\n\t\t\tcooldownMs: 60_000,\n\t\t\t...config,\n\t\t};\n\n\t\t// Subscribe here, like every other tracker-shaped detector. Without this the dry-track sets\n\t\t// stay empty forever and `update()` silently judges every publisher as having zero dry\n\t\t// subscribers — the detector would run, cost time, and never be able to find anything.\n\t\tthis.call.activeIssuesRegistry.addIssueTracker(this._config.dryOutboundIssueType, this);\n\t\tthis.call.activeIssuesRegistry.addIssueTracker(this._config.dryInboundIssueType, this);\n\t}\n\n\tpublic close(): void {\n\t\tthis.call.activeIssuesRegistry.removeIssueTracker(this);\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.clear();\n\t}\n\n\tpublic add(issue: ActiveClientIssue): void {\n\t\tif (issue.type === this._config.dryOutboundIssueType) {\n\t\t\tif (issue.trackId) {\n\t\t\t\tthis.dryOutboundTracks.add(issue.trackId);\n\t\t\t}\n\t\t} else if (issue.type === this._config.dryInboundIssueType) {\n\t\t\tif (issue.trackId) {\n\t\t\t\tthis.dryInboundTracks.add(issue.trackId);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\tif (issue.type === this._config.dryOutboundIssueType) {\n\t\t\tthis.dryOutboundTracks.delete(issue.trackId ?? '');\n\t\t} else if (issue.type === this._config.dryInboundIssueType) {\n\t\t\tthis.dryInboundTracks.delete(issue.trackId ?? '');\n\t\t}\n\t\t\n\t\treturn true;\n\t}\n\n\tpublic get size(): number {\n\t\treturn this.dryOutboundTracks.size + this.dryInboundTracks.size;\n\t}\n\n\tpublic clear(): void {\n\t\tthis.dryOutboundTracks.clear();\n\t\tthis.dryInboundTracks.clear();\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\tif (issue.type === this._config.dryOutboundIssueType) {\n\t\t\treturn this.dryOutboundTracks.has(issue.trackId ?? '');\n\t\t} else if (issue.type === this._config.dryInboundIssueType) {\n\t\t\treturn this.dryInboundTracks.has(issue.trackId ?? '');\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n\tpublic update(): void {\n\t\tconst now = Date.now();\n\t\tconst deliveries = new Map<string, DeliveryItem>();\n\n\t\tfor (const client of this.call.observedClients.values()) {\n\t\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\t\tfor (const outboundTrack of peerConnection.observedOutboundTracks.values()) {\n\t\t\t\t\tdeliveries.set(outboundTrack.id, {\n\t\t\t\t\t\tpublisherClientId: client.clientId,\n\t\t\t\t\t\tpublisherSending: (outboundTrack.bitrate ?? 0) > 0,\n\t\t\t\t\t\tpublisherDryIssue: this.dryOutboundTracks.has(outboundTrack.id),\n\t\t\t\t\t\tpublisherBitrate: outboundTrack.bitrate ?? 0,\n\t\t\t\t\t\tnumberOfDrySubscribers: 0,\n\t\t\t\t\t\tnumberOfSubscribers: 0,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tfor (const inboundTrack of peerConnection.observedInboundTracks.values()) {\n\t\t\t\t\tconst delivery = deliveries.get(inboundTrack.remoteOutboundTrack?.id ?? '');\n\n\t\t\t\t\tif (!delivery) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tdelivery.numberOfDrySubscribers += this.dryInboundTracks.has(inboundTrack.id) ? 1 : 0;\n\t\t\t\t\tdelivery.numberOfSubscribers += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const [ outboundTrackId, delivery ] of deliveries) {\n\t\t\tif (delivery.numberOfSubscribers < this._config.minReceivers) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Nothing is wrong: the publisher is sending and no subscriber reported a dry track. Without\n\t\t\t// this the `else` branch below is unconditional, so a perfectly healthy call raises\n\t\t\t// RECEIVER_TRACK_NOT_DELIVERED for every published track, every cooldown period.\n\t\t\tif (delivery.publisherSending && delivery.numberOfDrySubscribers === 0) continue;\n\n\t\t\tconst dryRatio = delivery.numberOfDrySubscribers / delivery.numberOfSubscribers;\n\t\t\tlet type: string;\n\n\t\t\tif (!delivery.publisherSending) {\n\t\t\t\ttype = TrackDeliveryMismatchTypes.publisherTrackDry;\n\t\t\t} else if (this._config.allReceiversRatio <= dryRatio) {\n\t\t\t\ttype = TrackDeliveryMismatchTypes.publishedTrackNotDelivered;\n\t\t\t} else {\n\t\t\t\ttype = TrackDeliveryMismatchTypes.receiverTrackNotDelivered;\n\t\t\t}\n\n\t\t\tconst key = `${outboundTrackId}:${type}`;\n\n\t\t\tif (now - (this._lastRaisedAt.get(key) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(key, now);\n\n\t\t\tthis.call.addIssue({\n\t\t\t\ttype,\n\t\t\t\ttimestamp: now,\n\t\t\t\tpayload: {\n\t\t\t\t\ttrackId: outboundTrackId,\n\t\t\t\t\tpublisherClientId: delivery.publisherClientId,\n\t\t\t\t\tpublisherSending: delivery.publisherSending,\n\t\t\t\t\tpublisherDryIssue: delivery.publisherDryIssue,\n\t\t\t\t\tpublisherBitrate: delivery.publisherBitrate,\n\t\t\t\t\tnumberOfSubscribers: delivery.numberOfSubscribers,\n\t\t\t\t\tnumberOfDrySubscribers: delivery.numberOfDrySubscribers,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n}\n","/**\n * Turning a correlation into a **conclusion**.\n *\n * Every detector in this library ultimately reports the same shape of observation: *N clients have\n * issue X open at once, and here is what they have in common*. That is useful but not yet\n * actionable — someone still has to know that congestion spread across unrelated calls means the\n * server, while CPU limitation spread across unrelated calls means a bad client release. This module\n * holds that interpretation step so it is stated once, consistently, instead of being re-derived by\n * whoever reads the alert at 3am.\n *\n * ### Two functions, because there are two questions\n *\n * A detector already knows its scope — it was constructed with an `ObservedCall` or with the\n * `Observer`. Handing that scope back to a single generic function meant every caller supplied\n * fields the other scope needed and its own scope ignored: a call-scoped detector passing\n * `affectedCalls: 1, totalCalls: 1` forever, an observer-scoped one passing a participant ratio that\n * was deliberately never read. Placeholders like that are a standing invitation to read them as if\n * they meant something.\n *\n * So there are two entry points, each taking only the facts its scope actually has:\n *\n * - {@link concludeCallIssue} — within one call. The axis is *how much of the meeting*, and whether\n * the affected clients all subscribe to one published track.\n * - {@link concludeObserverIssue} — across calls. The axis is *how many independent calls*, which is\n * the only thing that separates \"one bad room\" from \"our infrastructure\".\n *\n * Neither the issue family nor the spread concludes anything alone: congestion in one call is a\n * meeting problem, congestion in six calls is an infrastructure problem, and the issue type is\n * identical in both.\n */\n\n/** Where the fault most likely sits, given who is affected. */\nexport type IssueFaultDomain =\n\n\t/** Independent calls affected at once — they share only the servers and the network. */\n\t| 'infrastructure'\n\n\t/** One call, broadly affected — something that call shares (its SFU worker, room, or host). */\n\t| 'call'\n\n\t/** The subscribers of one published track — the publisher's path or the forwarding of it. */\n\t| 'published-track'\n\n\t/** A single endpoint — its own device or last mile. */\n\t| 'endpoint'\n\n\t/** Independent calls affected, but by something endpoints own — a client build, not a server. */\n\t| 'client-population'\n\n\t/** Not enough signal to attribute. */\n\t| 'unknown';\n\n/** A stated verdict, attached to the raised issue payload. */\nexport type IssueConclusion = {\n\n\t/** Where to look. */\n\tfaultDomain: IssueFaultDomain;\n\n\t/** One line, written to be readable in an alert without opening a dashboard. */\n\tsummary: string;\n\n\t/** What to check first. Omitted when the issue family is unknown to this module. */\n\trecommendation?: string;\n\n\t/**\n\t * How much the spread alone justifies the verdict, `0..1`. Not a probability — a coarse ranking\n\t * so alerting can threshold on it. More independent calls, or a tighter onset, means higher.\n\t */\n\tconfidence: number;\n};\n\n/** The facts a **call-scoped** conclusion is drawn from. */\nexport type CallIssueSpread = {\n\tissueType: string;\n\n\t/** Distinct clients of this call with the issue open. */\n\taffectedClients: number;\n\n\t/** Participants in the call — the denominator. */\n\ttotalClients: number;\n\n\t/** True when the onsets clustered — a shared trigger rather than drift. */\n\tonsetBurst: boolean;\n\n\t/**\n\t * Set when the affected clients are the subscriber set of **one published track**.\n\t *\n\t * The strongest call-scoped statement available: those clients share a publisher and nothing\n\t * else, so the receivers are exonerated and the source's path is implicated.\n\t */\n\tpublishedTrackId?: string;\n};\n\n/** The facts an **observer-scoped** conclusion is drawn from. */\nexport type ObserverIssueSpread = {\n\tissueType: string;\n\n\t/** Distinct clients across the fleet with the issue open. */\n\taffectedClients: number;\n\n\t/** Clients in the fleet. Reported for context; it does not gate anything at this scope. */\n\ttotalClients: number;\n\n\t/** Distinct calls containing at least one affected client. The dimension that matters here. */\n\taffectedCalls: number;\n\n\t/** Calls in flight. */\n\ttotalCalls: number;\n\n\t/** True when the onsets clustered. */\n\tonsetBurst: boolean;\n};\n\ntype IssueFamily = {\n\tlabel: string;\n\n\t/** What it means when this is widespread across independent calls. */\n\tinfrastructure?: { summary: string, recommendation: string };\n\n\t/** Overrides the default `infrastructure` domain — used where breadth implicates clients, not servers. */\n\tcrossCallDomain?: IssueFaultDomain;\n\n\t/** What it means when it is confined to one call. */\n\tcall?: { summary: string, recommendation: string };\n\n\t/** What it means when it follows one published track. */\n\tpublishedTrack?: { summary: string, recommendation: string };\n};\n\n/**\n * Issue families, keyed by prefix match against the client's issue type.\n *\n * Deliberately a small table rather than an exhaustive one: `client-monitor-js` detectors are\n * extensible and applications add their own types, so an unrecognised type still produces a\n * structurally correct conclusion (`unknown` family, spread-derived domain) rather than nothing.\n */\nconst families: { match: (type: string) => boolean, family: IssueFamily }[] = [\n\t{\n\t\tmatch: (t) => t.startsWith('congestion') || t.includes('bandwidth'),\n\t\tfamily: {\n\t\t\tlabel: 'congestion',\n\t\t\tinfrastructure: {\n\t\t\t\tsummary: 'network congestion is open across independent calls at the same time',\n\t\t\t\trecommendation: 'check SFU egress bandwidth and host network saturation before looking at any single participant',\n\t\t\t},\n\t\t\tcall: {\n\t\t\t\tsummary: 'most of this call is congested at once',\n\t\t\t\trecommendation: 'check the SFU worker serving this call, and whether one publisher is saturating it',\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tmatch: (t) => t.startsWith('ice-') || t.includes('turn') || t === 'unstable-ice-path',\n\t\tfamily: {\n\t\t\tlabel: 'connectivity',\n\t\t\tinfrastructure: {\n\t\t\t\tsummary: 'ICE connectivity is failing across independent calls at the same time',\n\t\t\t\trecommendation: 'check SFU reachability and TURN health; a worker restart shows up as a wave of DTLS-stage failures rather than pure consent loss',\n\t\t\t},\n\t\t\tcall: {\n\t\t\t\tsummary: 'this call is losing ICE connectivity broadly',\n\t\t\t\trecommendation: 'check the SFU worker and the transports belonging to this call',\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tmatch: (t) => t.startsWith('audio-') || t.includes('concealment') || t.includes('jitter-buffer'),\n\t\tfamily: {\n\t\t\tlabel: 'audio',\n\t\t\tinfrastructure: {\n\t\t\t\tsummary: 'audio impairment is open across independent calls at the same time',\n\t\t\t\trecommendation: 'check SFU egress and the audio forwarding path; concealment rising fleet-wide is a delivery symptom, not a device one',\n\t\t\t},\n\t\t\tcall: {\n\t\t\t\tsummary: 'audio is impaired for most of this call',\n\t\t\t\trecommendation: 'check this call\\'s SFU worker; if only receivers are affected, the publisher\\'s uplink is the next place to look',\n\t\t\t},\n\t\t\tpublishedTrack: {\n\t\t\t\tsummary: 'the subscribers of one published audio track are impaired while others are fine',\n\t\t\t\trecommendation: 'check that publisher\\'s uplink and the SFU\\'s forwarding of that producer; the receivers themselves are exonerated by the fan-out',\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tmatch: (t) => t.includes('video') || t.includes('freeze') || t.includes('keyframe') || t.includes('decoder'),\n\t\tfamily: {\n\t\t\tlabel: 'video delivery',\n\t\t\tinfrastructure: {\n\t\t\t\tsummary: 'video delivery is degraded across independent calls at the same time',\n\t\t\t\trecommendation: 'check SFU egress and the keyframe/PLI path; a keyframe storm fleet-wide usually means the server is dropping or not answering PLIs',\n\t\t\t},\n\t\t\tcall: {\n\t\t\t\tsummary: 'video is degraded for most of this call',\n\t\t\t\trecommendation: 'check this call\\'s SFU worker and whether PLIs are being answered',\n\t\t\t},\n\t\t\tpublishedTrack: {\n\t\t\t\tsummary: 'the subscribers of one published video track are degraded while others are fine',\n\t\t\t\trecommendation: 'check that publisher\\'s uplink and the forwarding of that producer; consider recreating the consumers if only some receivers are affected',\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tmatch: (t) => t.includes('cpu') || t.includes('overloaded') || t.includes('encoder') || t.includes('performance'),\n\t\tfamily: {\n\t\t\tlabel: 'endpoint capacity',\n\t\t\t// Breadth here does NOT implicate the servers. Endpoint CPU is owned by the endpoint, so the\n\t\t\t// same symptom in many unrelated calls points at what those endpoints have in common —\n\t\t\t// a client release, a browser update, a VDI host — not at the SFU.\n\t\t\tcrossCallDomain: 'client-population',\n\t\t\tinfrastructure: {\n\t\t\t\tsummary: 'endpoint CPU limitation is open across independent calls at the same time',\n\t\t\t\trecommendation: 'this is not an SFU symptom: look at what the affected clients share — a recent client release, a browser version, or shared/virtualised hardware',\n\t\t\t},\n\t\t\tcall: {\n\t\t\t\tsummary: 'most participants of this call are CPU limited',\n\t\t\t\trecommendation: 'check whether the call is asking too much of endpoints (layer count, resolution, participant grid size)',\n\t\t\t},\n\t\t},\n\t},\n];\n\nconst unknownFamily: IssueFamily = { label: 'issue' };\n\nfunction familyOf(issueType: string): IssueFamily {\n\treturn families.find((entry) => entry.match(issueType))?.family ?? unknownFamily;\n}\n\n/**\n * Confidence for a call-scoped conclusion, from the spread alone.\n *\n * Starts low and climbs with the share of the meeting affected. A clustered onset adds to it, because\n * simultaneity is hard to produce by coincidence, and a track-scoped group adds more still — it is a\n * statement about a specific source rather than about the room.\n */\nfunction callConfidenceOf(spread: CallIssueSpread): number {\n\tlet confidence = 0.3;\n\n\tif (0 < spread.totalClients) {\n\t\tconfidence += Math.min(0.3, (spread.affectedClients / spread.totalClients) * 0.3);\n\t}\n\n\tif (spread.onsetBurst) confidence += 0.2;\n\tif (spread.publishedTrackId !== undefined) confidence += 0.1;\n\n\treturn Math.min(1, Math.round(confidence * 100) / 100);\n}\n\n/**\n * Confidence for an observer-scoped conclusion.\n *\n * Independent calls are worth far more than participant count here: five affected clients in one\n * meeting have many shared local explanations, whereas five affected clients in five meetings have\n * exactly one. The call term saturates, because the tenth affected call tells you little the third\n * did not.\n */\nfunction observerConfidenceOf(spread: ObserverIssueSpread): number {\n\t// 2 calls -> +0.2, 3 -> +0.3, 5+ -> +0.4\n\tlet confidence = 0.3 + Math.min(0.4, Math.max(0, spread.affectedCalls - 1) * 0.1);\n\n\tif (spread.onsetBurst) confidence += 0.2;\n\n\treturn Math.min(1, Math.round(confidence * 100) / 100);\n}\n\n/**\n * Draw the conclusion for a group of clients **within one call**.\n *\n * Ordered most-to-least specific: a track-scoped group is a stronger statement than a call-wide one,\n * and a single affected endpoint is not a statement about the call at all.\n */\nexport function concludeCallIssue(spread: CallIssueSpread): IssueConclusion {\n\tconst family = familyOf(spread.issueType);\n\tconst confidence = callConfidenceOf(spread);\n\tconst affectedShare = 0 < spread.totalClients\n\t\t? ` (${spread.affectedClients}/${spread.totalClients} clients)`\n\t\t: '';\n\n\tif (spread.publishedTrackId !== undefined) {\n\t\tconst text = family.publishedTrack;\n\n\t\treturn {\n\t\t\tfaultDomain: 'published-track',\n\t\t\tconfidence,\n\t\t\tsummary: text\n\t\t\t\t? `${text.summary} — track ${spread.publishedTrackId}${affectedShare}`\n\t\t\t\t: `'${spread.issueType}' follows published track ${spread.publishedTrackId}${affectedShare}, so the fault is on that source's path rather than the receivers'`,\n\t\t\trecommendation: text?.recommendation,\n\t\t};\n\t}\n\n\tif (1 < spread.affectedClients) {\n\t\tconst text = family.call;\n\n\t\treturn {\n\t\t\tfaultDomain: 'call',\n\t\t\tconfidence,\n\t\t\tsummary: text\n\t\t\t\t? `${text.summary}${affectedShare}`\n\t\t\t\t: `'${spread.issueType}' is open for ${spread.affectedClients} of ${spread.totalClients} participants of one call`,\n\t\t\trecommendation: text?.recommendation,\n\t\t};\n\t}\n\n\treturn {\n\t\tfaultDomain: spread.affectedClients === 1 ? 'endpoint' : 'unknown',\n\t\tconfidence,\n\t\tsummary: `'${spread.issueType}' affects a single endpoint; nothing is shared with the other participants`,\n\t\trecommendation: 'treat as that participant\\'s own device or last mile',\n\t};\n}\n\n/**\n * Draw the conclusion for a group of clients spanning **several calls**.\n *\n * One affected call is not an observer-scoped finding — it has an obvious local explanation and the\n * call-scoped detector has already reported it — so that case returns `call` and says so rather than\n * dressing it up as a fleet event.\n *\n * Which domain breadth implicates depends on the family, and this is the whole reason the module\n * exists: `congestion` across unrelated calls points at the servers, `cpulimitation` across unrelated\n * calls points at what those *endpoints* share — a client release, a browser version, shared\n * virtualised hardware — and pointing an SFU team at the second one wastes a night.\n */\nexport function concludeObserverIssue(spread: ObserverIssueSpread): IssueConclusion {\n\tconst family = familyOf(spread.issueType);\n\tconst confidence = observerConfidenceOf(spread);\n\tconst affectedShare = 0 < spread.totalClients\n\t\t? ` (${spread.affectedClients}/${spread.totalClients} clients)`\n\t\t: '';\n\n\tif (spread.affectedCalls <= 1) {\n\t\treturn {\n\t\t\tfaultDomain: 'call',\n\t\t\tconfidence,\n\t\t\tsummary: `'${spread.issueType}' is confined to a single call${affectedShare}; that is a meeting problem, not a fleet one`,\n\t\t\trecommendation: 'look at the call-scoped finding for this call rather than at the infrastructure',\n\t\t};\n\t}\n\n\tconst domain = family.crossCallDomain ?? 'infrastructure';\n\tconst text = family.infrastructure;\n\n\treturn {\n\t\tfaultDomain: domain,\n\t\tconfidence,\n\t\tsummary: text\n\t\t\t? `${text.summary} — ${spread.affectedCalls} of ${spread.totalCalls} calls${affectedShare}`\n\t\t\t: `'${spread.issueType}' is open in ${spread.affectedCalls} of ${spread.totalCalls} independent calls at once${affectedShare}; they share only the infrastructure`,\n\t\trecommendation: text?.recommendation,\n\t};\n}\n","import type { Detector } from './Detector';\nimport type { ObservedCall } from '../ObservedCall';\nimport type { ActiveClientIssue } from '../issues/ActiveClientIssue';\nimport type { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\nimport { concludeCallIssue } from './IssueConclusion';\n\nexport const CallConcurrentIssueTypes = {\n\t/** Several participants of this call have the same issue open **at the same time**. */\n\tconcurrentClientIssues: 'CONCURRENT_CLIENT_ISSUES',\n\n\t/** Those issues also *began* together — the signature of one shared event. */\n\tissueOnsetBurst: 'ISSUE_ONSET_BURST',\n} as const;\n\nexport type CallConcurrentIssueDetectorConfig = {\n\n\t/**\n\t * The issue types to watch. **Required, and must not be empty** — the detector subscribes to\n\t * exactly these and sees nothing else.\n\t */\n\tissueTypes: string[];\n\n\t/**\n\t * Participants the call needs before a ratio means anything. Default `3`.\n\t *\n\t * In a 1:1 call \"half the participants\" is one person, which is a client problem and not a call\n\t * problem — `2` effectively disables the ratio gate. Raise it for large-meeting products where you\n\t * only care once a handful are affected.\n\t */\n\tminClients: number;\n\n\t/**\n\t * Distinct clients that must share the issue. Default `3`.\n\t *\n\t * The absolute floor under `affectedRatioThreshold`, so a small call cannot clear a ratio with two\n\t * unlucky people. Sensible range `2`–`5`; `2` is the lowest that can still mean \"more than one\n\t * participant\", which is the whole premise.\n\t */\n\tminAffectedClients: number;\n\n\t/**\n\t * Fraction of the call's participants that must share it, `0`–`1`. Default `0.5`.\n\t *\n\t * Typical `0.3`–`0.7`. Lower catches partial events — a subset on one SFU worker — at the cost of\n\t * firing on a few coincidentally unhappy participants; `1` demands literally everyone, which real\n\t * incidents rarely produce because someone always reconnects first.\n\t */\n\taffectedRatioThreshold: number;\n\n\t/**\n\t * Onsets falling within this span (ms) escalate the finding to `ISSUE_ONSET_BURST` — they did not\n\t * just overlap, they started together. Default `2_000`.\n\t *\n\t * Bound this by your sampling period, not below it: onsets are only known as accurately as clients\n\t * report them, so a window shorter than one sampling period can only fire by luck. Typical\n\t * `1_000`–`5_000`. Wider makes the escalation meaningless, since unrelated issues drift into the\n\t * same window.\n\t */\n\tonsetBurstWindowInMs: number;\n\n\t/**\n\t * Re-arm time per issue type (ms). Default `60_000`.\n\t *\n\t * A shared event is one incident, not one per tick. Too low and a persistent problem raises an\n\t * issue every tick for as long as it lasts; too high and a genuinely new occurrence is swallowed\n\t * by the previous one's cooldown. Typical `30_000`–`300_000`.\n\t */\n\tcooldownMs: number;\n};\n\n/** What the detector currently knows about one issue type in this call. */\nexport type CallConcurrentIssueGroup = {\n\ttype: string;\n\tissues: ActiveClientIssue[];\n\tclientIds: string[];\n\taffectedRatio: number;\n\ttotalClients: number;\n\n\t/**\n\t * Spread of the onsets, in **observer** time (ms) — `max(observedAt) - min(observedAt)`.\n\t *\n\t * Measured on the observer clock on purpose: `raisedAt` comes from each client's own clock, and\n\t * comparing those across machines makes clock skew look like a shared event.\n\t */\n\tonsetSpreadInMs: number;\n\tfirstObservedAt: number;\n};\n\n/**\n * Answers **\"is this meeting in trouble?\"** — several participants of one call with the same issue\n * open simultaneously.\n *\n * The client already decides *what* is wrong for itself — `congestion`, `ice-disconnected`,\n * `audio-concealment`, `video-decoder-overloaded` — with hysteresis and multi-signal confirmation\n * behind each verdict. Re-deriving those server-side from raw counters would be strictly worse. What\n * the server uniquely knows is *how many other participants of the same call are in that state right\n * now*, which is the difference between \"one person's Wi-Fi\" and \"this room is broken\".\n *\n * Concurrency is judged from the **open interval set**, not a window of recent reports. A window has\n * to guess whether a symptom is still happening; an interval is closed by the client when the episode\n * actually ends (client-monitor-js >= 4.6.0 ships the `<type>-resolved` companion for exactly this).\n *\n * ```ts\n * observedCall.addDetector('call-concurrent-issue-detector', {\n * issueTypes: [ 'congestion', 'ice-disconnected' ],\n * });\n * ```\n *\n * For the cross-call version of this question — which is a different question, not this one with a\n * bigger denominator — see `ObserverConcurrentIssueDetector`.\n */\nexport class CallConcurrentIssueDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'call-concurrent-issue-detector' as const;\n\n\tpublic readonly name = CallConcurrentIssueDetector.NAME;\n\n\tprivate readonly _config: CallConcurrentIssueDetectorConfig;\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\t/** issue type -> the issues of that type currently open in this call. */\n\tprivate readonly _byType = new Map<string, Set<ActiveClientIssue>>();\n\tprivate _size = 0;\n\n\t/** The groups that qualified on the most recent `update()`. Exposed for tests/dashboards. */\n\tpublic lastGroups: CallConcurrentIssueGroup[] = [];\n\n\tpublic constructor(\n\t\tprivate readonly _call: ObservedCall,\n\t\tconfig: Partial<CallConcurrentIssueDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tissueTypes: [],\n\t\t\tminClients: 3,\n\t\t\tminAffectedClients: 3,\n\t\t\taffectedRatioThreshold: 0.5,\n\t\t\tonsetBurstWindowInMs: 2_000,\n\t\t\tcooldownMs: 60_000,\n\t\t\t...config,\n\t\t};\n\n\t\tfor (const type of this._config.issueTypes) {\n\t\t\tthis._call.activeIssuesRegistry.addIssueTracker(type, this);\n\t\t}\n\t}\n\n\tpublic get size(): number {\n\t\treturn this._size;\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this._byType.get(issue.type)?.has(issue) ?? false;\n\t}\n\n\tpublic add(issue: ActiveClientIssue): void {\n\t\tlet bucket = this._byType.get(issue.type);\n\n\t\tif (!bucket) {\n\t\t\tbucket = new Set();\n\t\t\tthis._byType.set(issue.type, bucket);\n\t\t}\n\t\tif (bucket.has(issue)) return;\n\n\t\tbucket.add(issue);\n\t\t++this._size;\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\tconst bucket = this._byType.get(issue.type);\n\n\t\tif (!bucket?.delete(issue)) return false;\n\n\t\t--this._size;\n\t\t// Drop the empty bucket, so a long call doesn't retain one map entry per issue type ever seen.\n\t\tif (bucket.size === 0) this._byType.delete(issue.type);\n\n\t\treturn true;\n\t}\n\n\tpublic clear(): void {\n\t\tthis._byType.clear();\n\t\tthis._size = 0;\n\t\tthis.lastGroups = [];\n\t}\n\n\tpublic update(): void {\n\t\tthis.lastGroups = [];\n\n\t\t// Nothing subscribed is open: the overwhelmingly common case, and it costs one check.\n\t\tif (this._size === 0) return;\n\n\t\tconst now = Date.now();\n\t\tconst totalClients = this._call.observedClients.size;\n\n\t\tif (totalClients < this._config.minClients) return;\n\n\t\tfor (const [ type, issues ] of this._byType) {\n\t\t\tconst group = this._groupOf(type, issues, totalClients);\n\n\t\t\tif (group.clientIds.length < this._config.minAffectedClients) continue;\n\t\t\tif (group.affectedRatio < this._config.affectedRatioThreshold) continue;\n\n\t\t\tthis.lastGroups.push(group);\n\n\t\t\tif (now - (this._lastRaisedAt.get(type) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(type, now);\n\n\t\t\tconst burst = group.onsetSpreadInMs <= this._config.onsetBurstWindowInMs;\n\t\t\tconst issueType = burst\n\t\t\t\t? CallConcurrentIssueTypes.issueOnsetBurst\n\t\t\t\t: CallConcurrentIssueTypes.concurrentClientIssues;\n\t\t\t// The interpretation step: what this spread implies, stated once here rather than left to\n\t\t\t// whoever reads the alert.\n\t\t\tconst conclusion = concludeCallIssue({\n\t\t\t\tissueType: type,\n\t\t\t\taffectedClients: group.clientIds.length,\n\t\t\t\ttotalClients: group.totalClients,\n\t\t\t\tonsetBurst: burst,\n\t\t\t});\n\n\t\t\tthis._call.addIssue({\n\t\t\t\ttype: issueType,\n\t\t\t\ttimestamp: now,\n\t\t\t\tconclusion,\n\t\t\t\tpayload: {\n\t\t\t\t\tissueType: type,\n\t\t\t\t\tclients: group.totalClients,\n\t\t\t\t\taffectedClients: group.clientIds.length,\n\t\t\t\t\taffectedRatio: group.affectedRatio,\n\t\t\t\t\taffectedClientIds: group.clientIds,\n\t\t\t\t\tonsetSpreadInMs: group.onsetSpreadInMs,\n\t\t\t\t\tonsetBurst: burst,\n\t\t\t\t\tfirstObservedAt: group.firstObservedAt,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._call.activeIssuesRegistry.removeIssueTracker(this);\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.clear();\n\t}\n\n\tprivate _groupOf(type: string, issues: Set<ActiveClientIssue>, totalClients: number): CallConcurrentIssueGroup {\n\t\t// One pass, accumulating everything. `Math.min(...array)` on a spread would allocate and can\n\t\t// blow the stack on large groups, so the onsets are tracked here instead.\n\t\tconst clientIds = new Set<string>();\n\t\tconst list: ActiveClientIssue[] = [];\n\t\tlet earliest = Infinity;\n\t\tlet latest = -Infinity;\n\n\t\tfor (const issue of issues) {\n\t\t\tlist.push(issue);\n\t\t\t// One client can hold several issues of a type (one per track); the unit is the client, so\n\t\t\t// the ratio can never exceed 1.\n\t\t\tclientIds.add(issue.clientId);\n\n\t\t\tif (issue.observedAt < earliest) earliest = issue.observedAt;\n\t\t\tif (latest < issue.observedAt) latest = issue.observedAt;\n\t\t}\n\n\t\treturn {\n\t\t\ttype,\n\t\t\tissues: list,\n\t\t\tclientIds: [ ...clientIds ],\n\t\t\ttotalClients,\n\t\t\taffectedRatio: 0 < totalClients ? clientIds.size / totalClients : 0,\n\t\t\tonsetSpreadInMs: latest - earliest,\n\t\t\tfirstObservedAt: earliest,\n\t\t};\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { ObservedCall } from '../ObservedCall';\nimport type { ObservedOutboundTrack } from '../ObservedOutboundTrack';\nimport type { ActiveClientIssue } from '../issues/ActiveClientIssue';\nimport type { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\nimport { concludeCallIssue } from './IssueConclusion';\n\nexport const IssueFanOutTypes = {\n\t/** Most receivers of one published track have the same issue open → the fault follows the source. */\n\tpublishedTrackIssueFanOut: 'PUBLISHED_TRACK_ISSUE_FAN_OUT',\n\n\t/** Exactly one receiver of a track has it → that receiver's own problem. */\n\tsingleReceiverIssue: 'SINGLE_RECEIVER_ISSUE',\n} as const;\n\nexport type IssueFanOutDetectorConfig = {\n\n\t/**\n\t * The receiver-side issue types to attribute to publishers. **Required, and must not be empty** —\n\t * the detector subscribes to exactly these.\n\t *\n\t * There is no \"all types\" option. Which of a receiver's complaints are worth blaming a publisher\n\t * for is application knowledge: `freezed-video-track` fanning out across a track's subscribers\n\t * implicates the source, `cpulimitation` fanning out the same way implicates the receivers'\n\t * hardware and would be a false accusation.\n\t */\n\tissueTypes: string[];\n\n\t/**\n\t * Receivers a track needs before a ratio means anything. Default `3`.\n\t *\n\t * With two receivers, \"60% affected\" is one of them — which is the single-receiver case below, not\n\t * a fan-out. Sensible range `2`–`5`; in small calls a published track rarely has more than a couple\n\t * of subscribers, so raising this can silence the detector entirely.\n\t */\n\tminReceivers: number;\n\n\t/**\n\t * Fraction of a track's receivers that must share the issue, `0`–`1`. Default `0.6`.\n\t *\n\t * The higher this is, the more the finding points at the publisher rather than at the network\n\t * between: *everyone* receiving this track badly is hard to explain any other way. Typical\n\t * `0.5`–`0.8`. Below `0.5` you are reporting \"some receivers\", which usually means their own\n\t * last miles.\n\t */\n\taffectedRatioThreshold: number;\n\n\t/**\n\t * Also report when exactly one receiver is affected. Default `true`.\n\t *\n\t * Kept on because the finding is *useful and correctly weaker*: it is raised with a lower\n\t * confidence and the opposite conclusion — one unhappy receiver out of eight points at that\n\t * receiver, not at the publisher. Turn it off if you only want publisher-blaming findings and\n\t * treat single-receiver trouble as the client's own business.\n\t */\n\treportSingleReceiver: boolean;\n\n\t/**\n\t * Re-arm time per (track, issue type) in ms. Default `60_000`.\n\t *\n\t * Per track, so a call with many bad publishers still reports each of them. Typical\n\t * `30_000`–`300_000`.\n\t */\n\tcooldownMs: number;\n};\n\n/**\n * Attributes **client-reported issues to the published track they are about**, then asks how far\n * the problem fans out across that track's receivers.\n *\n * The join is what makes this possible: a receiver-side issue payload carries `trackId` (the client\n * detectors report it for every track-scoped issue), the observer resolves that to an inbound track,\n * and `RemoteTrackResolver` links the inbound track to the `remoteOutboundTrack` that published it.\n * With the whole subscriber set of one source in hand, the verdict is straightforward and is the\n * single most useful thing a server can say:\n *\n * - **most receivers of Alice's track are affected** → the fault is on Alice's path — her uplink, the\n * SFU's ingress, or its forwarding of that stream.\n * - **one receiver of Alice's track is affected** → that receiver's downlink. Nothing to do with\n * Alice, even though the symptom is reported against her stream.\n *\n * Deliberately generic over the issue vocabulary: `freezed-video-track`, `keyframe-storm`,\n * `audio-concealment`, `video-decoder-overloaded`, `stuck-decoder` and anything a custom client\n * detector invents all fan out the same way, so one mechanism replaces a family of symptom-specific\n * detectors.\n *\n * ### It walks the affected tracks, never all of them\n *\n * The detector is fed open issues by the call's registry and keeps only those carrying a `trackId`.\n * Each tick it resolves *those* tracks to their publishers — never the published tracks of the call,\n * of which there are many more and almost all of them fine. A call with nothing wrong costs one\n * `size === 0` check.\n *\n * ### Requires a `RemoteTrackResolver`\n *\n * Without publisher↔subscriber links there is no way to know which receivers belong to one source,\n * so the detector does nothing when the call has no resolver. It does not fall back to guessing:\n * \"one receiver of an unknown set\" is not a statement worth raising.\n */\nexport class IssueFanOutDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'issue-fan-out-detector';\n\n\tpublic readonly name = IssueFanOutDetector.NAME;\n\n\tprivate readonly _config: IssueFanOutDetectorConfig;\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\t/** Open issues that name a track. Issues without a `trackId` cannot be attributed and are dropped. */\n\tprivate readonly _trackIssues = new Set<ActiveClientIssue>();\n\n\tpublic constructor(\n\t\tprivate readonly _call: ObservedCall,\n\t\tconfig: Partial<IssueFanOutDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tissueTypes: [],\n\t\t\tminReceivers: 3,\n\t\t\taffectedRatioThreshold: 0.6,\n\t\t\treportSingleReceiver: true,\n\t\t\tcooldownMs: 60_000,\n\t\t\t...config,\n\t\t};\n\n\t\tfor (const type of this._config.issueTypes) {\n\t\t\tthis._call.activeIssuesRegistry.addIssueTracker(type, this);\n\t\t}\n\t}\n\n\tpublic get size(): number {\n\t\treturn this._trackIssues.size;\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this._trackIssues.has(issue);\n\t}\n\n\tpublic add(issue: ActiveClientIssue): void {\n\t\t// An issue with no `trackId` says nothing about a published track; holding it would only make\n\t\t// `size` lie about how much there is to do.\n\t\tif (issue.trackId === undefined) return;\n\n\t\tthis._trackIssues.add(issue);\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\treturn this._trackIssues.delete(issue);\n\t}\n\n\tpublic clear(): void {\n\t\tthis._trackIssues.clear();\n\t}\n\n\tpublic update(): void {\n\t\tif (this._trackIssues.size === 0) return;\n\t\t// Without links, \"the receivers of this track\" is unknowable — never guess.\n\t\tif (!this._call.remoteTrackResolver) return;\n\n\t\tconst now = Date.now();\n\t\t// publisher track -> issue type -> the affected receiver client ids.\n\t\tconst byPublisher = new Map<ObservedOutboundTrack, Map<string, Set<string>>>();\n\n\t\tfor (const issue of this._trackIssues) {\n\t\t\tconst publisher = this._publisherOf(issue);\n\n\t\t\tif (!publisher) continue;\n\n\t\t\tlet byType = byPublisher.get(publisher);\n\n\t\t\tif (!byType) {\n\t\t\t\tbyType = new Map();\n\t\t\t\tbyPublisher.set(publisher, byType);\n\t\t\t}\n\n\t\t\tconst clientIds = byType.get(issue.type) ?? new Set<string>();\n\n\t\t\tclientIds.add(issue.clientId);\n\t\t\tbyType.set(issue.type, clientIds);\n\t\t}\n\n\t\tfor (const [ publisher, byType ] of byPublisher) {\n\t\t\tconst numberOfReceivers = publisher.remoteInboundTracks.size;\n\n\t\t\tif (numberOfReceivers === 0) continue;\n\n\t\t\tfor (const [ issueType, clientIds ] of byType) {\n\t\t\t\t// A receiver may hold several issues of one type (one per track); the unit is the client,\n\t\t\t\t// and `clientIds` is already a set, so the ratio can never exceed 1.\n\t\t\t\tconst affectedRatio = clientIds.size / numberOfReceivers;\n\t\t\t\tconst isFanOut = this._config.minReceivers <= numberOfReceivers\n\t\t\t\t\t&& this._config.affectedRatioThreshold <= affectedRatio;\n\t\t\t\tconst isSingle = this._config.reportSingleReceiver\n\t\t\t\t\t&& clientIds.size === 1 && 1 < numberOfReceivers;\n\n\t\t\t\tif (!isFanOut && !isSingle) continue;\n\n\t\t\t\tconst key = `${publisher.id}:${issueType}`;\n\n\t\t\t\tif (now - (this._lastRaisedAt.get(key) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\t\tthis._lastRaisedAt.set(key, now);\n\n\t\t\t\tconst type = isFanOut\n\t\t\t\t\t? IssueFanOutTypes.publishedTrackIssueFanOut\n\t\t\t\t\t: IssueFanOutTypes.singleReceiverIssue;\n\n\t\t\t\t// What the fan-out implies. A track-scoped group is a strong statement: the affected\n\t\t\t\t// clients share a publisher and nothing else, so the receivers are exonerated.\n\t\t\t\tconst conclusion = concludeCallIssue({\n\t\t\t\t\tissueType,\n\t\t\t\t\taffectedClients: clientIds.size,\n\t\t\t\t\ttotalClients: numberOfReceivers,\n\t\t\t\t\tonsetBurst: false,\n\t\t\t\t\tpublishedTrackId: isFanOut ? publisher.id : undefined,\n\t\t\t\t});\n\n\t\t\t\tthis._call.addIssue({\n\t\t\t\t\ttype,\n\t\t\t\t\ttimestamp: now,\n\t\t\t\t\tconclusion,\n\t\t\t\t\tpayload: {\n\t\t\t\t\t\tissueType,\n\t\t\t\t\t\ttrackId: publisher.id,\n\t\t\t\t\t\tkind: publisher.kind,\n\t\t\t\t\t\tpublisherClientId: publisher.getPeerConnection().client.clientId,\n\t\t\t\t\t\t// The corroborating half: if the source's own egress looks fine while its\n\t\t\t\t\t\t// receivers do not, the SFU/forwarding path is implicated rather than the sender.\n\t\t\t\t\t\tpublisherBitrate: publisher.bitrate,\n\t\t\t\t\t\tpublisherDegraded: publisher.degraded,\n\t\t\t\t\t\tpublisherDegradedReasons: publisher.degradedReasons,\n\t\t\t\t\t\treceivers: numberOfReceivers,\n\t\t\t\t\t\taffectedReceivers: clientIds.size,\n\t\t\t\t\t\taffectedRatio,\n\t\t\t\t\t\taffectedClientIds: [ ...clientIds ],\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._call.activeIssuesRegistry.removeIssueTracker(this);\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.clear();\n\t}\n\n\t/**\n\t * Resolve the issue's `trackId` to the outbound track that published it.\n\t *\n\t * Looked up through the reporting client's own peer connections rather than by scanning the call:\n\t * the issue names its client, so the search is bounded by that client's transports (typically one\n\t * or two) instead of by the size of the meeting.\n\t */\n\tprivate _publisherOf(issue: ActiveClientIssue): ObservedOutboundTrack | undefined {\n\t\tconst client = this._call.observedClients.get(issue.clientId);\n\n\t\tif (!client || issue.trackId === undefined) return undefined;\n\n\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\tconst inboundTrack = peerConnection.observedInboundTracks.get(issue.trackId);\n\n\t\t\tif (inboundTrack) return inboundTrack.remoteOutboundTrack;\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { ObservedCall } from '../ObservedCall';\nimport type { ObservedOutboundTrack } from '../ObservedOutboundTrack';\nimport type { ActiveClientIssue } from '../issues/ActiveClientIssue';\nimport type { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\n\nexport const PublisherFaultTypes = {\n\t/**\n\t * A publisher is reporting trouble on its own send path **while** its subscribers report trouble\n\t * receiving it. Both ends agree, so the source is implicated rather than inferred.\n\t */\n\tcorroboratedPublisherFault: 'CORROBORATED_PUBLISHER_FAULT',\n} as const;\n\nexport type PublisherFaultCorroborationDetectorConfig = {\n\n\t/**\n\t * Issue types raised by the **publishing** client about its own outbound path. **Required.**\n\t *\n\t * The natural set from client-monitor-js: `encoder-bottleneck`, `capture-bottleneck`,\n\t * `dry-outbound-track`. All three mean \"I am failing to produce or send this properly\", which is\n\t * the half of the story the receivers cannot see.\n\t */\n\tpublisherIssueTypes: string[];\n\n\t/**\n\t * Issue types raised by the **subscribing** clients about the track they receive. **Required.**\n\t *\n\t * The natural set: `freezed-video-track`, `dry-inbound-track`, `video-recovery-failed`. These say\n\t * \"I am not getting this properly\", which is the half the publisher cannot see.\n\t */\n\treceiverIssueTypes: string[];\n\n\t/**\n\t * Subscribers of the track that must be complaining at the same time. Default `2`.\n\t *\n\t * `1` still yields a genuine two-sided corroboration — publisher and one receiver agreeing is\n\t * already more than either says alone — but `2` rules out the case where a single receiver's own\n\t * downlink is at fault and merely coincides with the publisher's complaint. Sensible range `1`–`3`;\n\t * higher mostly costs you findings in small calls, where a track may only have two subscribers.\n\t */\n\tminAffectedReceivers: number;\n\n\t/**\n\t * Re-arm time per published track (ms). Default `60_000`.\n\t *\n\t * Typical `30_000`–`300_000`. This detector raises the highest-confidence finding in the library,\n\t * so it is the one you least want repeating every tick.\n\t */\n\tcooldownMs: number;\n};\n\n/** The two-sided evidence behind one finding. */\nexport type CorroboratedPublisherFault = {\n\ttrackId: string;\n\tkind: string;\n\tpublisherClientId: string;\n\n\t/** The publisher's own open issue types on this track. */\n\tpublisherIssueTypes: string[];\n\n\t/** The receiver-side open issue types across this track's subscribers. */\n\treceiverIssueTypes: string[];\n\n\treceivers: number;\n\taffectedReceivers: number;\n\taffectedClientIds: string[];\n\tpublisherBitrate?: number;\n};\n\n/**\n * Fires only when **both ends of one published track are complaining at the same time**: the\n * publisher about its own send path, and its subscribers about receiving it.\n *\n * ### How this differs from `IssueFanOutDetector`\n *\n * Fan-out sees one end. It observes that most of Alice's subscribers are unhappy and *infers* that\n * the fault is on Alice's side, because the affected clients share a publisher and nothing else. That\n * inference is sound, and it is still a inference: the same observation is produced by the SFU\n * mangling Alice's stream on the way out, with Alice herself perfectly healthy.\n *\n * This detector removes the inference. When Alice reports `encoder-bottleneck` *and* four of her six\n * subscribers report `freezed-video-track` in the same window, there is nothing left to deduce — the\n * source said it was struggling and the receivers confirmed the consequence. That is the strongest\n * statement this library can make about where a fault sits, and it is only available to something\n * holding both ends at once. Neither the publisher nor any receiver can reach this conclusion alone.\n *\n * Run both: fan-out is broader and catches the SFU-forwarding case where the publisher is fine;\n * this one is narrower and, when it fires, needs no interpretation.\n *\n * ### Silence here is not health\n *\n * A quiet detector means only that the two halves have not coincided — most commonly because the\n * publisher is genuinely fine and the fault is in forwarding, which is exactly the case `fan-out`\n * exists to report. Do not read \"no corroborated fault\" as \"no publisher-side problem\".\n *\n * ```ts\n * observedCall.addDetector('publisher-fault-corroboration-detector', {\n * publisherIssueTypes: [ 'encoder-bottleneck', 'capture-bottleneck', 'dry-outbound-track' ],\n * receiverIssueTypes: [ 'freezed-video-track', 'dry-inbound-track' ],\n * });\n * ```\n *\n * ### Requires a `RemoteTrackResolver`\n *\n * Matching a publisher's issue to its subscribers' issues needs the publisher↔subscriber links. With\n * no resolver the detector does nothing rather than guessing.\n */\nexport class PublisherFaultCorroborationDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'publisher-fault-corroboration-detector' as const;\n\n\tpublic readonly name = PublisherFaultCorroborationDetector.NAME;\n\n\tprivate readonly _config: PublisherFaultCorroborationDetectorConfig;\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\t/** Open publisher-side issues that name a track. */\n\tprivate readonly _publisherIssues = new Set<ActiveClientIssue>();\n\n\t/** Open receiver-side issues that name a track. */\n\tprivate readonly _receiverIssues = new Set<ActiveClientIssue>();\n\n\t/** The faults corroborated on the most recent `update()`. Exposed for tests/dashboards. */\n\tpublic lastFaults: CorroboratedPublisherFault[] = [];\n\n\tpublic constructor(\n\t\tprivate readonly _call: ObservedCall,\n\t\tconfig: Partial<PublisherFaultCorroborationDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tpublisherIssueTypes: [],\n\t\t\treceiverIssueTypes: [],\n\t\t\tminAffectedReceivers: 2,\n\t\t\tcooldownMs: 60_000,\n\t\t\t...config,\n\t\t};\n\n\t\tfor (const type of this._config.publisherIssueTypes) {\n\t\t\tthis._call.activeIssuesRegistry.addIssueTracker(type, this);\n\t\t}\n\t\tfor (const type of this._config.receiverIssueTypes) {\n\t\t\tthis._call.activeIssuesRegistry.addIssueTracker(type, this);\n\t\t}\n\t}\n\n\tpublic get size(): number {\n\t\treturn this._publisherIssues.size + this._receiverIssues.size;\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this._publisherIssues.has(issue) || this._receiverIssues.has(issue);\n\t}\n\n\tpublic add(issue: ActiveClientIssue): void {\n\t\t// Without a `trackId` the issue cannot be attached to either end of a specific stream.\n\t\tif (issue.trackId === undefined) return;\n\n\t\tif (this._config.publisherIssueTypes.includes(issue.type)) this._publisherIssues.add(issue);\n\t\telse if (this._config.receiverIssueTypes.includes(issue.type)) this._receiverIssues.add(issue);\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\treturn this._publisherIssues.delete(issue) || this._receiverIssues.delete(issue);\n\t}\n\n\tpublic clear(): void {\n\t\tthis._publisherIssues.clear();\n\t\tthis._receiverIssues.clear();\n\t\tthis.lastFaults = [];\n\t}\n\n\tpublic update(): void {\n\t\tthis.lastFaults = [];\n\n\t\t// Corroboration needs both halves. If either side is silent there is nothing to correlate, and\n\t\t// this is the common case — so it costs two `size` checks.\n\t\tif (this._publisherIssues.size === 0 || this._receiverIssues.size === 0) return;\n\t\tif (!this._call.remoteTrackResolver) return;\n\n\t\tconst now = Date.now();\n\t\t// Start from the publisher side: it is the smaller set (one client per track, versus one per\n\t\t// subscriber) and it is the side that has to be present for a finding to exist at all.\n\t\tconst suspects = new Map<ObservedOutboundTrack, Set<string>>();\n\n\t\tfor (const issue of this._publisherIssues) {\n\t\t\tconst publisher = this._outboundTrackOf(issue);\n\n\t\t\tif (!publisher) continue;\n\n\t\t\tconst types = suspects.get(publisher) ?? new Set<string>();\n\n\t\t\ttypes.add(issue.type);\n\t\t\tsuspects.set(publisher, types);\n\t\t}\n\n\t\tif (suspects.size === 0) return;\n\n\t\t// Index the receiver complaints by the inbound track they name, so each suspect's subscriber\n\t\t// set can be checked with a lookup per subscriber instead of a scan of every open issue.\n\t\tconst receiverIssuesByTrackId = new Map<string, ActiveClientIssue[]>();\n\n\t\tfor (const issue of this._receiverIssues) {\n\t\t\tconst existing = receiverIssuesByTrackId.get(issue.trackId as string);\n\n\t\t\tif (existing) existing.push(issue);\n\t\t\telse receiverIssuesByTrackId.set(issue.trackId as string, [ issue ]);\n\t\t}\n\n\t\tfor (const [ publisher, publisherIssueTypes ] of suspects) {\n\t\t\tconst receivers = publisher.remoteInboundTracks;\n\n\t\t\tif (receivers.size === 0) continue;\n\n\t\t\tconst affectedClientIds = new Set<string>();\n\t\t\tconst receiverIssueTypes = new Set<string>();\n\n\t\t\tfor (const receiver of receivers) {\n\t\t\t\tconst issues = receiverIssuesByTrackId.get(receiver.id);\n\n\t\t\t\tif (!issues) continue;\n\n\t\t\t\tfor (const issue of issues) {\n\t\t\t\t\taffectedClientIds.add(issue.clientId);\n\t\t\t\t\treceiverIssueTypes.add(issue.type);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (affectedClientIds.size < this._config.minAffectedReceivers) continue;\n\n\t\t\tconst fault: CorroboratedPublisherFault = {\n\t\t\t\ttrackId: publisher.id,\n\t\t\t\tkind: publisher.kind,\n\t\t\t\tpublisherClientId: publisher.getPeerConnection().client.clientId,\n\t\t\t\tpublisherIssueTypes: [ ...publisherIssueTypes ],\n\t\t\t\treceiverIssueTypes: [ ...receiverIssueTypes ],\n\t\t\t\treceivers: receivers.size,\n\t\t\t\taffectedReceivers: affectedClientIds.size,\n\t\t\t\taffectedClientIds: [ ...affectedClientIds ],\n\t\t\t\tpublisherBitrate: publisher.bitrate,\n\t\t\t};\n\n\t\t\tthis.lastFaults.push(fault);\n\n\t\t\tif (now - (this._lastRaisedAt.get(publisher.id) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(publisher.id, now);\n\n\t\t\tthis._call.addIssue({\n\t\t\t\ttype: PublisherFaultTypes.corroboratedPublisherFault,\n\t\t\t\ttimestamp: now,\n\t\t\t\tconclusion: {\n\t\t\t\t\tfaultDomain: 'published-track',\n\t\t\t\t\tsummary: `${fault.publisherClientId} reports ${fault.publisherIssueTypes.join(', ')} on track ${fault.trackId} while ${fault.affectedReceivers} of ${fault.receivers} subscribers report ${fault.receiverIssueTypes.join(', ')} — both ends agree`,\n\t\t\t\t\trecommendation: 'the source is implicated, not inferred: check that publisher\\'s capture, encoder and uplink before looking at the SFU or the receivers',\n\t\t\t\t\t// Higher than any single-ended finding: two independent parties, one conclusion.\n\t\t\t\t\tconfidence: 0.9,\n\t\t\t\t},\n\t\t\t\tpayload: { ...fault },\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._call.activeIssuesRegistry.removeIssueTracker(this);\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.clear();\n\t}\n\n\t/**\n\t * Resolve a publisher-side issue's `trackId` to the outbound track it is about.\n\t *\n\t * Looked up through the reporting client's own peer connections: the issue names its client, so\n\t * the search is bounded by that client's transports rather than by the size of the call.\n\t */\n\tprivate _outboundTrackOf(issue: ActiveClientIssue): ObservedOutboundTrack | undefined {\n\t\tconst client = this._call.observedClients.get(issue.clientId);\n\n\t\tif (!client || issue.trackId === undefined) return undefined;\n\n\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\tconst outboundTrack = peerConnection.observedOutboundTracks.get(issue.trackId);\n\n\t\t\tif (outboundTrack) return outboundTrack;\n\t\t}\n\n\t\treturn undefined;\n\t}\n}\n","import { createLogger } from './common/logger';\nimport { ObservedCall, ObservedCallSettings } from './ObservedCall';\nimport { EventEmitter } from 'events';\nimport { ClientSample } from './schema/ClientSample';\nimport { ObservedTURN } from './ObservedTURN';\nimport type { RemoteTrackResolverFactory } from './resolvers/RemoteTrackResolver';\nimport type { ObserverEvents, ObserverEventBase, ObservedMediasoupRouterScope } from './ObserverEvents';\nimport type { ClientSampleSinkFactory } from './sinks/ClientSampleSink';\nimport { Middleware, MiddlewareProcessor } from './common/Middleware';\nimport { ObservedMediasoupRouter, ObservedMediasoupRouterSettings } from './ObservedMediasoupRouter';\nimport { AvailableCallScopeDetectorsConfigs, AvailableObserverScopeDetectorsConfigs, Detectors } from './detectors/Detectors';\nimport type { AvailableValidatorConfigs } from './validators/Validators';\nimport type { ValidationReport, RunningValidator } from './validators/Validator';\nimport type { ObserverIssue } from './common/Issue';\nimport { ActiveIssuesRegistry } from './issues/ActiveIssuesRegistry';\nimport { CallSummaryCollector } from './summaries/CallSummaryCollector';\nimport { defaultCallSummaryConfig, type CallSummaryConfig } from './summaries/CallSummary';\nimport type { Detector } from './detectors/Detector';\nimport { SfuCongestionDetector } from './detectors/SfuCongestionDetector';\nimport { ObserverConcurrentIssueDetector } from './detectors/ObserverConcurrentIssueDetector';\nimport { ClientPopulationIssueDetector } from './detectors/ClientPopulationIssueDetector';\nimport { TurnServerHealthDetector } from './detectors/TurnServerHealthDetector';\nimport { TurnServerOutageDetector } from './detectors/TurnServerOutageDetector';\nimport { SimulcastReceiverValidator } from './validators/SimulcastReceiverValidator';\nimport { RemoteTrackResolverValidator } from './validators/RemoteTrackResolverValidator';\nimport { CodecConsistencyValidator } from './validators/CodecConsistencyValidator';\n\nconst logger = createLogger('Observer');\n\nexport type SampleRejectedReason = 'observer-closed' | 'missing-callId' | 'missing-clientId';\n\nexport type { ObserverEvents } from './ObserverEvents';\n\n/**\n * Optional, free-form context supplied to `accept()`. A single context object is\n * threaded down the accept chain (Observer -> Client -> PeerConnection) and is\n * merged into the `appData` of entities created during the accept pass.\n */\nexport type AcceptContext = Record<string, unknown>;\n\n/** The payload threaded through `accept()` middlewares: the sample and its optional context. */\nexport type AcceptMiddlewarePayload = {\n\tsample: ClientSample,\n\tcontext?: AcceptContext,\n};\n\n/**\n * A global middleware run on every sample passed to `observer.accept()`, in registration order,\n * **before** the sample is dispatched to any call/client. It may inspect or mutate the sample\n * (e.g. set/normalize `callId`/`clientId`, enrich, redact) or the context, then call\n * `next(payload)` to continue the chain. Not calling `next` **drops** the sample.\n */\nexport type AcceptMiddleware = Middleware<AcceptMiddlewarePayload>;\n\n/** Produces the initial `appData` for a call created without an explicit `appData`. */\nexport type CallAppDataFactory = (params: { callId: string, observer: Observer, acceptCtx?: AcceptContext }) => Record<string, unknown>;\n\n/** Produces the initial `appData` for a client created without an explicit `appData`. */\nexport type ClientAppDataFactory = (params: { clientId: string, observedCall: ObservedCall, acceptCtx?: AcceptContext }) => Record<string, unknown>;\n\nexport type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = {\n\n\t/**\n\t * Application-owned data for the observer itself. Never read or modified by the library.\n\t *\n\t * For per-call / per-client data prefer `createCallAppData` / `createClientAppData`, which run at\n\t * creation time and can see the `accept()` context.\n\t */\n\tappData?: AppData,\n\n\t/**\n\t * Close a client that has not produced a sample for this long (ms). Default `60_000`.\n\t *\n\t * This is the **liveness timeout for a participant**, so set it from your client's sampling\n\t * period, not from taste: a client sampling every 5 s needs several missed samples to look gone.\n\t * Sensible range `3x`–`10x` the sampling period; `60_000` suits the usual 2–10 s collectors.\n\t *\n\t * Too low and a client that merely paused (tab backgrounded, brief network drop) is closed and\n\t * then re-created as a *new* client, which restarts its detectors and splits one participant into\n\t * two in any summary. Too high and left participants linger, inflating `peak`, the denominators of\n\t * every ratio-based detector, and memory. `undefined` disables the timeout — then nothing closes\n\t * an abandoned client but your own `client.close()`.\n\t */\n\tcloseClientIfIdleForMs?: number,\n\n\t/**\n\t * Close a call once it has had zero clients for this long (ms). Default `60_000`.\n\t *\n\t * The grace period exists so a brief empty moment — everyone reconnecting after a network blip,\n\t * the last participant refreshing — does not end the call and start a new one under the same\n\t * `callId`. Sensible range `10_000`–`300_000`.\n\t *\n\t * Too low splits one meeting into several calls, and each split emits its own `call-summary`. Too\n\t * high keeps dead calls in `observedCalls`, holding their detectors and summaries in memory.\n\t * `undefined` disables it: the call then lives until you call `call.close()`.\n\t */\n\tcloseCallIfEmptyForMs?: number,\n\n\t/**\n\t * When `true` (the default), every call update triggers an observer-wide `update()` pass.\n\t *\n\t * There is deliberately no timer and no separate policy object: a call is updated when any of its\n\t * clients is, and the observer is updated when any of its calls is — so the observer is updated\n\t * exactly when any client anywhere is. Set to `false` only if you drive `observer.update()`\n\t * yourself, and note that observer-scoped detectors and validators run *nowhere else*.\n\t */\n\tautoUpdateOnCallUpdate?: boolean;\n\n\t/**\n\t * Accumulate a {@link CallSummary} on every call this observer creates.\n\t *\n\t * **Absent or `null` means no summaries at all** — no accumulation, and not one bus subscription.\n\t * Pass an object (`{}` is valid) to switch it on; anything you leave out takes its default from\n\t * `defaultCallSummaryConfig`, including `include: []`, which collects *no* built-in section. A\n\t * summary that only runs `enrich` is a perfectly good summary.\n\t *\n\t * ```ts\n\t * const observer = new Observer({\n\t * callSummary: {\n\t * include: [ 'clients', 'issues' ],\n\t * enrich: {\n\t * 'client-joined': (summary, { observedClient }) => {\n\t * ((summary.attachments.regions ??= []) as string[]).push(String(observedClient.appData.region));\n\t * },\n\t * },\n\t * },\n\t * });\n\t *\n\t * observer.on('call-summary', ({ summary }) => archive(summary));\n\t * ```\n\t *\n\t * This is construction-time and fixed for the observer's life, unlike detectors, which are added\n\t * per call as an application decides what to watch. A summary is a record of what happened, and a\n\t * record you can turn on halfway through is a record with a hole in it — calls that started\n\t * earlier would carry different sections from calls that started later, with nothing on either to\n\t * say which. One shape for every call, or none.\n\t */\n\tcallSummary?: Partial<CallSummaryConfig> | null;\n\n\t/**\n\t * Thresholds that mark a **received** track as degraded. Omitted (the default) means no track is\n\t * ever marked degraded — `inboundTrack.degraded` stays `false` and `degradationReasons` stays\n\t * empty, so this is opt-in and absence is *not* a clean bill of health.\n\t *\n\t * Each field is an **exclusive upper bound**: the reason is added when the measured value is\n\t * strictly greater. All are evaluated on every update and any number can fire at once;\n\t * `degradationReasons` lists the ones that did.\n\t *\n\t * These feed `CallHealthAggregator` and `outboundTrack.degradedRatio` (how many of a publisher's\n\t * receivers are unhappy), which is what `TrackDeliveryMismatchDetector` and\n\t * `PublisherFaultCorroborationDetector` read. They are **not** a substitute for client issues:\n\t * client-monitor already decides \"this endpoint is in trouble\" with hysteresis and multi-signal\n\t * confirmation. Treat these as a coarse per-track flag for cross-participant comparison, and keep\n\t * them loose enough that a single bad tick does not trip them.\n\t */\n\tinboundTrackDegradationThresholds?: {\n\n\t\t/**\n\t\t * Freezes counted **in one sampling period**, not since the start of the call. `1` means \"any\n\t\t * freeze at all this tick\", which is strict; `2`–`3` tolerates the odd frame hiccup.\n\t\t * Reason: `'freezes'`.\n\t\t */\n\t\tdeltaFreezeCount: number,\n\n\t\t/**\n\t\t * Fraction of frames dropped, `0`–`1`. Typical `0.05`–`0.2`; below ~`0.02` you are inside\n\t\t * normal jitter for most decoders. Reason: `'frames-dropped'`.\n\t\t */\n\t\tframesDroppedRatio: number,\n\n\t\t/**\n\t\t * Jitter buffer delay in **ms**. Typical `150`–`500`: audio stays intelligible well past\n\t\t * `200`, while conversation turn-taking suffers beyond ~`400`. Reason:\n\t\t * `'jitter-buffer-delay'`.\n\t\t */\n\t\tjitterBufferDelayInMs: number,\n\n\t\t/**\n\t\t * Concealed (synthesised) audio samples as a fraction, `0`–`1`. Typical `0.05`–`0.15`; above\n\t\t * ~`0.1` is usually audible as robotic or clipped speech. Reason: `'concealment'`.\n\t\t */\n\t\tconcealmentRatio: number,\n\n\t\t/**\n\t\t * Round-trip time in **ms**, as reported for this inbound stream. Typical `250`–`500`.\n\t\t * Remember this is absolute, not relative to the participant's own baseline — a genuinely\n\t\t * distant participant will sit permanently above any fixed bound, which is why \"RTT got\n\t\t * worse\" belongs to client-monitor and not here. Reason: `'rtt'`.\n\t\t */\n\t\trttInMs: number,\n\t},\n\n\t/**\n\t * Thresholds that mark a **published** track as degraded, from what the receivers report back.\n\t * Omitted (the default) means neither threshold-based reason can fire.\n\t *\n\t * Note two reasons are added regardless of this setting, because they need no threshold:\n\t * `quality-limited-<reason>` when the encoder reports a `qualityLimitationReason`, and\n\t * `no-packets-sent` when an unmuted track sent nothing in a sampling period.\n\t *\n\t * Also an **exclusive upper bound** per field, evaluated on every update.\n\t */\n\toutboundTrackDegradationThresholds?: {\n\n\t\t/**\n\t\t * Fraction of packets lost as reported by the remote end, `0`–`1`. Typical `0.02`–`0.1`;\n\t\t * anything under ~`0.01` is normal on the open internet and will fire constantly. Reason:\n\t\t * `'remote-fraction-lost'`.\n\t\t */\n\t\tfractionLost: number,\n\n\t\t/**\n\t\t * Round-trip time in **ms** as reported by the remote end. Typical `250`–`500`, and absolute\n\t\t * rather than baseline-relative — see the note on the inbound `rttInMs`. Reason:\n\t\t * `'remote-rtt'`.\n\t\t */\n\t\trttInMs: number,\n\t},\n\n\t/**\n\t * Optional factory invoked when a call is created without an explicit `appData`\n\t * (e.g. lazily by `accept()`), so apps can enrich appData without pre-creating the\n\t * entity. `appData` is application-owned; it is never modified by the `accept()` context.\n\t */\n\tcreateCallAppData?: CallAppDataFactory,\n\n\t/** Same as `createCallAppData`, for clients. Receives the (already-created) parent call. */\n\tcreateClientAppData?: ClientAppDataFactory,\n\n\t/**\n\t * Optional factory invoked when a client is created, producing a per-client sink that\n\t * receives every sample the client accepts (or `undefined` for no sink). The destination\n\t * can be derived from `callId` / `clientId`.\n\t */\n\tcreateClientSink?: ClientSampleSinkFactory,\n\n\t/**\n\t * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`\n\t * (or `undefined` for none). Use the built-ins\n\t * (`createDefaultMediasoupRemoteTrackResolverFactory()` / `createP2pRemoteTrackResolverFactory()`)\n\t * or build a `RemoteTrackResolver` with custom publisher/subscriber id resolvers.\n\t */\n\tcreateRemoteTrackResolver?: RemoteTrackResolverFactory,\n\n}\n\nexport declare interface Observer {\n\ton<U extends keyof ObserverEvents>(event: U, listener: (...args: ObserverEvents[U]) => void): this;\n\toff<U extends keyof ObserverEvents>(event: U, listener: (...args: ObserverEvents[U]) => void): this;\n\tonce<U extends keyof ObserverEvents>(event: U, listener: (...args: ObserverEvents[U]) => void): this;\n\temit<U extends keyof ObserverEvents>(event: U, ...args: ObserverEvents[U]): boolean;\n}\n\nexport class Observer<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter {\n\tpublic readonly observedTURN = new ObservedTURN();\n\tpublic readonly observedCalls = new Map<string, ObservedCall>();\n\tpublic readonly observedMediasoupRouters = new Map<string, ObservedMediasoupRouter>();\n\n\t/** Ancestry base shared by all Observer-bus events originating at the observer. */\n\tpublic readonly eventScope: ObserverEventBase = { observer: this };\n\tpublic readonly config: ObserverConfig<AppData>;\n\n\tpublic closed = false;\n\n\tpublic totalAddedCall = 0;\n\tpublic totalRemovedCall = 0;\n\n\tpublic numberOfClientsUsingTurn = 0;\n\tpublic numberOfClients = 0;\n\tpublic numberOfInboundRtpStreams = 0;\n\tpublic numberOfOutboundRtpStreams = 0;\n\tpublic numberOfDataChannels = 0;\n\tpublic numberOfPeerConnections = 0;\n\n\t/** Global, pre-dispatch middleware chain run on every accepted sample. */\n\tpublic readonly acceptMiddlewares = new MiddlewareProcessor<AcceptMiddlewarePayload>();\n\n\t/**\n\t * Fleet-wide index of every open client issue, maintained incrementally as issues open and close.\n\t * Each call's index propagates into this one, so cross-call queries cost O(matching issues) rather\n\t * than a walk over every call and client. This is what observer-scoped detectors read.\n\t */\n\tpublic readonly activeIssuesRegistry = new ActiveIssuesRegistry();\n\n\t/**\n\t * Validators currently running. Each removes itself when it finishes, so this is normally empty —\n\t * a validator is a one-shot check, not a permanent fixture. Start one with {@link addValidator}.\n\t */\n\tpublic readonly validators = new Set<RunningValidator>();\n\n\t/**\n\t * Observer-scoped detector registry, run on every `observer.update()` — the place for findings\n\t * that span **calls**, e.g. \"many calls on the same SFU degraded at once\". Detectors raise\n\t * findings with `observer.addIssue(...)`, surfaced on the bus as `observer-issue`.\n\t * (For findings within a single call use `observedCall.detectors`.)\n\t *\n\t * Starts **empty**. Populate it with {@link addObserverDetector}, or `detectors.add(...)` for an\n\t * instance you built yourself.\n\t */\n\tpublic readonly detectors = new Detectors();\n\n\t/**\n\t * The call-scoped detectors to build on **every** call this observer creates, in registration\n\t * order. Written by {@link addCallDetector}; read by `createObservedCall`.\n\t *\n\t * Nothing is created implicitly. There is no detector configuration in `ObserverConfig` and no\n\t * default set, because a detector that nobody asked for is a detector nobody will act on: it costs\n\t * time on every tick and raises findings into a handler that was not written to expect them. An\n\t * application says what it wants to watch, or it watches nothing.\n\t *\n\t * ```ts\n\t * observer.addCallDetector('call-concurrent-issue-detector', {\n\t * issueTypes: [ 'congestion', 'ice-disconnected' ],\n\t * });\n\t * ```\n\t */\n\tpublic readonly callDetectorConfigs = new Map<\n\tkeyof AvailableCallScopeDetectorsConfigs,\n\tPartial<AvailableCallScopeDetectorsConfigs[keyof AvailableCallScopeDetectorsConfigs]>\n\t>();\n\n\t/**\n\t * Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep\n\t * the summaries current (one per event type, not one per call), and the summaries themselves.\n\t *\n\t * `undefined` when `config.callSummary` was absent or `null` — so its presence *is* the answer to\n\t * \"are summaries on\", and nothing is subscribed to anything.\n\t */\n\tpublic readonly callSummaryCollector?: CallSummaryCollector;\n\n\tpublic constructor(config: Partial<ObserverConfig<AppData>> = {}) {\n\t\tsuper();\n\t\tthis.setMaxListeners(Infinity);\n\t\tthis.update = this.update.bind(this);\n\n\t\tthis.config = {\n\t\t\tautoUpdateOnCallUpdate: true,\n\t\t\tappData: {} as AppData,\n\t\t\tcloseCallIfEmptyForMs: 60_000,\n\t\t\tcloseClientIfIdleForMs: 60_000,\n\t\t\t...config,\n\t\t};\n\n\t\tif (this.config.callSummary) {\n\t\t\t// The defaults are resolved once, here, and the collector is the only thing that holds the\n\t\t\t// result — there is no second copy on the observer to drift from `config`. One collector\n\t\t\t// holds one subscription per event type it needs, not one per call; see\n\t\t\t// `CallSummaryCollector` for why the per-call alternative is quadratic in concurrent calls.\n\t\t\tthis.callSummaryCollector = new CallSummaryCollector(this, {\n\t\t\t\t...defaultCallSummaryConfig,\n\t\t\t\t...this.config.callSummary,\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic get numberOfCalls() {\n\t\treturn this.observedCalls.size;\n\t}\n\n\tpublic get appData() {\n\t\treturn this.config.appData;\n\t}\n\n\t/**\n\t * Build a cross-call detector onto `observer.detectors`. Chainable.\n\t *\n\t * To get a handle on what was built — to inspect it, or to remove that exact instance later — read\n\t * it back off the registry: `observer.detectors.getAll(name)`, or `observer.detectors.instances`.\n\t */\n\tpublic addObserverDetector<K extends keyof AvailableObserverScopeDetectorsConfigs>(name: K, config: Partial<AvailableObserverScopeDetectorsConfigs[K]> = {}): this {\n\t\tif (this.closed) return this;\n\n\t\tlet detector: Detector | undefined;\n\n\t\tswitch (name) {\n\t\t\tcase SfuCongestionDetector.NAME: {\n\t\t\t\tdetector = new SfuCongestionDetector(this, config);\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ObserverConcurrentIssueDetector.NAME: {\n\t\t\t\tdetector = new ObserverConcurrentIssueDetector(this, config);\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase ClientPopulationIssueDetector.NAME: {\n\t\t\t\tdetector = new ClientPopulationIssueDetector(this, config);\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase TurnServerHealthDetector.NAME: {\n\t\t\t\tdetector = new TurnServerHealthDetector(this, config);\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase TurnServerOutageDetector.NAME: {\n\t\t\t\tdetector = new TurnServerOutageDetector(this, config);\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tlogger.warn('Unknown detector name %s; skipping', name);\n\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\t\tthis.detectors.add(detector);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Enable a call-scoped detector for calls created **from now on**.\n\t *\n\t * This edits the config, not the live calls: calls already open keep the detector set they were\n\t * built with. To add one to an existing call, use `observedCall.addDetector(...)` directly.\n\t */\n\tpublic addCallDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config: Partial<AvailableCallScopeDetectorsConfigs[K]> = {}): this {\n\t\tif (this.closed) return this;\n\n\t\tthis.callDetectorConfigs.set(name, config);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Remove an observer-scoped detector by name, returning how many were removed.\n\t *\n\t * **Every** instance registered under the name goes, since a name can legitimately be registered\n\t * more than once (`ClientPopulationIssueDetector` is meant to be added once per `groupBy` axis).\n\t * When you want one of them specifically, go through the registry, which deals in instances:\n\t *\n\t * ```ts\n\t * const [ byBrowser, byOs ] = observer.detectors.getAll('client-population-issue-detector');\n\t *\n\t * observer.detectors.remove(byOs); // keeps the browser axis running\n\t * ```\n\t *\n\t * Either route `close()`s the detector, so it unsubscribes from the issue registry and drops any\n\t * timers or bus listeners it held.\n\t */\n\tpublic removeObserverDetector(name: keyof AvailableObserverScopeDetectorsConfigs): number {\n\t\treturn this.detectors.removeByName(name);\n\t}\n\n\t/**\n\t * Stop building `name` on calls created from now on.\n\t *\n\t * By default this also removes it from the calls **already open**, so that \"remove this detector\"\n\t * means the same thing whether you say it before or after a call started — the alternative leaves\n\t * a fleet where the detector is live on some calls and not others, decided by join time. Pass\n\t * `{ includeOpenCalls: false }` to change only what future calls are built with.\n\t *\n\t * Returns the number of live detector instances removed (`0` when only the config changed).\n\t */\n\tpublic removeCallDetector(\n\t\tname: keyof AvailableCallScopeDetectorsConfigs,\n\t\t{ includeOpenCalls = true }: { includeOpenCalls?: boolean } = {},\n\t): number {\n\t\tthis.callDetectorConfigs.delete(name);\n\n\t\tif (!includeOpenCalls) return 0;\n\n\t\tlet removed = 0;\n\n\t\tfor (const call of this.observedCalls.values()) removed += call.removeDetector(name);\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Start a structural check. It runs on each `observer.update()` until it can decide, reports once\n\t * on `validation-ready`, and removes itself.\n\t *\n\t * ```ts\n\t * observer.validate('simulcast-receiver-validator', { minChecks: 5 });\n\t * ```\n\t *\n\t * Config keys are optional and merged over that validator's defaults. Call it again — after a\n\t * deploy, say — to check again; there is no revalidation timer, because a deploy rather than\n\t * elapsed time is what makes a structural verdict stale.\n\t */\n\tpublic addValidator<K extends keyof AvailableValidatorConfigs>(name: K, config: Partial<AvailableValidatorConfigs[K]> = {}): this {\n\t\tif (this.closed) return this;\n\n\t\tlet validator: RunningValidator | undefined;\n\t\tconst onDone = (report: ValidationReport) => {\n\t\t\tif (validator) this.validators.delete(validator);\n\t\t\tthis._notify('validation-ready', { ...this.eventScope, report, validator: name });\n\t\t};\n\n\t\tswitch (name) {\n\t\t\tcase SimulcastReceiverValidator.NAME:\n\t\t\t\tvalidator = new SimulcastReceiverValidator(this, onDone, config);\n\t\t\t\tbreak;\n\t\t\tcase RemoteTrackResolverValidator.NAME:\n\t\t\t\tvalidator = new RemoteTrackResolverValidator(this, onDone, config);\n\t\t\t\tbreak;\n\t\t\tcase CodecConsistencyValidator.NAME:\n\t\t\t\tvalidator = new CodecConsistencyValidator(this, onDone, config);\n\t\t\t\tbreak;\n\t\t}\n\n\t\tif (!validator) {\n\t\t\tlogger.warn('Unknown validator name %s; skipping', name);\n\n\t\t\treturn this;\n\t\t}\n\n\t\tthis.validators.add(validator);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Stop a running validation, by name or by instance. Returns how many were cancelled.\n\t *\n\t * Cancelling is **not** silent discarding. The validator finishes with `inconclusive` and the given\n\t * `reason`, emits `validation-ready` like any other completion, and removes itself. That matters\n\t * because anything waiting on the verdict — a deploy gate, a dashboard, a promise — would otherwise\n\t * wait forever, and because \"we stopped asking\" is a materially different outcome from \"we asked\n\t * and learned nothing\", which is exactly what `inconclusive` with a reason records.\n\t *\n\t * ```ts\n\t * observer.cancelValidator('simulcast-receivers', 'sfu redeployed');\n\t *\n\t * // or one specific instance — `observer.validators` holds what is running\n\t * for (const validator of observer.validators) observer.cancelValidator(validator, 'shutting down');\n\t * ```\n\t *\n\t * Pass a real reason. The default tells the reader nothing they could not already infer.\n\t */\n\tpublic cancelValidator(target: keyof AvailableValidatorConfigs | RunningValidator, reason = 'cancelled'): number {\n\t\t// Copy first: `cancel()` finishes the validator, whose `onDone` deletes it from this very set.\n\t\tconst running = [ ...this.validators ];\n\t\tconst matching = typeof target === 'string'\n\t\t\t? running.filter((validator) => validator.name === target)\n\t\t\t: running.filter((validator) => validator === target);\n\n\t\tfor (const validator of matching) {\n\t\t\ttry {\n\t\t\t\tvalidator.cancel(reason);\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn('Error cancelling validator %s: %o', validator.name, err);\n\t\t\t}\n\t\t}\n\n\t\treturn matching.length;\n\t}\n\n\tpublic getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined {\n\t\tif (this.closed || !this.observedCalls.has(callId)) return;\n\n\t\treturn this.observedCalls.get(callId) as ObservedCall<T>;\n\t}\n\n\t/**\n\t * @param acceptCtx the `accept()` context, when this call is being created to receive a sample.\n\t * Passed on to `ObserverConfig.createCallAppData`, so the factory can read whatever the caller (or\n\t * an accept middleware) put there — a tenant, a region, a trace id.\n\t */\n\tpublic createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(\n\t\tsettings: ObservedCallSettings<T>, acceptCtx?: AcceptContext\n\t): ObservedCall<T> | undefined {\n\t\tif (this.closed) {\n\t\t\tlogger.warn('Attempted to create a call (callId: %s) on a closed observer', settings.callId);\n\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.observedCalls.has(settings.callId)) {\n\t\t\tlogger.warn('Observed Call with id %s already exists; returning the existing instance', settings.callId);\n\n\t\t\treturn this.observedCalls.get(settings.callId) as ObservedCall<T>;\n\t\t}\n\n\t\t// Apply observer-level defaults without mutating the caller's (discriminated-union) settings object.\n\t\tconst callSettings = {\n\t\t\t...settings,\n\t\t\tcloseCallIfEmptyForMs: settings.closeCallIfEmptyForMs ?? this.config.closeCallIfEmptyForMs,\n\t\t\tappData: settings.appData ?? this.config.createCallAppData?.({ callId: settings.callId, observer: this, acceptCtx }),\n\t\t} as ObservedCallSettings<T>;\n\n\t\tconst callActiveIssuesRegistry = new ActiveIssuesRegistry(this.activeIssuesRegistry);\n\t\tconst observedCall = new ObservedCall(\n\t\t\tcallSettings,\n\t\t\tthis,\n\t\t\tcallActiveIssuesRegistry\n\t\t);\n\n\t\t// Build the call's track resolver from the configured factory (if any).\n\t\tobservedCall.remoteTrackResolver = this.config.createRemoteTrackResolver?.(observedCall);\n\n\t\tobservedCall.enableSummary();\n\n\t\t// Exactly what the application registered, in registration order. Nothing is created\n\t\t// implicitly: a call with no configured detectors runs none.\n\t\tfor (const [ name, detectorConfig ] of this.callDetectorConfigs) {\n\t\t\tobservedCall.addDetector(name, detectorConfig);\n\t\t}\n\n\t\t// A call is updated when any of its clients is; the observer is updated when any of its calls\n\t\t// is. Composed, that means the observer is updated exactly when any client anywhere is — no\n\t\t// timer, no separate policy object, nothing to keep in sync.\n\t\tif (this.config.autoUpdateOnCallUpdate) {\n\t\t\tconst onCallUpdate = () => this.update();\n\n\t\t\tobservedCall.once('close', () => observedCall.off('update', onCallUpdate));\n\t\t\tobservedCall.on('update', onCallUpdate);\n\t\t}\n\n\t\tobservedCall.once('close', () => {\n\t\t\tthis.observedCalls.delete(observedCall.callId);\n\t\t\t++this.totalRemovedCall;\n\t\t});\n\n\t\tthis.observedCalls.set(observedCall.callId, observedCall);\n\t\t++this.totalAddedCall;\n\n\t\tthis._notify('call-added', { ...this.eventScope, observedCall });\n\n\t\treturn observedCall;\n\t}\n\n\tpublic getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(\n\t\tsettings: ObservedCallSettings<T>, acceptCtx?: AcceptContext\n\t): ObservedCall<T> | undefined {\n\t\treturn this.getObservedCall<T>(settings.callId) ?? this.createObservedCall<T>(settings, acceptCtx);\n\t}\n\n\tpublic createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(\n\t\tsettings: ObservedMediasoupRouterSettings<T> & {\n\t\t\t// When `true`, the observer emits `mediasoup-router-matched-with-peer-connection` for every\n\t\t\t// observed peer connection whose id matches one of this router's WebRTC transport ids.\n\t\t\t// When `false` / omitted, no peer-connection matching is performed.\n\t\t\tmatchPeerConnectionByWebRtcTransportId?: boolean,\n\t\t},\n\t) {\n\t\tif (this.closed) {\n\t\t\tlogger.warn('Attempted to create mediasoup router (id: %d) on a closed observer', settings.router.id);\n\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.observedMediasoupRouters.has(settings.router.id)) {\n\t\t\tlogger.warn('Observed Mediasoup Router (id %s) already exists; returning the existing instance', settings.router.id);\n\n\t\t\treturn this.observedMediasoupRouters.get(settings.router.id);\n\t\t}\n\n\t\tconst observedMediasoupRouter = new ObservedMediasoupRouter(settings);\n\t\tconst observedMediasoupRouterScope: ObservedMediasoupRouterScope = {\n\t\t\tobservedMediasoupRouter,\n\t\t\tobserver: this,\n\t\t};\n\n\t\t// Peer-connection matching is opt-in (see `matchPeerConnectionByWebRtcTransportId`). When on,\n\t\t// emit `mediasoup-router-matched-with-peer-connection` for each peer connection whose id matches\n\t\t// one of the router's WebRTC transport ids; the observer stores nothing — the app owns the pairing.\n\t\tif (settings.matchPeerConnectionByWebRtcTransportId) {\n\t\t\tconst onPeerConnectionAdded = (observedPeerConnectionScope: ObserverEvents['peer-connection-added'][0]) => {\n\t\t\t\tif (!observedMediasoupRouter.webrtcTransportIds.has(observedPeerConnectionScope.observedPeerConnection.peerConnectionId)) return;\n\n\t\t\t\tthis.emit('mediasoup-router-matched-with-peer-connection', {\n\t\t\t\t\t...observedMediasoupRouterScope,\n\t\t\t\t\t...observedPeerConnectionScope,\n\t\t\t\t});\n\t\t\t};\n\t\t\tconst stopMatching = () => this.off('peer-connection-added', onPeerConnectionAdded);\n\n\t\t\tthis.on('peer-connection-added', onPeerConnectionAdded);\n\t\t\tthis.once('observer-closed', stopMatching);\n\t\t\tobservedMediasoupRouter.once('close', stopMatching);\n\t\t}\n\n\t\tobservedMediasoupRouter.once('close', () => {\n\t\t\tthis.observedMediasoupRouters.delete(observedMediasoupRouter.id);\n\n\t\t\tthis.emit('mediasoup-router-removed', observedMediasoupRouterScope);\n\t\t});\n\t\tthis.observedMediasoupRouters.set(observedMediasoupRouter.id, observedMediasoupRouter);\n\n\t\tthis.emit('mediasoup-router-added', observedMediasoupRouterScope);\n\n\t\treturn observedMediasoupRouter;\n\t}\n\n\tpublic close() {\n\t\tif (this.closed) {\n\t\t\treturn logger.debug('Attempted to close twice');\n\t\t}\n\t\tthis.closed = true;\n\n\t\t// Copy first: `call.close()` removes the call from this map, and mutating a Map while iterating\n\t\t// it with forEach skips entries — half the calls would survive a close.\n\t\tfor (const call of [ ...this.observedCalls.values() ]) call.close();\n\n\t\t// Release detectors (they may hold bus subscriptions, timers, or tracker registrations) before\n\t\t// announcing the close.\n\t\tthis.detectors.clear();\n\t\t// Free anything waiting on a verdict rather than leaving it hanging.\n\t\tfor (const validator of [ ...this.validators ]) validator.cancel('observer closed');\n\t\tthis.validators.clear();\n\t\t// After the calls, so each one's `call-summary` still fires with its subscriptions intact.\n\t\tthis.callSummaryCollector?.close();\n\t\t// Each call cleared its own issues on close; this covers anything raised after that.\n\t\tthis.activeIssuesRegistry.clear();\n\n\t\tthis._notify('observer-closed', { ...this.eventScope });\n\t}\n\n\tpublic accept(sample: ClientSample, context?: AcceptContext): void {\n\t\tif (this.closed) {\n\t\t\treturn this._notify('sample-rejected', { ...this.eventScope, reason: 'observer-closed', sample });\n\t\t}\n\n\t\ttry {\n\t\t\tthis.acceptMiddlewares.process({ sample, context });\n\t\t} catch (err) {\n\t\t\tlogger.warn('An accept middleware threw; dropping the sample. %o', err);\n\t\t}\n\n\t\tif (!sample.callId) {\n\t\t\tthis._notify('sample-rejected', { ...this.eventScope, reason: 'missing-callId', sample });\n\n\t\t\treturn;\n\t\t}\n\t\tif (!sample.clientId) {\n\t\t\tthis._notify('sample-rejected', { ...this.eventScope, reason: 'missing-clientId', sample });\n\n\t\t\treturn;\n\t\t}\n\n\t\t// The context is handed to the create methods rather than used to pre-compute `appData` here:\n\t\t// they already run the factories for anything created without explicit `appData`, and calling\n\t\t// them from both places is two implementations of one rule, waiting to disagree.\n\t\tconst call = this.getOrCreateObservedCall({ callId: sample.callId }, context);\n\n\t\tif (!call) return;\n\n\t\tconst client = call.getOrCreateObservedClient({ clientId: sample.clientId }, context);\n\n\t\tif (!client) return;\n\n\t\tclient.accept(sample, context);\n\t}\n\n\tpublic update() {\n\t\tif (this.closed) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.numberOfInboundRtpStreams = 0;\n\t\tthis.numberOfOutboundRtpStreams = 0;\n\t\tthis.numberOfPeerConnections = 0;\n\t\tthis.numberOfDataChannels = 0;\n\t\tthis.numberOfClients = 0;\n\t\tthis.numberOfClientsUsingTurn = 0;\n\n\t\tfor (const call of this.observedCalls.values()) {\n\t\t\tthis.numberOfInboundRtpStreams += call.numberOfInboundRtpStreams;\n\t\t\tthis.numberOfOutboundRtpStreams += call.numberOfOutboundRtpStreams;\n\t\t\tthis.numberOfPeerConnections += call.numberOfPeerConnections;\n\t\t\tthis.numberOfDataChannels += call.numberOfDataChannels;\n\t\t\tthis.numberOfClients += call.numberOfClients;\n\t\t\tthis.numberOfClientsUsingTurn += call.clientsUsedTurn.size;\n\t\t}\n\n\t\tthis.observedTURN.update();\n\t\tthis.detectors.update();\n\t\t// A validator removes itself the moment it decides, so this set is normally empty.\n\t\tfor (const validator of [ ...this.validators ]) {\n\t\t\ttry {\n\t\t\t\tvalidator.update();\n\t\t\t} catch (err) {\n\t\t\t\tlogger.warn('Error running validator %s: %o', validator.name, err);\n\t\t\t}\n\t\t}\n\n\t\tthis._notify('observer-updated', { ...this.eventScope });\n\t}\n\n\t/**\n\t * Raise an observer-scoped (cross-call / SFU-wide) finding. Emitted on the bus as\n\t * `observer-issue`. Intended for `observer.detectors`, but the application may call it too.\n\t *\n\t * `payload` takes an **object**; see `ObserverIssue`.\n\t */\n\tpublic addIssue(issue: Omit<ObserverIssue, 'scope'>) {\n\t\tif (this.closed) return;\n\n\t\t// `scope` is stamped here rather than asked of every caller: it is a fact about *where the\n\t\t// finding was raised*, which this object knows and a detector should not have to restate.\n\t\tthis._notify('observer-issue', { ...this.eventScope, issue: { ...issue, scope: 'observer' } });\n\t}\n\n\t/** Emit an Observer-bus event. */\n\tprivate _notify<K extends keyof ObserverEvents>(type: K, ...args: ObserverEvents[K]): void {\n\t\tthis.emit(type, ...args);\n\t}\n}\n","import { EventEmitter } from 'events';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { ObservedTurnServer } from './ObservedTurnServer';\nimport { createLogger } from './common/logger';\n\nconst logger = createLogger('ObservedTURN');\n\nexport type ObservedTURNEventMap = {\n\t'update': [ObservedTURN];\n\t'close': [];\n}\n\nexport declare interface ObservedTURN {\n\ton<U extends keyof ObservedTURNEventMap>(event: U, listener: (...args: ObservedTURNEventMap[U]) => void): this;\n\toff<U extends keyof ObservedTURNEventMap>(event: U, listener: (...args: ObservedTURNEventMap[U]) => void): this;\n\tonce<U extends keyof ObservedTURNEventMap>(event: U, listener: (...args: ObservedTURNEventMap[U]) => void): this;\n\temit<U extends keyof ObservedTURNEventMap>(event: U, ...args: ObservedTURNEventMap[U]): boolean;\n}\n\nexport class ObservedTURN extends EventEmitter {\n\tpublic totalBytesSent = 0;\n\tpublic totalBytesReceived = 0;\n\tpublic totalPacketsSent = 0;\n\tpublic totalPacketsReceived = 0;\n\n\tpublic packetsSentPerSecond = 0;\n\tpublic packetsReceivedPerSecond = 0;\n\tpublic outboundBitrate = 0;\n\tpublic inboundBitrate = 0;\n\tpublic numberOfClients = 0;\n\n\tpublic readonly servers = new Map<string, ObservedTurnServer>();\n\n\tpublic constructor(\n\t) {\n\t\tsuper();\n\t}\n\n\tpublic update() {\n\t\tconst clientIds = new Set<string>();\n\n\t\tthis.packetsReceivedPerSecond = 0;\n\t\tthis.packetsSentPerSecond = 0;\n\t\tthis.outboundBitrate = 0;\n\t\tthis.inboundBitrate = 0;\n\n\t\tfor (const server of this.servers.values()) {\n\t\t\tserver.update();\n\t\t\tserver.observedPeerConnections.forEach((pc) => clientIds.add(pc.client.clientId));\n\t\t}\n\n\t\tthis.numberOfClients = clientIds.size;\n\t}\n\n\tpublic addPeerConnection(peerConnection: ObservedPeerConnection) {\n\t\t// selected pairs whose *local* candidate is a relay candidate with a `turn(s):` url\n\t\t// (the ICE server url is only exposed on local candidates).\n\t\tconst turnPairs = peerConnection.selectedIceCandiadtePairForTurn;\n\n\t\tif (turnPairs.length !== 1) {\n\t\t\treturn (logger.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), undefined);\n\t\t}\n\n\t\tconst candidatePair = turnPairs[0];\n\t\tconst rawUrl = candidatePair.getLocalCandidate()?.url;\n\n\t\tif (!rawUrl) {\n\t\t\treturn (logger.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), undefined);\n\t\t}\n\n\t\t// A TURN url looks like `turn(s):host[:port][?transport=udp|tcp]` (RFC 7065). The WHATWG\n\t\t// `URL` class parses these non-special schemes as opaque paths (empty hostname/port), so\n\t\t// derive the server key by stripping the query instead of using `new URL()`.\n\t\tconst turnServerUrl = rawUrl.split('?')[0];\n\t\tlet turnServer = this.servers.get(turnServerUrl);\n\n\t\tif (!turnServer) {\n\t\t\tturnServer = new ObservedTurnServer(turnServerUrl, this);\n\t\t\tthis.servers.set(turnServerUrl, turnServer);\n\t\t}\n\n\t\tturnServer.observedPeerConnections.set(peerConnection.peerConnectionId, peerConnection);\n\n\t\treturn turnServer;\n\t}\n\n\tpublic removePeerConnection(peerConnection: ObservedPeerConnection) {\n\t\tfor (const turnServer of this.servers.values()) {\n\t\t\tturnServer.observedPeerConnections.delete(peerConnection.peerConnectionId);\n\t\t}\n\t}\n\n}","import { ObservedIceCandidatePair } from './ObservedIceCandidatePair';\nimport { ObservedPeerConnection } from './ObservedPeerConnection';\nimport { ObservedTURN } from './ObservedTURN';\n\nexport class ObservedTurnServer {\n\tpublic totalBytesSent = 0;\n\tpublic totalBytesReceived = 0;\n\tpublic totalPacketsSent = 0;\n\tpublic totalPacketsReceived = 0;\n\n\tpublic packetsSentPerSecond = 0;\n\tpublic packetsReceivedPerSecond = 0;\n\tpublic outboundBitrate = 0;\n\tpublic inboundBitrate = 0;\n\n\tpublic deltaBytesSent = 0;\n\tpublic deltaBytesReceived = 0;\n\tpublic deltaPacketsSent = 0;\n\tpublic deltaPacketsReceived = 0;\n \n\tpublic readonly observedPeerConnections = new Map<string, ObservedPeerConnection>(); \n\n\tpublic updated = Date.now();\n\n\tpublic constructor(\n\t\tpublic readonly url: string,\n\t\tpublic readonly observedTURN: ObservedTURN,\n\t) {\n\n\t}\n\n\tpublic updateTurnUsage(...selectedcandidatepairs: ObservedIceCandidatePair[]) {\n\t\tfor (const selectedcandidatepair of selectedcandidatepairs) {\n\t\t\tthis.deltaBytesReceived += selectedcandidatepair.deltaBytesReceived;\n\t\t\tthis.deltaBytesSent += selectedcandidatepair.deltaBytesSent;\n\t\t\tthis.deltaPacketsReceived += selectedcandidatepair.deltaPacketsReceived;\n\t\t\tthis.deltaPacketsSent += selectedcandidatepair.deltaPacketsSent;\n\t\t}\n\t}\n\n\tpublic update() {\n\t\tconst now = Date.now();\n\t\tconst deltaInS = (now - this.updated) / 1000;\n \n\t\tthis.packetsSentPerSecond = this.deltaPacketsSent / deltaInS;\n\t\tthis.packetsReceivedPerSecond = this.deltaPacketsReceived / deltaInS; \n\t\tthis.outboundBitrate = (this.deltaBytesReceived * 8) / deltaInS;\n\t\tthis.inboundBitrate = (this.deltaBytesSent * 8) / deltaInS;\n \n\t\tthis.totalBytesSent += this.deltaBytesSent;\n\t\tthis.totalBytesReceived += this.deltaBytesReceived;\n\t\tthis.totalPacketsSent += this.deltaPacketsSent;\n\t\tthis.totalPacketsReceived += this.deltaPacketsReceived;\n\t\t\n\t\tthis.observedTURN.totalBytesSent += this.deltaBytesSent;\n\t\tthis.observedTURN.totalBytesReceived += this.deltaBytesReceived;\n\t\tthis.observedTURN.totalPacketsSent += this.deltaPacketsSent;\n\t\tthis.observedTURN.totalPacketsReceived += this.deltaPacketsReceived;\n \n\t\tthis.observedTURN.inboundBitrate += this.inboundBitrate;\n\t\tthis.observedTURN.outboundBitrate += this.outboundBitrate;\n\t\tthis.observedTURN.packetsReceivedPerSecond += this.packetsReceivedPerSecond;\n\t\tthis.observedTURN.packetsSentPerSecond += this.packetsSentPerSecond;\n \n\t\tthis.deltaBytesSent = 0;\n\t\tthis.deltaBytesReceived = 0;\n\t\tthis.deltaPacketsSent = 0;\n\t\tthis.deltaPacketsReceived = 0;\n\n\t\tthis.updated = now;\n\t}\n}","export type Middleware<T> = (\n\tinput: T,\n\tnext: (nextInput: T) => void\n) => void;\n\nexport interface Processor<T> {\n\tfinalCallback?: Callback<T>;\n\tprocess(value: T): void;\n\taddMiddleware(...middlewares: Middleware<T>[]): Processor<T>;\n\tremoveMiddleware(...middlewares: Middleware<T>[]): Processor<T>;\n}\n\ntype Callback<T> = (input: T) => void;\n\nclass Executor<T> {\n\tpublic done = false;\n\tprivate index = 0;\n\tprivate prevIndex = -1;\n\n\tconstructor(\n\t\tprivate readonly stack: Middleware<T>[],\n\t\tprivate readonly finalCallback?: Callback<T>\n\t) {\n\t}\n\n\t// Executes the middleware stack\n\tpublic execute(input: T): void {\n\t\tif (this.index <= this.prevIndex) {\n\t\t\tthrow new Error('Middleware must call next() only once!');\n\t\t} else if (this.done) {\n\t\t\tthrow new Error('Middleware stack has already been executed!');\n\t\t}\n\n\t\tthis.prevIndex = this.index;\n\n\t\tconst middleware = this.stack[this.index];\n\n\t\tthis.index++;\n\n\t\tif (middleware) {\n\t\t\treturn middleware(input, (nextInput: T) => this.execute(nextInput));\n\t\t}\n\n\t\tthis.done = true;\n\t\tthis.finalCallback?.(input);\n\t}\n}\n\nexport class MiddlewareProcessor<T> implements Processor<T> {\n\tprivate stack: Middleware<T>[] = [];\n\tpublic finalCallback?: Callback<T>;\n\n\tpublic addMiddleware(...middlewares: Middleware<T>[]): Processor<T> {\n\t\tif (middlewares && middlewares.length > 0) {\n\t\t\tthis.stack.push(...middlewares);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\tpublic removeMiddleware(...middlewares: Middleware<T>[]): Processor<T> {\n\t\tif (middlewares && middlewares.length > 0) {\n\t\t\tfor (const middleware of middlewares) {\n\t\t\t\tconst index = this.stack.indexOf(middleware);\n\n\t\t\t\tif (index >= 0) {\n\t\t\t\t\tthis.stack.splice(index, 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n\n\tpublic process(value: T): void {\n\t\tif (this.stack.length === 0) {\n\t\t\treturn this.finalCallback?.(value);\n\t\t}\n\n\t\tconst executor = new Executor(this.stack, this.finalCallback);\n\n\t\texecutor.execute(value);\n\t}\n}\n","import { EventEmitter } from 'events';\nimport type { types } from 'mediasoup';\nimport {\n\tMediasoupRouterSample,\n\tMediasoupTransportSample,\n\tMediasoupProducerSample,\n\tMediasoupConsumerSample,\n\tMediasoupDataProducerSample,\n\tMediasoupDataConsumerSample,\n\tMediasoupWebRtcTransportSampleEventMap,\n\tMediasoupPlainTransportSampleEventMap,\n\tMediasoupPipeTransportSampleEventMap,\n} from './schema/MediasoupRouter';\nimport { createLogger } from './common/logger';\n\nconst logger = createLogger('ObservedMediasoupRouter');\n\n/**\n * Declarative enrichment: return the `attachments` to stamp onto an entity's sample the moment it is\n * created. Called once per entity, before the corresponding `*-sample-added` event.\n *\n * The mediasoup object is handed in, so the common case — mirroring mediasoup's own `appData`, where\n * applications already keep `participantId`, `purpose` and friends — is a one-liner. Returning\n * `undefined` attaches nothing.\n */\nexport type MediasoupSampleEnricher = {\n\ttransport?: (transport: types.Transport) => Record<string, unknown> | undefined;\n\tproducer?: (producer: types.Producer, transport: types.Transport) => Record<string, unknown> | undefined;\n\tconsumer?: (consumer: types.Consumer, transport: types.Transport) => Record<string, unknown> | undefined;\n\tdataProducer?: (dataProducer: types.DataProducer, transport: types.Transport) => Record<string, unknown> | undefined;\n\tdataConsumer?: (dataConsumer: types.DataConsumer, transport: types.Transport) => Record<string, unknown> | undefined;\n};\n\nexport type ObservedMediasoupRouterSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {\n\trouter: types.Router,\n\tappData?: AppData;\n\tattachments?: Record<string, unknown>,\n\n\t/** Stamp `attachments` onto each entity sample as it is created. See {@link MediasoupSampleEnricher}. */\n\tenrich?: MediasoupSampleEnricher,\n};\n\n/**\n * Lifecycle hooks for building your own report.\n *\n * Each entity announces itself as `<entity>-sample-added` when it appears and\n * `<entity>-sample-closed` when it goes away, carrying **the live sample object** plus the mediasoup\n * object it came from. Mutating `sample.attachments` inside a handler is the intended way to extend\n * a sample on the fly — the object you receive is the one held in `observedRouter.sample`, not a copy.\n */\nexport type ObservedMediasoupRouterEvents = {\n\tclose: [];\n\n\t'transport-sample-added': [{ sample: MediasoupTransportSample, transport: types.Transport }];\n\t'transport-sample-closed': [{ sample: MediasoupTransportSample, transport: types.Transport }];\n\n\t'producer-sample-added': [{ sample: MediasoupProducerSample, producer: types.Producer, transport: types.Transport }];\n\t'producer-sample-closed': [{ sample: MediasoupProducerSample, producer: types.Producer, transport: types.Transport }];\n\n\t'consumer-sample-added': [{ sample: MediasoupConsumerSample, consumer: types.Consumer, transport: types.Transport }];\n\t'consumer-sample-closed': [{ sample: MediasoupConsumerSample, consumer: types.Consumer, transport: types.Transport }];\n\n\t'data-producer-sample-added': [{ sample: MediasoupDataProducerSample, dataProducer: types.DataProducer, transport: types.Transport }];\n\t'data-producer-sample-closed': [{ sample: MediasoupDataProducerSample, dataProducer: types.DataProducer, transport: types.Transport }];\n\n\t'data-consumer-sample-added': [{ sample: MediasoupDataConsumerSample, dataConsumer: types.DataConsumer, transport: types.Transport }];\n\t'data-consumer-sample-closed': [{ sample: MediasoupDataConsumerSample, dataConsumer: types.DataConsumer, transport: types.Transport }];\n};\n\nexport declare interface ObservedMediasoupRouter {\n\ton<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;\n\toff<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;\n\tonce<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;\n\temit<U extends keyof ObservedMediasoupRouterEvents>(event: U, ...args: ObservedMediasoupRouterEvents[U]): boolean;\n}\n\n/**\n * Observes a live mediasoup `Router` by subscribing to its `observer` API and **accumulates** its\n * topology and lifecycle into an in-memory `MediasoupRouterSample` (`observedRouter.sample`):\n * transports, producers, consumers, data producers/consumers, their state-change history and\n * `createdAt` / `closedAt`. The sample grows for the life of the router (closed entities are kept,\n * with their `closedAt` set) and is yours to read, snapshot, or persist.\n *\n * NOTE: this is intentionally the simplest approach — everything is held in memory. For very large\n * routers (e.g. ~100 participants producing and consuming on one router, where consumers grow as\n * O(N²)) this can become substantial; in that case do your own periodic sampling/persistence and\n * discard what you don't need (see the README's \"Memory & large meetings\" note).\n */\nexport class ObservedMediasoupRouter<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter {\n\tpublic readonly router: types.Router;\n\tpublic appData: AppData;\n\tpublic readonly sample: MediasoupRouterSample;\n\n\tpublic readonly webrtcTransportIds = new Set<string>();\n\tpublic closed = false;\n\n\t// Id -> sample indexes over the arrays in `this.sample`, so an entity can be reached in O(1)\n\t// instead of scanning (`sample.producers.find(...)`). They hold the *same* objects as the arrays.\n\tprivate readonly _transportSamples = new Map<string, MediasoupTransportSample>();\n\tprivate readonly _producerSamples = new Map<string, MediasoupProducerSample>();\n\tprivate readonly _consumerSamples = new Map<string, MediasoupConsumerSample>();\n\tprivate readonly _dataProducerSamples = new Map<string, MediasoupDataProducerSample>();\n\tprivate readonly _dataConsumerSamples = new Map<string, MediasoupDataConsumerSample>();\n\tprivate readonly _enrich?: MediasoupSampleEnricher;\n\n\tpublic constructor(\n\t\tsettings: ObservedMediasoupRouterSettings<AppData>,\n\t) {\n\t\tsuper();\n\n\t\tthis.router = settings.router;\n\t\tthis.appData = (settings.appData ?? {}) as AppData;\n\t\tthis._enrich = settings.enrich;\n\t\tthis.sample = {\n\t\t\trouterId: this.router.id,\n\t\t\tattachments: settings.attachments ?? {},\n\t\t\tcreatedAt: Date.now(),\n\t\t\tproducers: [],\n\t\t\tconsumers: [],\n\t\t\tdataProducers: [],\n\t\t\tdataConsumers: [],\n\t\t\ttransports: [],\n\t\t};\n\n\t\tthis.attachRouterListeners();\n\t}\n\n\tpublic get id() {\n\t\treturn this.router.id;\n\t}\n\n\tpublic get attachments() {\n\t\treturn this.sample.attachments;\n\t}\n\n\t/* ---------------------------------------------------------------------------------------------\n\t * Reaching individual entity samples.\n\t *\n\t * `sample.producers` (and friends) are arrays because that is the shape you want to serialize.\n\t * These accessors index the same objects by id, so annotating one entity is an O(1) lookup and a\n\t * mutation, rather than a scan that silently does nothing when the id is wrong.\n\t * ------------------------------------------------------------------------------------------- */\n\n\tpublic getTransportSample(id: string) {\n\t\treturn this._transportSamples.get(id);\n\t}\n\n\tpublic getProducerSample(id: string) {\n\t\treturn this._producerSamples.get(id);\n\t}\n\n\tpublic getConsumerSample(id: string) {\n\t\treturn this._consumerSamples.get(id);\n\t}\n\n\tpublic getDataProducerSample(id: string) {\n\t\treturn this._dataProducerSamples.get(id);\n\t}\n\n\tpublic getDataConsumerSample(id: string) {\n\t\treturn this._dataConsumerSamples.get(id);\n\t}\n\n\t/**\n\t * Merge `attachments` into an entity's sample, whichever kind it is.\n\t *\n\t * Ids are unique across mediasoup entity kinds, so one method covers all of them. Returns `false`\n\t * when the id is unknown — a real answer instead of failing quietly, which matters when the\n\t * annotation is driven by application events that may race the mediasoup ones.\n\t */\n\tpublic attachTo(id: string, attachments: Record<string, unknown>): boolean {\n\t\tconst sample = this._transportSamples.get(id)\n\t\t\t?? this._producerSamples.get(id)\n\t\t\t?? this._consumerSamples.get(id)\n\t\t\t?? this._dataProducerSamples.get(id)\n\t\t\t?? this._dataConsumerSamples.get(id);\n\n\t\tif (!sample) return false;\n\n\t\tsample.attachments = { ...sample.attachments, ...attachments };\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * A **detached deep copy** of the current sample — the basis for building your own report.\n\t *\n\t * `this.sample` is live: its arrays grow and its `history` entries are appended as the router\n\t * runs, so a report built directly on it keeps changing after you think you're done. This returns\n\t * a snapshot that never moves.\n\t */\n\tpublic snapshot(): MediasoupRouterSample {\n\t\t// `structuredClone` is a global that isn't guaranteed everywhere the library runs (older\n\t\t// runtimes, some test environments), so fall back to a JSON round-trip. The sample is plain\n\t\t// serializable data by construction, which is exactly what a report needs anyway.\n\t\tif (typeof structuredClone === 'function') return structuredClone(this.sample);\n\n\t\treturn JSON.parse(JSON.stringify(this.sample)) as MediasoupRouterSample;\n\t}\n\n\tpublic close() {\n\t\tif (this.closed) return;\n\n\t\tthis.closed = true;\n\t\tif (this.sample.closedAt === undefined) this.sample.closedAt = Date.now();\n\n\t\tthis.emit('close');\n\t}\n\n\tpublic addTransport = (transport: types.Transport) => {\n\t\tlet transportType = transport.type;\n\n\t\t// for backward compatibility\n\t\tif (!transportType) {\n\t\t\tif (transport.constructor.name === 'WebRtcTransport') transportType = 'webrtc';\n\t\t\telse if (transport.constructor.name === 'PlainTransport') transportType = 'plain';\n\t\t\telse if (transport.constructor.name === 'PipeTransport') transportType = 'pipe';\n\t\t\telse if (transport.constructor.name === 'DirectTransport') transportType = 'direct';\n\t\t}\n\n\t\tif (!transportType) return logger.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);\n\n\t\tswitch (transportType) {\n\t\t\tcase 'webrtc':\n\t\t\t\treturn this.addWebRtcTransport(transport as types.WebRtcTransport);\n\t\t\tcase 'plain':\n\t\t\t\treturn this.addPlainTransport(transport as types.PlainTransport);\n\t\t\tcase 'pipe':\n\t\t\t\treturn this.addPipeTransport(transport as types.PipeTransport);\n\t\t\tcase 'direct':\n\t\t\t\treturn this.addDirectTransport(transport as types.DirectTransport);\n\t\t\tdefault:\n\t\t\t\treturn logger.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);\n\t\t}\n\t};\n\n\tpublic addWebRtcTransport(transport: types.WebRtcTransport) {\n\t\tconst history: MediasoupWebRtcTransportSampleEventMap[] = [];\n\t\tconst transportSample: MediasoupTransportSample = {\n\t\t\tid: transport.id, type: 'webrtc', createdAt: Date.now(), tuple: transport.iceSelectedTuple, history,\n\t\t};\n\n\t\tthis._addTransportSample(transportSample, transport);\n\t\tthis.webrtcTransportIds.add(transport.id);\n\n\t\tconst onIceStateChange = (iceState: types.IceState) =>\n\t\t\thistory.push({ type: `icestate-changed-to-${iceState}`, timestamp: Date.now() } as MediasoupWebRtcTransportSampleEventMap);\n\t\tconst onIceSelectedTupleChange = (tuple: types.TransportTuple) => {\n\t\t\ttransportSample.tuple = tuple;\n\t\t\thistory.push({ type: 'iceselectedtuple-changed', timestamp: Date.now(), ...tuple });\n\t\t};\n\t\tconst onDtlsStateChange = (dtlsState: types.DtlsState) => {\n\t\t\thistory.push({ type: `dtlsstate-changed-to-${dtlsState}`, timestamp: Date.now() } as MediasoupWebRtcTransportSampleEventMap);\n\t\t\tif (dtlsState === 'connected' && transportSample.connectedAt === undefined) transportSample.connectedAt = Date.now();\n\t\t};\n\t\tconst onSctpStateChange = (sctpState: types.SctpState) =>\n\t\t\thistory.push({ type: `sctpstate-changed-to-${sctpState}`, timestamp: Date.now() } as MediasoupWebRtcTransportSampleEventMap);\n\n\t\tthis._attachTransportObserverListeners(transport, transportSample, () => {\n\t\t\ttransport.observer.off('icestatechange', onIceStateChange);\n\t\t\ttransport.observer.off('iceselectedtuplechange', onIceSelectedTupleChange);\n\t\t\ttransport.observer.off('dtlsstatechange', onDtlsStateChange);\n\t\t\ttransport.observer.off('sctpstatechange', onSctpStateChange);\n\n\t\t\tthis.webrtcTransportIds.delete(transport.id);\n\t\t});\n\n\t\ttransport.observer.on('icestatechange', onIceStateChange);\n\t\ttransport.observer.on('iceselectedtuplechange', onIceSelectedTupleChange);\n\t\ttransport.observer.on('dtlsstatechange', onDtlsStateChange);\n\t\ttransport.observer.on('sctpstatechange', onSctpStateChange);\n\t}\n\n\tpublic addPlainTransport(transport: types.PlainTransport) {\n\t\tconst history: MediasoupPlainTransportSampleEventMap[] = [];\n\t\tconst transportSample: MediasoupTransportSample = {\n\t\t\tid: transport.id, type: 'plain', createdAt: Date.now(), tuple: transport.tuple, rtcpTuple: transport.rtcpTuple, history,\n\t\t};\n\n\t\tthis._addTransportSample(transportSample, transport);\n\n\t\tconst onTuple = (tuple: types.TransportTuple) => {\n\t\t\ttransportSample.tuple = tuple;\n\t\t\tif (transportSample.connectedAt === undefined) transportSample.connectedAt = Date.now();\n\t\t\thistory.push({ type: 'tuple-changed', timestamp: Date.now(), ...tuple });\n\t\t};\n\t\tconst onRtcpTuple = (rtcpTuple: types.TransportTuple) => {\n\t\t\ttransportSample.rtcpTuple = rtcpTuple;\n\t\t\thistory.push({ type: 'rtcptuple-changed', timestamp: Date.now(), ...rtcpTuple });\n\t\t};\n\t\tconst onSctpStateChange = (sctpState: types.SctpState) => {\n\t\t\tif (sctpState === 'connected' && transportSample.connectedAt === undefined) transportSample.connectedAt = Date.now();\n\t\t\thistory.push({ type: `sctpstate-changed-to-${sctpState}`, timestamp: Date.now() } as MediasoupPlainTransportSampleEventMap);\n\t\t};\n\n\t\tthis._attachTransportObserverListeners(transport, transportSample, () => {\n\t\t\ttransport.observer.off('tuple', onTuple);\n\t\t\ttransport.observer.off('rtcptuple', onRtcpTuple);\n\t\t\ttransport.observer.off('sctpstatechange', onSctpStateChange);\n\t\t});\n\n\t\ttransport.observer.on('tuple', onTuple);\n\t\ttransport.observer.on('rtcptuple', onRtcpTuple);\n\t\ttransport.observer.on('sctpstatechange', onSctpStateChange);\n\t}\n\n\tpublic addPipeTransport(transport: types.PipeTransport) {\n\t\tconst history: MediasoupPipeTransportSampleEventMap[] = [];\n\t\tconst transportSample: MediasoupTransportSample = {\n\t\t\tid: transport.id, type: 'pipe', createdAt: Date.now(), tuple: transport.tuple, history,\n\t\t};\n\n\t\tthis._addTransportSample(transportSample, transport);\n\n\t\tconst onSctpStateChange = (sctpState: types.SctpState) => {\n\t\t\tif (sctpState === 'connected' && transportSample.connectedAt === undefined) transportSample.connectedAt = Date.now();\n\t\t\thistory.push({ type: `sctpstate-changed-to-${sctpState}`, timestamp: Date.now() } as MediasoupPipeTransportSampleEventMap);\n\t\t};\n\n\t\tthis._attachTransportObserverListeners(transport, transportSample, () => {\n\t\t\ttransport.observer.off('sctpstatechange', onSctpStateChange);\n\t\t});\n\n\t\ttransport.observer.on('sctpstatechange', onSctpStateChange);\n\t}\n\n\tpublic addDirectTransport(transport: types.DirectTransport) {\n\t\tconst now = Date.now();\n\t\tconst transportSample: MediasoupTransportSample = {\n\t\t\tid: transport.id, type: 'direct', createdAt: now, connectedAt: now, history: [],\n\t\t};\n\n\t\tthis._addTransportSample(transportSample, transport);\n\n\t\tthis._attachTransportObserverListeners(transport, transportSample);\n\t}\n\n\tpublic addProducer(transport: types.Transport, producer: types.Producer) {\n\t\tconst firstCodec = producer.rtpParameters.codecs[0];\n\t\tconst producerSample: MediasoupProducerSample = {\n\t\t\tid: producer.id,\n\t\t\ttransportId: transport.id,\n\t\t\tcreatedAt: Date.now(),\n\t\t\tkind: producer.kind,\n\t\t\tcodecInfo: {\n\t\t\t\tmimeType: firstCodec?.mimeType ?? 'unknown/unknown',\n\t\t\t\tpayloadType: firstCodec?.payloadType ?? 0,\n\t\t\t\tclockRate: firstCodec?.clockRate ?? 0,\n\t\t\t\tchannels: firstCodec?.channels,\n\t\t\t\tparameters: firstCodec?.parameters,\n\t\t\t\trtcpFeedback: firstCodec?.rtcpFeedback,\n\t\t\t},\n\t\t\tssrcs: producer.rtpParameters.encodings?.map((e) => e.ssrc).filter((s): s is number => typeof s === 'number'),\n\t\t\trids: producer.rtpParameters.encodings?.map((e) => e.rid).filter((r): r is string => typeof r === 'string'),\n\t\t\thistory: [],\n\t\t};\n\n\t\tthis._addProducerSample(producerSample, producer, transport);\n\n\t\tconst onPause = () => producerSample.history.push({ type: 'pause', timestamp: Date.now() });\n\t\tconst onResume = () => producerSample.history.push({ type: 'resume', timestamp: Date.now() });\n\n\t\tproducer.observer.once('close', () => {\n\t\t\tproducer.observer.off('pause', onPause);\n\t\t\tproducer.observer.off('resume', onResume);\n\t\t\tproducerSample.closedAt = Date.now();\n\t\t\tthis.emit('producer-sample-closed', { sample: producerSample, producer, transport });\n\t\t});\n\t\tproducer.observer.on('pause', onPause);\n\t\tproducer.observer.on('resume', onResume);\n\t}\n\n\tpublic addConsumer(transport: types.Transport, consumer: types.Consumer) {\n\t\tconst consumerSample: MediasoupConsumerSample = {\n\t\t\tid: consumer.id,\n\t\t\tproducerId: consumer.producerId,\n\t\t\ttransportId: transport.id,\n\t\t\tcreatedAt: Date.now(),\n\t\t\tkind: consumer.kind,\n\t\t\thistory: [],\n\t\t};\n\n\t\tthis._addConsumerSample(consumerSample, consumer, transport);\n\n\t\tconst onPause = () => consumerSample.history.push({ type: 'pause', timestamp: Date.now() });\n\t\tconst onResume = () => consumerSample.history.push({ type: 'resume', timestamp: Date.now() });\n\t\tconst onProducerPause = () => consumerSample.history.push({ type: 'producerPaused', timestamp: Date.now() });\n\t\tconst onProducerResume = () => consumerSample.history.push({ type: 'producerResumed', timestamp: Date.now() });\n\n\t\tconsumer.observer.once('close', () => {\n\t\t\tconsumer.observer.off('pause', onPause);\n\t\t\tconsumer.observer.off('resume', onResume);\n\t\t\tconsumer.off('producerresume', onProducerResume);\n\t\t\tconsumer.off('producerpause', onProducerPause);\n\t\t\tconsumerSample.closedAt = Date.now();\n\t\t\tthis.emit('consumer-sample-closed', { sample: consumerSample, consumer, transport });\n\t\t});\n\n\t\tconsumer.observer.on('pause', onPause);\n\t\tconsumer.observer.on('resume', onResume);\n\t\tconsumer.on('producerpause', onProducerPause);\n\t\tconsumer.on('producerresume', onProducerResume);\n\t}\n\n\tpublic addDataProducer(transport: types.Transport, dataProducer: types.DataProducer) {\n\t\tconst dataProducerSample: MediasoupDataProducerSample = {\n\t\t\tid: dataProducer.id,\n\t\t\ttransportId: transport.id,\n\t\t\tcreatedAt: Date.now(),\n\t\t\tlabel: dataProducer.label,\n\t\t\tprotocol: dataProducer.protocol,\n\t\t};\n\n\t\tthis._addDataProducerSample(dataProducerSample, dataProducer, transport);\n\n\t\tdataProducer.observer.once('close', () => {\n\t\t\tdataProducerSample.closedAt = Date.now();\n\t\t\tthis.emit('data-producer-sample-closed', { sample: dataProducerSample, dataProducer, transport });\n\t\t});\n\t}\n\n\tpublic addDataConsumer(transport: types.Transport, dataConsumer: types.DataConsumer) {\n\t\tconst dataConsumerSample: MediasoupDataConsumerSample = {\n\t\t\tid: dataConsumer.id,\n\t\t\tdataProducerId: dataConsumer.dataProducerId,\n\t\t\ttransportId: transport.id,\n\t\t\tcreatedAt: Date.now(),\n\t\t\tlabel: dataConsumer.label,\n\t\t\tprotocol: dataConsumer.protocol,\n\t\t};\n\n\t\tthis._addDataConsumerSample(dataConsumerSample, dataConsumer, transport);\n\n\t\tdataConsumer.observer.once('close', () => {\n\t\t\tdataConsumerSample.closedAt = Date.now();\n\t\t\tthis.emit('data-consumer-sample-closed', { sample: dataConsumerSample, dataConsumer, transport });\n\t\t});\n\t}\n\n\tprivate attachRouterListeners() {\n\t\tthis.router.observer.once('close', () => {\n\t\t\tthis.router.observer.off('newtransport', this.addTransport);\n\n\t\t\tthis.sample.closedAt = Date.now();\n\t\t\tthis.close();\n\t\t});\n\t\tthis.router.observer.on('newtransport', this.addTransport);\n\t}\n\n\t/* ---------------------------------------------------------------------------------------------\n\t * Registration: push into the sample array, index by id, run the enricher, announce.\n\t * The order matters — enrichment lands *before* the event, so a listener always sees the\n\t * declaratively-attached data and can build on it rather than race it.\n\t * ------------------------------------------------------------------------------------------- */\n\n\tprivate _addTransportSample(sample: MediasoupTransportSample, transport: types.Transport) {\n\t\tthis.sample.transports.push(sample);\n\t\tthis._transportSamples.set(sample.id, sample);\n\t\tthis._applyEnrichment(sample, this._enrich?.transport && (() => this._enrich?.transport?.(transport)));\n\t\tthis.emit('transport-sample-added', { sample, transport });\n\t}\n\n\tprivate _addProducerSample(sample: MediasoupProducerSample, producer: types.Producer, transport: types.Transport) {\n\t\tthis.sample.producers.push(sample);\n\t\tthis._producerSamples.set(sample.id, sample);\n\t\tthis._applyEnrichment(sample, this._enrich?.producer && (() => this._enrich?.producer?.(producer, transport)));\n\t\tthis.emit('producer-sample-added', { sample, producer, transport });\n\t}\n\n\tprivate _addConsumerSample(sample: MediasoupConsumerSample, consumer: types.Consumer, transport: types.Transport) {\n\t\tthis.sample.consumers.push(sample);\n\t\tthis._consumerSamples.set(sample.id, sample);\n\t\tthis._applyEnrichment(sample, this._enrich?.consumer && (() => this._enrich?.consumer?.(consumer, transport)));\n\t\tthis.emit('consumer-sample-added', { sample, consumer, transport });\n\t}\n\n\tprivate _addDataProducerSample(sample: MediasoupDataProducerSample, dataProducer: types.DataProducer, transport: types.Transport) {\n\t\tthis.sample.dataProducers.push(sample);\n\t\tthis._dataProducerSamples.set(sample.id, sample);\n\t\tthis._applyEnrichment(sample, this._enrich?.dataProducer && (() => this._enrich?.dataProducer?.(dataProducer, transport)));\n\t\tthis.emit('data-producer-sample-added', { sample, dataProducer, transport });\n\t}\n\n\tprivate _addDataConsumerSample(sample: MediasoupDataConsumerSample, dataConsumer: types.DataConsumer, transport: types.Transport) {\n\t\tthis.sample.dataConsumers.push(sample);\n\t\tthis._dataConsumerSamples.set(sample.id, sample);\n\t\tthis._applyEnrichment(sample, this._enrich?.dataConsumer && (() => this._enrich?.dataConsumer?.(dataConsumer, transport)));\n\t\tthis.emit('data-consumer-sample-added', { sample, dataConsumer, transport });\n\t}\n\n\t/**\n\t * Run an enricher and merge what it returns.\n\t *\n\t * Takes a thunk rather than a value so the **invocation** is inside the guard — application code\n\t * runs here, and a throwing enricher must not take the router's bookkeeping down with it.\n\t */\n\tprivate _applyEnrichment(\n\t\tsample: { attachments?: Record<string, unknown> },\n\t\tenrich?: () => Record<string, unknown> | undefined,\n\t) {\n\t\tif (!enrich) return;\n\n\t\ttry {\n\t\t\tconst attachments = enrich();\n\n\t\t\tif (attachments) sample.attachments = { ...sample.attachments, ...attachments };\n\t\t} catch (err) {\n\t\t\tlogger.warn('Mediasoup sample enricher threw for router %s: %o', this.id, err);\n\t\t}\n\t}\n\n\tprivate _attachTransportObserverListeners(\n\t\ttransport: types.Transport,\n\t\ttransportSample: MediasoupTransportSample,\n\t\tonClose?: () => void,\n\t) {\n\t\tconst onNewProducer = (producer: types.Producer) => this.addProducer(transport, producer);\n\t\tconst onNewConsumer = (consumer: types.Consumer) => this.addConsumer(transport, consumer);\n\t\tconst onNewDataProducer = (dataProducer: types.DataProducer) => this.addDataProducer(transport, dataProducer);\n\t\tconst onNewDataConsumer = (dataConsumer: types.DataConsumer) => this.addDataConsumer(transport, dataConsumer);\n\n\t\ttransport.observer.once('close', () => {\n\t\t\ttransport.observer.off('newproducer', onNewProducer);\n\t\t\ttransport.observer.off('newconsumer', onNewConsumer);\n\t\t\ttransport.observer.off('newdataproducer', onNewDataProducer);\n\t\t\ttransport.observer.off('newdataconsumer', onNewDataConsumer);\n\n\t\t\tonClose?.();\n\n\t\t\ttransportSample.closedAt = Date.now();\n\t\t\tthis.emit('transport-sample-closed', { sample: transportSample, transport });\n\t\t});\n\n\t\ttransport.observer.on('newproducer', onNewProducer);\n\t\ttransport.observer.on('newconsumer', onNewConsumer);\n\t\ttransport.observer.on('newdataproducer', onNewDataProducer);\n\t\ttransport.observer.on('newdataconsumer', onNewDataConsumer);\n\t}\n}\n","import { createLogger } from '../common/logger';\nimport { ActiveClientIssue } from './ActiveClientIssue';\nimport { ActiveIssueTracker } from './ActiveIssueTracker';\n\nconst logger = createLogger('ActiveIssuesRegistry');\n\n/**\n * The set of client issues currently believed to be **open**, plus the fan-out that pushes them to\n * whoever asked for them.\n *\n * ### Push, not poll\n *\n * A detector does not scan for the issues it cares about; it registers as an\n * {@link ActiveIssueTracker} for the types it consumes and is handed them as they open and close.\n * The cost of a detector is then proportional to the issues it actually receives, not to the number\n * of participants — a healthy 500-client fleet does no work per tick.\n *\n * ```ts\n * observer.activeIssuesRegistry.addIssueTracker('congestion', detector);\n * ```\n *\n * There is **no wildcard**. A tracker names the types it consumes, and nothing else reaches it. \"Feed\n * me everything and I'll work out what matters\" pushes the decision from the application — which\n * knows its client build and its issue vocabulary — onto a detector that has to guess, and it makes\n * the cost of a subscription unbounded and invisible. If a detector should watch five issue types,\n * the caller lists five issue types.\n *\n * ### Two levels\n *\n * Every call owns a registry constructed with the observer's as its `parent`. An add or delete\n * touches both, so a call-scoped tracker sees only that call's issues while an observer-scoped one\n * sees the fleet — without either side iterating the other. The child keeps **its own** storage:\n * `size` is this scope's count, and {@link clear} (called when the call closes) removes only this\n * scope's issues from the parent and never touches the parent's tracker registrations.\n *\n * ### Only keyed issues arrive here\n *\n * An issue without a `key` has no lifecycle — nothing can ever close it — so treating it as \"active\"\n * would mean holding a symptom that may have ended long ago. Keyless issues stay one-shot: emitted\n * as `client-issue`, never registered. `client-monitor-js` >= 4.6.0 sends `key` on everything\n * stateful.\n */\nexport class ActiveIssuesRegistry implements ActiveIssueTracker {\n\tprivate readonly issues = new Set<ActiveClientIssue>();\n\tprivate readonly typesToTrackers = new Map<string, ActiveIssueTracker[]>();\n\n\tpublic constructor(\n\t\tprivate readonly parent?: ActiveIssueTracker,\n\t) {\n\t}\n\n\tpublic get size(): number {\n\t\treturn this.issues.size;\n\t}\n\n\t/**\n\t * The open issues in this scope, in insertion order.\n\t *\n\t * Insertion order is age order (`observedAt` is assigned on insert), which is what lets a consumer\n\t * stop at the first entry newer than its cutoff instead of scanning the whole set.\n\t */\n\tpublic values(): IterableIterator<ActiveClientIssue> {\n\t\treturn this.issues.values();\n\t}\n\n\tpublic [Symbol.iterator](): IterableIterator<ActiveClientIssue> {\n\t\treturn this.issues.values();\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this.issues.has(issue);\n\t}\n\n\tpublic add(issue: ActiveClientIssue): this {\n\t\t// Re-adding an issue already held would push it to every tracker a second time, and a tracker\n\t\t// counting distinct occurrences would double count. `Set.add` is idempotent; the fan-out is not.\n\t\tif (this.issues.has(issue)) return this;\n\n\t\tthis.issues.add(issue);\n\t\tthis.parent?.add(issue);\n\t\tthis._trackIssue(issue);\n\n\t\treturn this;\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\t// Membership decides everything below: without this guard, deleting an issue this scope never\n\t\t// held still removes it from the parent — one call closing could evict another call's issues.\n\t\tif (!this.issues.delete(issue)) return false;\n\n\t\tthis.parent?.delete(issue);\n\t\tthis._untrackIssue(issue);\n\n\t\treturn true;\n\t}\n\n\t/** Feed `tracker` every issue of `type` as it opens and closes. One call per type; no wildcard. */\n\tpublic addIssueTracker(type: string, tracker: ActiveIssueTracker): this {\n\t\tthis.typesToTrackers.set(\n\t\t\ttype,\n\t\t\t(this.typesToTrackers.get(type) ?? []).concat(tracker),\n\t\t);\n\n\t\treturn this;\n\t}\n\n\tpublic removeIssueTracker(tracker: ActiveIssueTracker): this {\n\t\tfor (const [ type, trackers ] of [ ...this.typesToTrackers ]) {\n\t\t\tconst remaining = trackers.filter((candidate) => candidate !== tracker);\n\n\t\t\t// Drop the key rather than leaving an empty array behind, so a long-lived observer that\n\t\t\t// churns detectors doesn't accumulate one map entry per issue type ever seen.\n\t\t\tif (remaining.length === 0) this.typesToTrackers.delete(type);\n\t\t\telse this.typesToTrackers.set(type, remaining);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Drop every issue in this scope, e.g. because the call closed.\n\t *\n\t * Deletes through {@link delete} so the parent sheds exactly this scope's issues. Tracker\n\t * *registrations* survive: a detector subscribed to the observer's registry must keep receiving\n\t * issues after any one call ends.\n\t */\n\tpublic clear(): void {\n\t\tfor (const issue of [ ...this.issues ]) this.delete(issue);\n\t}\n\n\tprivate _trackIssue(issue: ActiveClientIssue): void {\n\t\tthis._trackersOf(issue.type, (tracker) => tracker.add(issue));\n\t}\n\n\tprivate _untrackIssue(issue: ActiveClientIssue): void {\n\t\tthis._trackersOf(issue.type, (tracker) => tracker.delete(issue));\n\t}\n\n\t/**\n\t * Apply `apply` to every tracker interested in `type`.\n\t *\n\t * A tracker throwing must not abort the fan-out: the issue has already been added to (or removed\n\t * from) this registry, so a partial dispatch would leave the remaining trackers permanently out of\n\t * step with it. One broken detector should not desynchronise the others.\n\t */\n\tprivate _trackersOf(type: string, apply: (tracker: ActiveIssueTracker) => void): void {\n\t\tconst trackers = this.typesToTrackers.get(type);\n\n\t\tif (trackers) for (const tracker of trackers) this._safely(tracker, apply);\n\t}\n\n\tprivate _safely(target: ActiveIssueTracker, apply: (tracker: ActiveIssueTracker) => void): void {\n\t\ttry {\n\t\t\tapply(target);\n\t\t} catch (err) {\n\t\t\tlogger.warn('An issue tracker threw while being notified; continuing with the rest. %o', err);\n\t\t}\n\t}\n}\n","import type { ObserverEvents, ObservedCallScope } from '../ObserverEvents';\nimport type { CallIssue } from '../common/Issue';\n\n/**\n * The bus events that carry an `observedCall`, i.e. the ones an enricher can attribute to a summary.\n *\n * Derived from the event map rather than listed by hand, so it cannot drift: adding a call-scoped\n * event makes it enrichable automatically, and an enricher on an observer-scoped event\n * (`observer-issue`, `validation-ready`) will not compile — there is no single call it belongs to,\n * and quietly writing a fleet-wide fact into every open summary would be worse than a type error.\n */\nexport type CallScopedEventName = {\n\t[K in keyof ObserverEvents]: ObserverEvents[K][0] extends ObservedCallScope ? K : never;\n}[keyof ObserverEvents];\n\n/** A function that folds one event into the summary. Runs on every occurrence, for its own call. */\nexport type CallSummaryEnricher<K extends CallScopedEventName> =\n\t(summary: CallSummary, ...args: ObserverEvents[K]) => void;\n\n/** The enricher map: any subset of the call-scoped events, each fully typed against its payload. */\nexport type CallSummaryEnrichers = {\n\t[K in CallScopedEventName]?: CallSummaryEnricher<K>;\n};\n\n/** The built-in sections. Ask for what you want; anything absent is simply not collected. */\nexport type CallSummarySection = 'clients' | 'issues' | 'turnServers' | 'scores';\n\nexport type CallSummaryConfig = {\n\n\t/**\n\t * Which built-in sections to accumulate. Empty (the default) collects none of them — a summary\n\t * with only `enrich` is a perfectly good summary.\n\t */\n\tinclude: CallSummarySection[];\n\n\t/**\n\t * Fold arbitrary state in from any call-scoped event, into `summary.attachments`. See\n\t * {@link CallSummaryEnrichers}.\n\t *\n\t * Each enricher runs on **every** occurrence of its event, for its own call, so prefer the\n\t * low-frequency lifecycle events (`client-joined`, `call-issue`) over `client-updated`, which fires\n\t * once per sample per client. Keep them cheap and side-effect free: an enricher that throws is logged\n\t * and skipped rather than allowed to disturb the call, but one that is slow is on the ingestion path.\n\t */\n\tenrich?: CallSummaryEnrichers;\n\n\t/**\n\t * Cap on retained issues. Default `500`.\n\t *\n\t * A bound on memory per call, so scale it by how long your calls run and how noisy they are, not by\n\t * taste — a two-hour call with a struggling participant can raise hundreds. Past the cap issues are\n\t * dropped and `summary.truncated.issues` counts them, so the real total stays recoverable as\n\t * `issues.length + (truncated?.issues ?? 0)`. `0` collects the count only, keeping no issue objects.\n\t */\n\tmaxIssues: number;\n\n\t/**\n\t * Cap on retained client ids. Default `10_000`.\n\t *\n\t * High because the elements are short strings and the usual reason to read a summary is *who was in\n\t * this call*. It exists so a webinar-scale room cannot grow the summary without limit. Overflow is\n\t * counted in `summary.truncated.clientIds`; `joined`, `left` and `peak` are unaffected by the cap,\n\t * since they are counters rather than a list.\n\t */\n\tmaxClientIds: number;\n};\n\n/**\n * Who was in the call over its whole life — not just who is in it now.\n *\n * Deliberately identifiers and counts only. Anything *about* a client — browser, platform, region —\n * is already on `observedClient` while the call is live, and belongs in `attachments` via an enricher\n * if you want it kept. Duplicating it here would mean the library deciding which client attributes\n * matter, and it would mean reading the highest-frequency event on the bus to do it.\n */\nexport type CallSummaryClients = {\n\n\t/** Every client id seen, in join order, capped by `maxClientIds`. */\n\tclientIds: string[];\n\n\t/** The most participants present at any one moment. */\n\tpeak: number;\n\n\tjoined: number;\n\tleft: number;\n};\n\n/** Which TURN relays carried this call's media. */\nexport type CallSummaryTurnServers = {\n\tserverUrls: string[];\n\n\t/** Distinct clients seen relaying through any of them. */\n\tclientsRelayed: number;\n};\n\n/** The call score over time. Percentiles, not a mean — see `utils/stats`. */\nexport type CallSummaryScores = {\n\tmin?: number;\n\tmax?: number;\n\tmedian?: number;\n\n\t/** How many score readings went into the above. `0` means nothing was measured. */\n\tsamples: number;\n};\n\n/** What had to be dropped to stay within the caps. Absent when nothing was. */\nexport type CallSummaryTruncation = {\n\tissues?: number;\n\tclientIds?: number;\n};\n\n/**\n * An accumulating record of one call's life, finalised when the call closes.\n *\n * ### Why this exists\n *\n * Everything else in this library is about *now*. Detectors answer \"is something wrong right now\",\n * validators answer a structural question once, and both read state that the call throws away when\n * it ends. Nothing kept the answer to *\"what happened in that meeting?\"* — who was in it, what was\n * raised, how it scored — and that is the question asked after the call, by support, by billing, by\n * whoever is writing the incident note.\n *\n * ### It is opt-in, and its sections are opt-in\n *\n * `observedCall.summary` is `undefined` unless a summary was configured, and each section is present\n * only if it was requested. **An absent section means \"not collected\", never \"nothing happened\"** —\n * the same rule as `inconclusive` on a validator. Reading `summary.issues` as \"this call had no\n * issues\" when `'issues'` was never in `include` is the one misreading this type invites, so it does\n * not offer a default-empty section to make it easy.\n *\n * ### Read it live, receive it once\n *\n * The object is live: read `observedCall.summary` at any point during the call. It is also delivered\n * on the `call-summary` event, emitted inside `close()` while the call is still reachable — after\n * that the call is gone from `observer.observedCalls` and there is nothing left to ask.\n */\nexport type CallSummary = {\n\tcallId: string;\n\n\t/** First client join (client clock), as `ObservedCall` computed it. */\n\tstartedAt?: number;\n\n\t/** Last client leave. */\n\tendedAt?: number;\n\n\t/** `endedAt - startedAt`, when both are known. */\n\tdurationInMs?: number;\n\n\t/** When the summary itself was finalised (observer clock). Set by `close()`. */\n\tclosedAt?: number;\n\n\tclients?: CallSummaryClients;\n\n\t/**\n\t * Every issue raised against this call, in the order they were raised, capped by `maxIssues`.\n\t *\n\t * Just the issues — no derived tallies. A count is `issues.length`, a per-type count is one\n\t * `filter`, and either is cheaper to write at the call site than to keep correct here. The one\n\t * thing you cannot derive is what the cap discarded, which is why `truncated.issues` exists:\n\t * the issues actually raised is `issues.length + (truncated?.issues ?? 0)`.\n\t */\n\tissues?: CallIssue[];\n\tturnServers?: CallSummaryTurnServers;\n\tscores?: CallSummaryScores;\n\n\t/**\n\t * Whatever your enrichers put here. The library never writes to it, so it cannot collide with a\n\t * section added in a future version.\n\t *\n\t * **`attachments`, not `appData`, and the distinction is load-bearing.** `appData` is the live\n\t * working state an application hangs off an entity for the entity's lifetime, and it may hold\n\t * references that cannot be serialised — a mediasoup router, an `RTCPeerConnection`, a socket. A\n\t * summary is the opposite: it outlives the call precisely so it can be *shipped* — archived,\n\t * queued, written to a column — and it is handed to you on `call-summary` at the moment the call\n\t * it came from is being torn down. Anything unserialisable in it is a reference to something\n\t * already gone.\n\t *\n\t * So put serialisable facts here, the same contract as `attachments` on a `ClientSample`. If you\n\t * need the live object, read it off `observedCall` / `observedClient` inside the enricher and\n\t * attach what you can serialise: the router's `id`, not the router.\n\t */\n\tattachments: Record<string, unknown>;\n\n\t/**\n\t * What the caps discarded, and how much.\n\t *\n\t * Present **only** when something was actually dropped. A silently truncated summary is worse\n\t * than no summary — someone will count `log.length` and report it as the issue count — so the\n\t * shortfall is stated rather than left to be inferred from a suspiciously round number.\n\t */\n\ttruncated?: CallSummaryTruncation;\n};\n\n/** The defaults a summary is created with. Caps are generous but finite; see {@link CallSummary}. */\nexport const defaultCallSummaryConfig: CallSummaryConfig = {\n\tinclude: [],\n\tmaxIssues: 500,\n\tmaxClientIds: 10_000,\n};\n\n/** A fresh summary for `callId`, with only the requested sections present. */\nexport function createCallSummary(callId: string, config: CallSummaryConfig): CallSummary {\n\tconst summary: CallSummary = { callId, attachments: {} };\n\n\tif (config.include.includes('clients')) {\n\t\tsummary.clients = { clientIds: [], peak: 0, joined: 0, left: 0 };\n\t}\n\tif (config.include.includes('issues')) {\n\t\tsummary.issues = [];\n\t}\n\tif (config.include.includes('turnServers')) {\n\t\tsummary.turnServers = { serverUrls: [], clientsRelayed: 0 };\n\t}\n\tif (config.include.includes('scores')) {\n\t\tsummary.scores = { samples: 0 };\n\t}\n\n\treturn summary;\n}\n","import { createLogger } from '../common/logger';\nimport { percentile } from '../utils/stats';\nimport type { Observer } from '../Observer';\nimport type { ObservedCall } from '../ObservedCall';\nimport type { ObserverEvents } from '../ObserverEvents';\nimport { createCallSummary, type CallSummary, type CallSummaryConfig, type CallScopedEventName } from './CallSummary';\n\nconst logger = createLogger('CallSummaryCollector');\n\n/** Per-call scratch the summary itself has no place for — score readings awaiting a percentile. */\ntype CallScratch = {\n\tscores: number[];\n\tturnClientIds: Set<string>;\n};\n\n/**\n * Keeps every configured `CallSummary` up to date, from **observer-level** bus subscriptions.\n *\n * ### Why one collector and not one per call\n *\n * The obvious implementation subscribes each call's summary to the events it needs. But the bus is\n * observer-wide: a listener attached for call A is invoked for every event of every call, so that\n * design costs `calls × events` listeners *and* `calls` invocations per event — quadratic in the\n * thing most likely to be large. At 500 concurrent calls and eight subscribed events that is 4 000\n * listeners doing 500 no-op calls each, per event.\n *\n * So the collector attaches **one listener per event type, once**, and routes each event to the\n * summary of the call it names. Cost is O(subscribed event types), independent of how many calls are\n * in flight, and an event for a call with no summary costs one `undefined` check.\n *\n * ### Only call-scoped events\n *\n * Routing needs `observedCall` on the payload, which is exactly what `CallScopedEventName` selects.\n * Observer-scoped events have no single call to attribute to; see that type for why fanning them out\n * to every open summary would be worse than refusing.\n */\nexport class CallSummaryCollector {\n\tprivate readonly _scratch = new WeakMap<CallSummary, CallScratch>();\n\tprivate readonly _listeners: { event: keyof ObserverEvents, listener: (...args: never[]) => void }[] = [];\n\tprivate _closed = false;\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tprivate readonly _config: CallSummaryConfig,\n\t) {\n\t\tthis._subscribeBuiltIns();\n\t\tthis._subscribeEnrichers();\n\t}\n\n\t/**\n\t * Build a summary for `callId` and start tracking it.\n\t *\n\t * Creating it here, rather than letting the call create one and hand it over, keeps the resolved\n\t * configuration inside the single object that owns it — and makes it impossible to end up with a\n\t * summary whose sections nobody subscribed to fill.\n\t */\n\tpublic createSummary(callId: string): CallSummary {\n\t\tconst summary = createCallSummary(callId, this._config);\n\n\t\tthis._scratch.set(summary, { scores: [], turnClientIds: new Set() });\n\n\t\treturn summary;\n\t}\n\n\t/**\n\t * Finalise `call`'s summary: fold in what only makes sense once, and stamp the closing times.\n\t *\n\t * Percentiles are computed here rather than on every update — a median recomputed per tick over a\n\t * growing array is quadratic work to produce a number nobody reads until the end.\n\t */\n\tpublic finalise(call: ObservedCall): void {\n\t\tconst summary = call.summary;\n\n\t\tif (!summary) return;\n\n\t\tconst scratch = this._scratch.get(summary);\n\n\t\tsummary.startedAt = call.startedAt;\n\t\tsummary.endedAt = call.endedAt;\n\t\tsummary.durationInMs = summary.startedAt !== undefined && summary.endedAt !== undefined\n\t\t\t? Math.max(0, summary.endedAt - summary.startedAt)\n\t\t\t: undefined;\n\t\tsummary.closedAt = Date.now();\n\n\t\tif (summary.scores && scratch) {\n\t\t\tsummary.scores.samples = scratch.scores.length;\n\t\t\tif (0 < scratch.scores.length) {\n\t\t\t\tsummary.scores.min = Math.min(...scratch.scores);\n\t\t\t\tsummary.scores.max = Math.max(...scratch.scores);\n\t\t\t\tsummary.scores.median = percentile(scratch.scores, 0.5);\n\t\t\t}\n\t\t}\n\n\t\tif (summary.turnServers && scratch) {\n\t\t\tsummary.turnServers.clientsRelayed = scratch.turnClientIds.size;\n\t\t}\n\t}\n\n\t/** Drop every bus subscription. Called when the observer closes. */\n\tpublic close(): void {\n\t\tif (this._closed) return;\n\t\tthis._closed = true;\n\n\t\tfor (const { event, listener } of this._listeners) {\n\t\t\tthis._observer.off(event, listener as never);\n\t\t}\n\t\tthis._listeners.length = 0;\n\t}\n\n\t/**\n\t * Subscribe `listener` to `event`, routed to the summary of the call the event names.\n\t *\n\t * The `observedCall` is read off the payload rather than closed over, which is what lets one\n\t * subscription serve every call.\n\t */\n\tprivate _on<K extends CallScopedEventName>(\n\t\tevent: K,\n\t\thandler: (summary: CallSummary, scratch: CallScratch, ...args: ObserverEvents[K]) => void,\n\t): void {\n\t\tconst listener = (...args: ObserverEvents[K]) => {\n\t\t\tconst summary = (args[0] as { observedCall: ObservedCall }).observedCall.summary;\n\n\t\t\tif (!summary) return;\n\n\t\t\tconst scratch = this._scratch.get(summary);\n\n\t\t\tif (!scratch) return;\n\n\t\t\ttry {\n\t\t\t\thandler(summary, scratch, ...args);\n\t\t\t} catch (err) {\n\t\t\t\t// A summary is a side-channel. Nothing about a call should break because a field could\n\t\t\t\t// not be recorded — least of all an application's own enricher.\n\t\t\t\tlogger.warn('A call-summary handler for %s threw; continuing. %o', event, err);\n\t\t\t}\n\t\t};\n\n\t\tthis._observer.on(event, listener as never);\n\t\tthis._listeners.push({ event, listener: listener as never });\n\t}\n\n\tprivate _subscribeBuiltIns(): void {\n\t\tconst include = this._config.include;\n\n\t\tif (include.includes('clients')) {\n\t\t\tthis._on('client-added', (summary, _scratch, { observedCall, observedClient }) => {\n\t\t\t\tconst clients = summary.clients;\n\n\t\t\t\tif (!clients) return;\n\n\t\t\t\tclients.joined += 1;\n\t\t\t\tclients.peak = Math.max(clients.peak, observedCall.observedClients.size);\n\n\t\t\t\tif (clients.clientIds.length < this._config.maxClientIds) {\n\t\t\t\t\tclients.clientIds.push(observedClient.clientId);\n\t\t\t\t} else {\n\t\t\t\t\tsummary.truncated = { ...summary.truncated, clientIds: (summary.truncated?.clientIds ?? 0) + 1 };\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tthis._on('client-closed', (summary) => {\n\t\t\t\tif (summary.clients) summary.clients.left += 1;\n\t\t\t});\n\t\t}\n\n\t\tif (include.includes('issues')) {\n\t\t\tthis._on('call-issue', (summary, _scratch, { issue }) => {\n\t\t\t\tconst issues = summary.issues;\n\n\t\t\t\tif (!issues) return;\n\n\t\t\t\t// Past the cap the issue is dropped, but the fact that it existed is not: `truncated`\n\t\t\t\t// carries the shortfall, so the true count stays recoverable from a bounded summary.\n\t\t\t\tif (issues.length < this._config.maxIssues) issues.push(issue);\n\t\t\t\telse summary.truncated = { ...summary.truncated, issues: (summary.truncated?.issues ?? 0) + 1 };\n\t\t\t});\n\t\t}\n\n\t\tif (include.includes('scores') || include.includes('turnServers')) {\n\t\t\tthis._on('call-updated', (summary, scratch, { observedCall }) => {\n\t\t\t\tif (summary.scores && observedCall.score !== undefined) scratch.scores.push(observedCall.score);\n\n\t\t\t\tif (!summary.turnServers) return;\n\n\t\t\t\tfor (const clientId of observedCall.clientsUsedTurn) scratch.turnClientIds.add(clientId);\n\n\t\t\t\tfor (const client of observedCall.observedClients.values()) {\n\t\t\t\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\t\t\t\tfor (const pair of peerConnection.selectedIceCandiadtePairForTurn) {\n\t\t\t\t\t\t\tconst url = pair.getLocalCandidate()?.url;\n\n\t\t\t\t\t\t\tif (url && !summary.turnServers.serverUrls.includes(url)) {\n\t\t\t\t\t\t\t\tsummary.turnServers.serverUrls.push(url);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate _subscribeEnrichers(): void {\n\t\tconst enrich = this._config.enrich;\n\n\t\tif (!enrich) return;\n\n\t\tfor (const name of Object.keys(enrich) as CallScopedEventName[]) {\n\t\t\tconst enricher = enrich[name];\n\n\t\t\tif (!enricher) continue;\n\n\t\t\tthis._on(name, (summary, _scratch, ...args) => (enricher as (s: CallSummary, ...a: unknown[]) => void)(summary, ...args));\n\t\t}\n\t}\n}\n","import { Detector, Observer } from '..';\nimport { median, robustZScore } from '../utils/stats';\nimport { ActiveClientIssue } from '../issues/ActiveClientIssue';\nimport { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\n\n/**\n * One completed sampling bucket: how many/which clients reported congestion during it.\n *\n * `totalClients` (and therefore `congestedClientRatio`) is a snapshot of `observer.numberOfClients`\n * taken when the bucket closes — an approximation of \"how many clients could have been congested\",\n * not a claim that every client sent exactly one sample within the bucket. Good enough for a ratio\n * that only needs to be comparable bucket-to-bucket.\n */\nexport type SfuCongestionDetectorBucket = {\n\tobservedAt: number;\n\ttotalClients: number;\n\tcongestedClients: number;\n\tcongestedClientRatio: number;\n\taffectedClientIds: string[];\n\taffectedCallIds: string[];\n};\n\nexport type SfuCongestionDetectorReport = {\n\taffectedCallIds: string[];\n\taffectedClientIds: string[];\n\tcongestedClientRatio: number;\n\ttotalNumberOfClients: number;\n\tnumberOfCongestedClients: number;\n\t// the number of completed buckets kept in history at the time of detection\n\thistorySize: number;\n\t// diagnostics: the baseline this bucket was judged against, and by how much it cleared it\n\tbaselineCongestedClientRatio: number;\n\trobustZ: number;\n\tabsoluteIncrease: number;\n\trelativeIncrease: number;\n}\n\nexport type SfuCongestionDetectorConfig = {\n\n\t/**\n\t * Client issue types counted as \"congestion\" for this indicator. Default `[ 'congestion' ]`.\n\t *\n\t * Keep this narrow. Every type you add widens what counts as a congested client, and the whole\n\t * method rests on comparing *like with like* across time buckets — mixing in a type that appears\n\t * for unrelated reasons raises the baseline and buries the spike you are looking for.\n\t */\n\tconsumedClientIssueTypes: string[];\n\n\t/** The `observer-issue` type raised when a bucket is judged congested. Default `'sfu-congestion'`. */\n\temittedObserverIssueType: string;\n\n\t/**\n\t * How long your clients take to send a sample (ms). Default `10_000`.\n\t *\n\t * **Set this to your collector's actual sampling period** — it is a description of your clients,\n\t * not a tuning knob. The bucket is `samplesSendingTimeInMs * 2`, so every client gets a fair\n\t * chance to report at least once inside each bucket. Set it too short and clients that simply had\n\t * not reported yet look absent, so the ratio jumps around on sampling noise; too long and the\n\t * detector reacts slowly and averages a spike away.\n\t */\n\tsamplesSendingTimeInMs: number;\n\n\t/**\n\t * Completed buckets kept as history — the candidate plus its baseline. Default `30`.\n\t *\n\t * At the default bucket size this is ~10 minutes of baseline. Sensible range `10`–`60`. Longer is\n\t * more robust to a single odd bucket but slower to accept a genuinely changed normal (a growth\n\t * spurt, a new region coming online); shorter adapts quickly but lets a sustained problem become\n\t * the new baseline and stop being reported.\n\t */\n\thistorySize: number;\n\n\t/**\n\t * Buckets required before any judgement is made. Default `5`.\n\t *\n\t * Below this the detector is silent, which is the point: a median and MAD over two buckets is not\n\t * a baseline. Costs `minHistorySize * samplesSendingTimeInMs * 2` of warm-up after start — about\n\t * 100 s at the defaults. Do not lower it to make a test fire faster; shorten the bucket instead.\n\t */\n\tminHistorySize: number;\n\n\t/**\n\t * Distinct congested clients required in the candidate bucket. Default `3`.\n\t *\n\t * The absolute floor beneath every ratio below, so that a tiny fleet cannot produce a finding: two\n\t * unhappy clients out of four is 50% and means nothing. Raise it on a large fleet where three\n\t * clients is always noise.\n\t */\n\tminAffectedClients: number;\n\n\t/**\n\t * How far the candidate's congested-client **ratio** must exceed the baseline median, in absolute\n\t * terms (`0`–`1`). Default `0.05`, i.e. five percentage points.\n\t *\n\t * This is the practical-significance gate: it stops a statistically striking move from 0.5% to 2%\n\t * being reported as an event. Typical `0.03`–`0.15`.\n\t */\n\tminAbsoluteRatioIncrease: number;\n\n\t/**\n\t * How many times the baseline median the candidate ratio must reach. Default `2`.\n\t *\n\t * Multiplicative counterpart to the absolute gate — both must pass. Typical `1.5`–`3`. Below\n\t * `1.5` ordinary fluctuation qualifies; above ~`4` only near-total events do.\n\t */\n\tminRelativeRatioIncrease: number;\n\n\t/**\n\t * Robust z-score the candidate must reach against a median+MAD baseline. Default `3`.\n\t *\n\t * The statistical-significance gate. `3` is the conventional \"clearly outside normal variation\";\n\t * `2` is noticeably chattier, `4`–`5` only for very stable fleets. Median and MAD rather than mean\n\t * and standard deviation on purpose — a couple of past incidents in the history would inflate a\n\t * standard deviation enough to hide the next one. Note that a perfectly flat baseline gives\n\t * `MAD = 0`, where any increase scores `Infinity`; the two ratio gates above are what keep that\n\t * honest.\n\t */\n\trobustZThreshold: number;\n}\n\n/** The statistical/practical-significance verdict for one candidate bucket against its baseline. */\nexport type SfuCongestionDetectorEvaluation = {\n\tisCongested: boolean;\n\tbaselineCongestedClientRatio: number;\n\trobustZ: number;\n\tabsoluteIncrease: number;\n\trelativeIncrease: number;\n};\n\n/**\n * Detects a **shared** congestion event: many clients, across different calls, reporting congestion\n * inside the same slice of time.\n *\n * Only add this when the observer's calls all come from the **same SFU** — the finding's whole\n * meaning is \"these clients have nothing in common except that server\", and that is only true if the\n * server really is the common factor.\n *\n * ### Why fixed-interval buckets, and not the update tick\n *\n * The obvious implementation counts congested clients on each `update()`. It is wrong here, for two\n * separate reasons:\n *\n * - **The tick is not evenly spaced.** `update()` fires when a client is updated, so its rate is a\n * function of how many clients are connected and how their sampling happens to interleave. Two\n * counts taken from windows of different length are not comparable, and this detector's entire\n * job is to compare a count against earlier counts.\n * - **Clients report on their own schedule.** A client sends a sample roughly every\n * `samplesSendingTimeInMs`, unsynchronised with every other client. A window shorter than that\n * systematically undercounts — half the congested clients simply hadn't spoken yet — and the\n * undercount varies with arrival phase, which is noise indistinguishable from signal.\n *\n * So the detector runs on a wall-clock interval and closes a bucket every\n * `samplesSendingTimeInMs`, giving every client a fair chance to be heard in each one. Buckets are\n * equal-length and equally lagged, which is what makes bucket-to-bucket comparison mean something.\n * {@link update} is deliberately empty: nothing here is driven by the update tick.\n *\n * ### Occurrences, not intervals\n *\n * Unlike `ConcurrentIssueDetector`, this one ignores resolutions — see {@link delete}. It counts how\n * many *distinct clients reported* congestion in a bucket, not how many are still congested. A\n * client that hits congestion and immediately drops its bitrate resolves the issue within seconds\n * and would vanish from an open-interval view, yet it is exactly the evidence wanted here.\n */\nexport class SfuCongestionDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'sfu-congestion-detector';\n\n\tpublic readonly name = SfuCongestionDetector.NAME;\n\n\tprivate readonly _config: SfuCongestionDetectorConfig;\n\tprivate readonly _history: SfuCongestionDetectorBucket[] = [];\n\tprivate readonly _trackedIssues = new Set<ActiveClientIssue>();\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\tprivate _lastEvaluatedBucket: SfuCongestionDetectorBucket | undefined;\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tconfig: Partial<SfuCongestionDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tconsumedClientIssueTypes: [\n\t\t\t\t'congestion'\n\t\t\t],\n\t\t\temittedObserverIssueType: 'sfu-congestion',\n\t\t\tsamplesSendingTimeInMs: 10_000,\n\t\t\thistorySize: 30,\n\t\t\tminHistorySize: 5,\n\t\t\tminAffectedClients: 3,\n\t\t\tminAbsoluteRatioIncrease: 0.05,\n\t\t\tminRelativeRatioIncrease: 2,\n\t\t\trobustZThreshold: 3,\n\t\t\t...config,\n\t\t};\n\n\t\tfor (const issueType of this._config.consumedClientIssueTypes) {\n\t\t\tthis._observer.activeIssuesRegistry.addIssueTracker(issueType, this);\n\t\t}\n\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis._closeBucket(Date.now());\n\t\t\tthis._evaluateLatestBucket();\n\t\t}, this._config.samplesSendingTimeInMs);\n\n\t\t// Do not hold the process open. This is a monitoring side-channel: if the application has\n\t\t// nothing else to do, it should be allowed to exit, and without this a library import alone\n\t\t// keeps Node alive forever. Optional-called because some fake-timer implementations return a\n\t\t// handle without `unref`, and a monitoring detail must never break a caller's test run.\n\t\tthis.timer.unref?.();\n\t}\n\tpublic get size() {\n\t\treturn this._trackedIssues.size;\n\t}\n\n\t/** Record the issue against the bucket currently open. The timer, not this, closes the bucket. */\n\tpublic add(issue: ActiveClientIssue): void {\n\t\tthis._trackedIssues.add(issue);\n\t}\n\n\t/**\n\t * Deliberately a no-op returning `false`.\n\t *\n\t * Resolutions are not interesting here. A congested client typically fixes its own symptom by\n\t * dropping bitrate hard, so the issue closes within seconds — but it still *happened*, and it is\n\t * evidence that the server was under pressure during this bucket. What matters is how many\n\t * distinct clients reported congestion within the bucket and whether that count suddenly jumps,\n\t * not how long any one client's issue stayed open.\n\t *\n\t * Nothing leaks: the tracked set is emptied wholesale every time a bucket closes.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\tpublic delete(_issue: ActiveClientIssue): boolean {\n\t\treturn false;\n\t}\n\n\tpublic clear(): void {\n\t\tthis._trackedIssues.clear();\n\t\tthis._history.length = 0;\n\t\tthis._lastEvaluatedBucket = undefined;\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this._trackedIssues.has(issue);\n\t}\n\n\tpublic close(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\n\t\t// Unsubscribe, or the registry keeps feeding a detector that has stopped rotating its buckets —\n\t\t// `_trackedIssues` would then grow for the life of the observer.\n\t\tthis._observer.activeIssuesRegistry.removeIssueTracker(this);\n\n\t\tthis.clear();\n\t}\n\n\t/** The completed buckets kept so far, oldest first. Read-only — for introspection/tests. */\n\tpublic get history(): readonly SfuCongestionDetectorBucket[] {\n\t\treturn this._history;\n\t}\n\n\t/**\n\t * Intentionally empty — see the class description.\n\t *\n\t * Everything here is driven by the bucket timer, because the update tick is neither evenly spaced\n\t * nor long enough for every client to have reported. Counting on it would compare windows of\n\t * different lengths and call the difference a signal.\n\t */\n\tpublic update(): void {\n\t\t// no-op\n\t}\n\n\tprivate _closeBucket(observedAt: number): void {\n\t\tconst totalClients = this._observer.numberOfClients;\n\t\tconst affectedClientIds = [ ...new Set(Array.from(this._trackedIssues, (issue) => issue.clientId)) ];\n\t\tconst affectedCallIds = [ ...new Set(Array.from(this._trackedIssues, (issue) => issue.callId)) ];\n\t\tconst congestedClients = affectedClientIds.length;\n\t\tconst congestedClientRatio = 0 < totalClients ? congestedClients / totalClients : 0;\n\n\t\tthis._history.push({\n\t\t\tobservedAt,\n\t\t\ttotalClients,\n\t\t\tcongestedClients,\n\t\t\tcongestedClientRatio,\n\t\t\taffectedClientIds,\n\t\t\taffectedCallIds,\n\t\t});\n\n\t\twhile (this._config.historySize < this._history.length) this._history.shift();\n\n\t\tthis._trackedIssues.clear();\n\t}\n\n\t/**\n\t * Evaluate only the latest completed bucket (the candidate) against the buckets before it (the\n\t * baseline) — never against itself. Reached once per newly-closed bucket via {@link update}; the\n\t * identity check below additionally guards against evaluating the same bucket twice, in case\n\t * `update()` is ever called again before the next rotation.\n\t */\n\tprivate _evaluateLatestBucket(): void {\n\t\tconst candidate = this._history[this._history.length - 1];\n\n\t\tif (candidate === this._lastEvaluatedBucket) return;\n\t\tthis._lastEvaluatedBucket = candidate;\n\n\t\tconst baseline = this._history.slice(0, -1);\n\t\tconst evaluation = this._evaluateBucket(candidate, baseline);\n\n\t\tif (!evaluation.isCongested) return;\n\n\t\tconst payload: SfuCongestionDetectorReport = {\n\t\t\taffectedCallIds: candidate.affectedCallIds,\n\t\t\taffectedClientIds: candidate.affectedClientIds,\n\t\t\tcongestedClientRatio: candidate.congestedClientRatio,\n\t\t\ttotalNumberOfClients: candidate.totalClients,\n\t\t\tnumberOfCongestedClients: candidate.congestedClients,\n\t\t\thistorySize: this._history.length,\n\t\t\tbaselineCongestedClientRatio: evaluation.baselineCongestedClientRatio,\n\t\t\trobustZ: evaluation.robustZ,\n\t\t\tabsoluteIncrease: evaluation.absoluteIncrease,\n\t\t\trelativeIncrease: evaluation.relativeIncrease,\n\t\t};\n\n\t\t// Through `addIssue`, not a raw `emit`: that is what stamps `scope` and keeps this finding\n\t\t// indistinguishable in shape from every other observer-scoped one.\n\t\tthis._observer.addIssue({\n\t\t\ttype: this._config.emittedObserverIssueType,\n\t\t\ttimestamp: Date.now(),\n\t\t\tpayload: { ...payload },\n\t\t});\n\t}\n\n\t/**\n\t * Is `candidate` — the latest completed bucket — abnormally high compared with the `baseline`\n\t * buckets before it?\n\t *\n\t * Requires both **statistical** significance (a robust z-score against a median+MAD baseline —\n\t * deliberately not Mann-Kendall, which asks \"is this a monotonic trend\", not \"is the latest point\n\t * an outlier\"; a single sudden spike on an otherwise flat series is exactly what should trigger\n\t * here and exactly what a trend test would miss) and **practical** significance (enough affected\n\t * clients, and a big enough absolute/relative jump — a statistically significant move in a tiny\n\t * or trivial ratio is not worth an alert).\n\t */\n\tprivate _evaluateBucket(candidate: SfuCongestionDetectorBucket, baseline: SfuCongestionDetectorBucket[]): SfuCongestionDetectorEvaluation {\n\t\tconst baselineRatios = baseline.map((bucket) => bucket.congestedClientRatio);\n\t\tconst baselineMedian = median(baselineRatios) ?? 0;\n\t\tconst robustZ = robustZScore(candidate.congestedClientRatio, baselineRatios) ?? 0;\n\t\tconst absoluteIncrease = candidate.congestedClientRatio - baselineMedian;\n\t\tconst relativeIncrease = 0 < baselineMedian\n\t\t\t? candidate.congestedClientRatio / baselineMedian\n\t\t\t: (0 < candidate.congestedClientRatio ? Infinity : 0);\n\n\t\tconst isCongested = this._config.minHistorySize <= baseline.length + 1\n\t\t\t// Practical significance first: these are cheap, and they are what stop a statistically\n\t\t\t// perfect signal over three clients from waking anyone.\n\t\t\t&& this._config.minAffectedClients <= candidate.congestedClients\n\t\t\t&& this._config.minAbsoluteRatioIncrease <= absoluteIncrease\n\t\t\t&& this._config.minRelativeRatioIncrease <= relativeIncrease\n\t\t\t&& this._config.robustZThreshold <= robustZ;\n\n\t\treturn { isCongested, baselineCongestedClientRatio: baselineMedian, robustZ, absoluteIncrease, relativeIncrease };\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { Observer } from '../Observer';\nimport type { ActiveClientIssue } from '../issues/ActiveClientIssue';\nimport type { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\nimport { concludeObserverIssue } from './IssueConclusion';\n\nexport const ObserverConcurrentIssueTypes = {\n\t/**\n\t * The same issue is open in **several unrelated calls at once**. Those clients share no meeting,\n\t * no publisher and no room — only the infrastructure serving them.\n\t */\n\tcrossCallConcurrentIssues: 'CROSS_CALL_CONCURRENT_ISSUES',\n\n\t/** The cross-call version that also started together. The strongest \"it's us\" signal available. */\n\tcrossCallIssueOnsetBurst: 'CROSS_CALL_ISSUE_ONSET_BURST',\n} as const;\n\nexport type ObserverConcurrentIssueDetectorConfig = {\n\n\t/**\n\t * The issue types to watch. **Required, and must not be empty** — the detector subscribes to\n\t * exactly these and sees nothing else.\n\t */\n\tissueTypes: string[];\n\n\t/**\n\t * Distinct clients that must share the open issue. Default `3`.\n\t *\n\t * Absolute, deliberately — see `affectedCallRatioThreshold` for why no client *ratio* exists at\n\t * this scope. Sensible range `3`–`10`; scale it with fleet size, since three clients is a real\n\t * signal across five calls and background noise across five hundred.\n\t */\n\tminAffectedClients: number;\n\n\t/**\n\t * Minimum number of *distinct calls* the affected clients must span. Default `2`.\n\t *\n\t * This is what makes an observer-scoped finding mean something a call-scoped one doesn't. Without\n\t * it, one thirty-person meeting with congestion satisfies every client-count threshold and raises\n\t * a fleet-wide alert for what is really one bad room — which `CallConcurrentIssueDetector` has\n\t * already reported. Requiring two or more independent calls is the difference between a\n\t * coincidence and a shared cause.\n\t */\n\tminAffectedCalls: number;\n\n\t/**\n\t * Fraction of the calls in flight that must be affected. Default `0` — off, because absolute\n\t * counts matter more than ratios here: three broken calls out of a thousand is still worth\n\t * knowing about, and a ratio threshold would hide it. Raise it if you only care about fleet-wide\n\t * events.\n\t *\n\t * Note there is deliberately **no participant ratio** at this scope. Six broken calls out of forty\n\t * is a handful of clients against the whole fleet, so any meaningful client ratio would suppress\n\t * exactly the finding this detector exists to produce.\n\t */\n\taffectedCallRatioThreshold: number;\n\n\t/**\n\t * Onsets falling within this span (ms) escalate the finding to `CROSS_CALL_ISSUE_ONSET_BURST`.\n\t * Default `2_000`.\n\t *\n\t * This is the strongest evidence the detector produces: independent calls starting to fail *at the\n\t * same instant* has no explanation other than something they share. Keep it at or above your\n\t * sampling period — onsets are only as precise as clients report them — and no wider than a few\n\t * seconds, or unrelated failures drift into the same window. Typical `1_000`–`5_000`.\n\t */\n\tonsetBurstWindowInMs: number;\n\n\t/**\n\t * Re-arm time per issue type (ms). Default `60_000`.\n\t *\n\t * Typical `60_000`–`600_000`. A fleet-wide event is one incident: without a cooldown a sustained\n\t * outage would raise an issue on every tick for its whole duration.\n\t */\n\tcooldownMs: number;\n};\n\n/** What the detector currently knows about one issue type across the fleet. */\nexport type ObserverConcurrentIssueGroup = {\n\ttype: string;\n\tissues: ActiveClientIssue[];\n\tclientIds: string[];\n\ttotalClients: number;\n\taffectedRatio: number;\n\tcallIds: string[];\n\ttotalCalls: number;\n\taffectedCallRatio: number;\n\n\t/** Per-call breakdown, largest first — the first question anyone asks is \"which calls, how badly?\". */\n\tperCall: { callId: string, affectedClients: number, totalClients: number }[];\n\n\t/**\n\t * Spread of the onsets, in **observer** time (ms). Client clocks are never compared across\n\t * machines here — skew between them would masquerade as a synchronized event.\n\t */\n\tonsetSpreadInMs: number;\n\tfirstObservedAt: number;\n};\n\n/**\n * Answers **\"is our infrastructure in trouble?\"** — the same issue open across several *unrelated*\n * calls at the same moment.\n *\n * This is not the call-scoped question with a bigger denominator, which is why it is a separate\n * detector with separate gates and its own finding types. Participant count alone is a bad fleet\n * signal: one thirty-person meeting where everybody is congested clears every client threshold, yet\n * it has an obvious local explanation. Clients in *different* calls share no room, no publisher and\n * no host — only the servers and the network. When the same issue opens across several of them at\n * once, the infrastructure is the only remaining common factor, and that is the finding worth paging\n * someone about.\n *\n * ```ts\n * observer.addObserverDetector('observer-concurrent-issue-detector', {\n * issueTypes: [ 'congestion', 'ice-disconnected' ],\n * minAffectedCalls: 3,\n * });\n *\n * observer.on('observer-issue', ({ issue }) => {\n * if (issue.type === ObserverConcurrentIssueTypes.crossCallIssueOnsetBurst) page(issue);\n * });\n * // → CROSS_CALL_ISSUE_ONSET_BURST { issueType: 'congestion', calls: 40, affectedCalls: 6, … }\n * ```\n *\n * Onsets are compared on the **observer clock** (`observedAt`), never on client clocks: participants\n * degrading together within a couple of seconds is far more likely to be a deploy, a TURN failover or\n * a link flap than a coincidence — but only if the timestamps being compared came from one clock.\n */\nexport class ObserverConcurrentIssueDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'observer-concurrent-issue-detector' as const;\n\n\tpublic readonly name = ObserverConcurrentIssueDetector.NAME;\n\n\tprivate readonly _config: ObserverConcurrentIssueDetectorConfig;\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\t/** issue type -> the issues of that type currently open anywhere in the fleet. */\n\tprivate readonly _byType = new Map<string, Set<ActiveClientIssue>>();\n\tprivate _size = 0;\n\n\t/** The groups that qualified on the most recent `update()`. Exposed for tests/dashboards. */\n\tpublic lastGroups: ObserverConcurrentIssueGroup[] = [];\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tconfig: Partial<ObserverConcurrentIssueDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tissueTypes: [],\n\t\t\tminAffectedClients: 3,\n\t\t\tminAffectedCalls: 2,\n\t\t\taffectedCallRatioThreshold: 0,\n\t\t\tonsetBurstWindowInMs: 2_000,\n\t\t\tcooldownMs: 60_000,\n\t\t\t...config,\n\t\t};\n\n\t\tfor (const type of this._config.issueTypes) {\n\t\t\tthis._observer.activeIssuesRegistry.addIssueTracker(type, this);\n\t\t}\n\t}\n\n\tpublic get size(): number {\n\t\treturn this._size;\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this._byType.get(issue.type)?.has(issue) ?? false;\n\t}\n\n\tpublic add(issue: ActiveClientIssue): void {\n\t\tlet bucket = this._byType.get(issue.type);\n\n\t\tif (!bucket) {\n\t\t\tbucket = new Set();\n\t\t\tthis._byType.set(issue.type, bucket);\n\t\t}\n\t\tif (bucket.has(issue)) return;\n\n\t\tbucket.add(issue);\n\t\t++this._size;\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\tconst bucket = this._byType.get(issue.type);\n\n\t\tif (!bucket?.delete(issue)) return false;\n\n\t\t--this._size;\n\t\tif (bucket.size === 0) this._byType.delete(issue.type);\n\n\t\treturn true;\n\t}\n\n\tpublic clear(): void {\n\t\tthis._byType.clear();\n\t\tthis._size = 0;\n\t\tthis.lastGroups = [];\n\t}\n\n\tpublic update(): void {\n\t\tthis.lastGroups = [];\n\n\t\tif (this._size === 0) return;\n\n\t\tconst now = Date.now();\n\n\t\tfor (const [ type, issues ] of this._byType) {\n\t\t\tconst group = this._groupOf(type, issues);\n\n\t\t\tif (group.clientIds.length < this._config.minAffectedClients) continue;\n\t\t\tif (group.callIds.length < this._config.minAffectedCalls) continue;\n\t\t\tif (group.affectedCallRatio < this._config.affectedCallRatioThreshold) continue;\n\n\t\t\tthis.lastGroups.push(group);\n\n\t\t\tif (now - (this._lastRaisedAt.get(type) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(type, now);\n\n\t\t\tconst burst = group.onsetSpreadInMs <= this._config.onsetBurstWindowInMs;\n\t\t\tconst issueType = burst\n\t\t\t\t? ObserverConcurrentIssueTypes.crossCallIssueOnsetBurst\n\t\t\t\t: ObserverConcurrentIssueTypes.crossCallConcurrentIssues;\n\t\t\tconst conclusion = concludeObserverIssue({\n\t\t\t\tissueType: type,\n\t\t\t\taffectedClients: group.clientIds.length,\n\t\t\t\ttotalClients: group.totalClients,\n\t\t\t\taffectedCalls: group.callIds.length,\n\t\t\t\ttotalCalls: group.totalCalls,\n\t\t\t\tonsetBurst: burst,\n\t\t\t});\n\n\t\t\tthis._observer.addIssue({\n\t\t\t\ttype: issueType,\n\t\t\t\ttimestamp: now,\n\t\t\t\tconclusion,\n\t\t\t\tpayload: {\n\t\t\t\t\tissueType: type,\n\t\t\t\t\tclients: group.totalClients,\n\t\t\t\t\taffectedClients: group.clientIds.length,\n\t\t\t\t\taffectedRatio: group.affectedRatio,\n\t\t\t\t\taffectedClientIds: group.clientIds,\n\t\t\t\t\t// The dimension that makes this finding what it is.\n\t\t\t\t\tcalls: group.totalCalls,\n\t\t\t\t\taffectedCalls: group.callIds.length,\n\t\t\t\t\taffectedCallRatio: group.affectedCallRatio,\n\t\t\t\t\taffectedCallIds: group.callIds,\n\t\t\t\t\tperCall: group.perCall,\n\t\t\t\t\tonsetSpreadInMs: group.onsetSpreadInMs,\n\t\t\t\t\tonsetBurst: burst,\n\t\t\t\t\tfirstObservedAt: group.firstObservedAt,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._observer.activeIssuesRegistry.removeIssueTracker(this);\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.clear();\n\t}\n\n\tprivate _groupOf(type: string, issues: Set<ActiveClientIssue>): ObserverConcurrentIssueGroup {\n\t\tconst clientIds = new Set<string>();\n\t\tconst affectedByCall = new Map<string, Set<string>>();\n\t\tconst list: ActiveClientIssue[] = [];\n\t\tlet earliest = Infinity;\n\t\tlet latest = -Infinity;\n\n\t\tfor (const issue of issues) {\n\t\t\tlist.push(issue);\n\t\t\tclientIds.add(issue.clientId);\n\n\t\t\tlet clients = affectedByCall.get(issue.callId);\n\n\t\t\tif (!clients) {\n\t\t\t\tclients = new Set();\n\t\t\t\taffectedByCall.set(issue.callId, clients);\n\t\t\t}\n\t\t\tclients.add(issue.clientId);\n\n\t\t\tif (issue.observedAt < earliest) earliest = issue.observedAt;\n\t\t\tif (latest < issue.observedAt) latest = issue.observedAt;\n\t\t}\n\n\t\tconst totalClients = this._observer.numberOfClients;\n\t\tconst totalCalls = this._observer.observedCalls.size;\n\t\tconst perCall: ObserverConcurrentIssueGroup['perCall'] = [];\n\n\t\tfor (const [ callId, clients ] of affectedByCall) {\n\t\t\tperCall.push({\n\t\t\t\tcallId,\n\t\t\t\taffectedClients: clients.size,\n\t\t\t\t// A call that closed while its issues were still open has no participant count left;\n\t\t\t\t// fall back to the affected count rather than reporting a 0 denominator.\n\t\t\t\ttotalClients: this._observer.observedCalls.get(callId)?.observedClients.size ?? clients.size,\n\t\t\t});\n\t\t}\n\t\tperCall.sort((a, b) => b.affectedClients - a.affectedClients);\n\n\t\treturn {\n\t\t\ttype,\n\t\t\tissues: list,\n\t\t\tclientIds: [ ...clientIds ],\n\t\t\ttotalClients,\n\t\t\taffectedRatio: 0 < totalClients ? clientIds.size / totalClients : 0,\n\t\t\tcallIds: perCall.map((entry) => entry.callId),\n\t\t\ttotalCalls,\n\t\t\taffectedCallRatio: 0 < totalCalls ? perCall.length / totalCalls : 0,\n\t\t\tperCall,\n\t\t\tonsetSpreadInMs: latest - earliest,\n\t\t\tfirstObservedAt: earliest,\n\t\t};\n\t}\n}\n","/** A point on the earth, as an application reports it for one client. */\nexport type ClientLocation = {\n\tlatitude: number;\n\tlongitude: number;\n};\n\nconst BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';\n\n/**\n * Approximate cell width at each geohash length, for choosing a precision.\n *\n * Index is the character count; the value is the rough cell size at the equator. Cells are taller\n * than they are wide at high latitudes, so treat these as an order of magnitude, not a radius.\n */\nexport const GEOHASH_CELL_SIZES = [\n\t'',\n\t'~5000 km',\n\t'~1250 km',\n\t'~156 km',\n\t'~39 km',\n\t'~5 km',\n\t'~1.2 km',\n\t'~150 m',\n] as const;\n\n/**\n * Encode a point as a geohash of `precision` characters — a **grid cell key**, not a cluster.\n *\n * ### Why cells rather than \"within N kilometres\"\n *\n * Grouping clients \"within a radius\" sounds like the natural thing and is a much worse fit. It is a\n * clustering problem, not a keying one: the groups depend on which client you start from, two\n * clients can each be within the radius of a third but not of each other, group identity is not\n * stable as participants join and leave, and maintaining it costs pairwise distance work. None of\n * that survives contact with a detector that has to produce the *same* group name on every tick so\n * a cooldown and a control group mean anything.\n *\n * A geohash prefix is a plain function of the coordinates: O(1), stable for the life of the client,\n * and usable directly as a population label. The honest cost is that a cell boundary can separate\n * two clients who are physically adjacent, which splits a real group into two smaller ones. That\n * biases towards **missing** a finding rather than inventing one, which is the right direction for\n * something that raises issues.\n *\n * Returns `undefined` for coordinates that are not finite or not on the earth, rather than encoding\n * nonsense into a plausible-looking cell key.\n */\nexport function geohash(location: ClientLocation, precision = 3): string | undefined {\n\tconst { latitude, longitude } = location;\n\n\tif (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return undefined;\n\tif (latitude < -90 || 90 < latitude) return undefined;\n\tif (longitude < -180 || 180 < longitude) return undefined;\n\tif (!Number.isFinite(precision) || precision < 1) return undefined;\n\n\tconst chars = Math.min(12, Math.floor(precision));\n\tconst latRange = { min: -90, max: 90 };\n\tconst lonRange = { min: -180, max: 180 };\n\n\tlet hash = '';\n\tlet bits = 0;\n\tlet value = 0;\n\tlet isLongitude = true;\n\n\twhile (hash.length < chars) {\n\t\t// Geohash interleaves longitude and latitude bits, longitude first.\n\t\tconst range = isLongitude ? lonRange : latRange;\n\t\tconst mid = (range.min + range.max) / 2;\n\t\tconst coordinate = isLongitude ? longitude : latitude;\n\n\t\tif (mid <= coordinate) {\n\t\t\tvalue = (value << 1) + 1;\n\t\t\trange.min = mid;\n\t\t} else {\n\t\t\tvalue <<= 1;\n\t\t\trange.max = mid;\n\t\t}\n\n\t\tisLongitude = !isLongitude;\n\n\t\tif (++bits === 5) {\n\t\t\thash += BASE32[value];\n\t\t\tbits = 0;\n\t\t\tvalue = 0;\n\t\t}\n\t}\n\n\treturn hash;\n}\n","import type { Detector } from './Detector';\nimport type { Observer } from '../Observer';\nimport type { ObservedClient } from '../ObservedClient';\nimport type { ActiveClientIssue } from '../issues/ActiveClientIssue';\nimport type { ActiveIssueTracker } from '../issues/ActiveIssueTracker';\nimport { geohash, type ClientLocation } from '../utils/geohash';\nimport { createLogger } from '../common/logger';\n\nconst logger = createLogger('ClientPopulationIssueDetector');\n\nexport const ClientPopulationIssueTypes = {\n\t/** One issue type is concentrated on one client population while the rest of the fleet is fine. */\n\tclientPopulationIssue: 'CLIENT_POPULATION_ISSUE',\n} as const;\n\n/** The client attribute to group by. One axis per detector — see the class description. */\nexport type ClientPopulationAxis = 'browser' | 'engine' | 'platform' | 'operationSystem' | 'location';\n\n/**\n * Reads a client's coordinates, for `groupBy: 'location'`. **Required for that axis.**\n *\n * There is no coordinate field in `ClientSample`, so the shape is yours: read it off\n * `client.attachments`, off a custom meta item, or from an `appData` field your accept middleware\n * filled in. Return `undefined` for clients whose location you do not know — they are then excluded\n * from both the population and the control group, exactly like a client that never reported its\n * browser.\n */\nexport type ClientLocationResolver = (client: ObservedClient) => ClientLocation | undefined;\n\nexport type ClientPopulationIssueDetectorConfig = {\n\n\t/**\n\t * The issue types to watch. **Required, and must not be empty.**\n\t *\n\t * **Match the issue family to the axis.** On the endpoint axes (`browser`, `engine`, `platform`,\n\t * `operationSystem`) the types worth grouping are the ones an endpoint owns: `cpulimitation`,\n\t * `encoder-bottleneck`, `capture-bottleneck`, `stuck-decoder`, `video-decoder-overloaded`.\n\t * Grouping a *network* symptom by browser is a category error — `congestion` clusters by ISP and\n\t * geography, not by build, and the detector would happily report a browser correlation that is\n\t * really a \"most of our users are on Chrome\" artefact.\n\t *\n\t * On the `location` axis it is the other way round: group the network symptoms — `congestion`,\n\t * `ice-disconnected`, `unstable-ice-path` — and not the endpoint ones, since there is no reason a\n\t * decoder should stall by geography.\n\t */\n\tissueTypes: string[];\n\n\t/** Which client attribute to group by. Default `'browser'`. */\n\tgroupBy: ClientPopulationAxis;\n\n\t/**\n\t * Where to read a client's coordinates. **Required when `groupBy` is `'location'`**, ignored\n\t * otherwise. See {@link ClientLocationResolver}.\n\t */\n\tresolveClientLocation?: ClientLocationResolver;\n\n\t/**\n\t * Geohash characters to group locations by, i.e. how coarse a \"place\" is. Default `3` (~156 km).\n\t *\n\t * `2` ~1250 km, `3` ~156 km, `4` ~39 km, `5` ~5 km. Coarser cells hold more clients, which is what\n\t * makes a rate mean anything, so start coarse: a city-sized cell rarely has `minPopulationSize`\n\t * participants in it. See `utils/geohash` for why this is a grid cell and not a radius.\n\t */\n\tlocationPrecision: number;\n\n\t/**\n\t * Group by `name` only, or by `name + version`. Default `true` (include version).\n\t *\n\t * Version is usually the point: \"Chrome\" is not actionable, \"Chrome 141\" is, because it names a\n\t * thing that changed on a date. Set to `false` when comparing whole engines.\n\t */\n\tincludeVersion: boolean;\n\n\t/**\n\t * Clients in a population before its rate means anything. Default `20`.\n\t *\n\t * Higher than the other detectors' minimums on purpose: this one compares *rates*, and a rate over\n\t * five clients is not a rate. Sensible range `20`–`100`. On the `location` axis this is the field\n\t * most likely to silence the detector — a city-sized cell rarely holds twenty concurrent\n\t * participants, so reach for a coarser `locationPrecision` before lowering this.\n\t */\n\tminPopulationSize: number;\n\n\t/**\n\t * Affected clients required within the population. Default `5`.\n\t *\n\t * Checked independently of `affectedRatioThreshold`, so one unlucky user on a rare browser cannot\n\t * page anyone however striking the ratio looks. Sensible range `5`–`20`.\n\t */\n\tminAffectedClients: number;\n\n\t/**\n\t * Share of the population that must be affected, `0`–`1`. Default `0.3`.\n\t *\n\t * Lower than the per-call thresholds deliberately: an issue hitting 30% of one browser version while\n\t * the rest of the fleet is clean is already a strong signal, and endpoint faults rarely affect\n\t * *everyone* on a build. Typical `0.2`–`0.5`. This is the weakest of the gates —\n\t * `minRelativeRisk` is what makes the finding mean anything.\n\t */\n\taffectedRatioThreshold: number;\n\n\t/**\n\t * How many times worse the suspect population must be than the rest of the fleet. Default `3`.\n\t *\n\t * **This is the gate that makes the finding mean anything** — see the class description. Typical\n\t * `2`–`10`. At `2` you will see populations that are merely somewhat worse, which is often just a\n\t * different usage pattern; at `10` only stark, unambiguous concentrations survive. A spotless\n\t * control group yields `Infinity`, which clears any threshold, so the minimum-count gates above are\n\t * what stop that from being trivial.\n\t */\n\tminRelativeRisk: number;\n\n\t/**\n\t * Clients **outside** the suspect population before a comparison is possible. Default `20`.\n\t *\n\t * \"Worse than everyone else\" needs an everyone else. Sensible range `20`–`100`. Note the practical\n\t * consequence: a fleet that is overwhelmingly one browser can never have that browser reported,\n\t * because there is no control group left — which is honest, since at 95% Chrome you cannot separate a\n\t * Chrome fault from a fleet-wide one.\n\t */\n\tminControlSize: number;\n\n\t/**\n\t * Re-arm time per (population, issue type) in ms. Default `300_000`.\n\t *\n\t * Long by design: a bad client build is a condition lasting days, not an event, and the action it\n\t * prompts — ship a fix, roll back a version — is not one you take twice an hour. Typical\n\t * `300_000`–`3_600_000`.\n\t */\n\tcooldownMs: number;\n};\n\n/** The rollup for one population on one issue type. */\nexport type ClientPopulation = {\n\n\t/**\n\t * e.g. `'Chrome 141'`, or `'Chrome'` when `includeVersion` is off. For the `'location'` axis this\n\t * is the geohash cell — never the coordinates themselves, so an archived payload carries a place\n\t * at the configured resolution and not a person's position.\n\t */\n\tpopulation: string;\n\taxis: ClientPopulationAxis;\n\tissueType: string;\n\n\tclients: number;\n\taffectedClients: number;\n\taffectedRatio: number;\n\taffectedClientIds: string[];\n\n\t/** Everyone not in this population. */\n\tcontrolClients: number;\n\tcontrolAffectedClients: number;\n\tcontrolAffectedRatio: number;\n\n\t/** `affectedRatio / controlAffectedRatio`. `Infinity` when the control group is completely clean. */\n\trelativeRisk: number;\n};\n\n/**\n * Finds an issue that is concentrated on **one kind of client** — one browser, one browser version,\n * one OS — rather than on anything the servers own.\n *\n * ### Why this exists\n *\n * The other observer-scoped detectors all answer \"who else has this open, and what do they share?\"\n * with the answer *the infrastructure*, because clients in unrelated calls share nothing else. That\n * inference is right for network symptoms and **wrong for endpoint symptoms**, and the difference\n * matters at 3am. `cpulimitation` opening across six unrelated calls is not an SFU event: CPU is\n * owned by the endpoint, so what those endpoints have in common is a client release, a browser\n * update, or a fleet of identical VDI hosts. `IssueConclusion` already says exactly this — it maps\n * the endpoint-capacity family to a `client-population` fault domain instead of `infrastructure` —\n * but until now nothing in the library actually computed the grouping that claim refers to. This\n * detector is that computation.\n *\n * It is the one correlation in this library that is neither per-call nor per-server. A client knows\n * its own browser and nothing about anyone else's; only something sitting above the whole fleet can\n * notice that every complaint is coming from the same build.\n *\n * ### The control group is the whole point\n *\n * \"30% of Chrome 141 users report encoder-bottleneck\" is not a finding on its own. If 30% of\n * *everyone* reports it, Chrome 141 is not the story — you have a fleet-wide problem and this\n * detector would be pointing at the largest population rather than at a cause. Naive share-based\n * grouping always indicts whichever browser is most popular, which is why the gate here is\n * **relative risk**: the suspect population's rate divided by the rate among everyone else. A\n * population only qualifies when it is `minRelativeRisk` times worse than the rest of the fleet, and\n * only when the rest of the fleet is large enough (`minControlSize`) for \"the rest of the fleet\" to\n * be a real measurement.\n *\n * A completely clean control group gives `Infinity`, which is honest — nobody outside this\n * population has the problem at all — and is exactly why `minAffectedClients` and\n * `minPopulationSize` are checked independently, so a single unlucky user on a rare browser cannot\n * page anyone.\n *\n * ### One axis per detector\n *\n * `groupBy` takes a single attribute. Add a second instance if you want a second axis:\n *\n * ```ts\n * observer.addObserverDetector('client-population-issue-detector', {\n * issueTypes: [ 'cpulimitation', 'encoder-bottleneck', 'stuck-decoder' ],\n * groupBy: 'browser',\n * });\n *\n * observer.on('observer-issue', ({ issue }) => {\n * if (issue.type !== ClientPopulationIssueTypes.clientPopulationIssue) return;\n * // → { population: 'Chrome 141', issueType: 'encoder-bottleneck',\n * // affectedRatio: 0.34, controlAffectedRatio: 0.02, relativeRisk: 17 }\n * });\n * ```\n *\n * Deliberately not a cross-product of every axis at once: an issue that clusters on macOS *and* on\n * Safari is usually one fact reported twice, and a detector that emits both leaves the reader to\n * work out which one is causal. Pick the axis you want to reason about.\n *\n * ### The `location` axis\n *\n * With `groupBy: 'location'` the population is a **geohash cell** rather than a client attribute, so\n * the same machinery answers a different question: *is this symptom concentrated in one place?* That\n * is the grouping the note above says browsers cannot give you, and it is the one that matters for\n * network symptoms.\n *\n * The observer does not derive \"this client's RTT jumped\" — `client-monitor`'s `CongestionDetector`\n * already owns that verdict, comparing each peer connection's RTT against its own EWMA baseline and\n * requiring a bandwidth-limitation corroboration before it raises `congestion`. Absolute RTT is not\n * comparable between clients anyway: someone 200 ms away is *always* 200 ms away, so the only signal\n * is deviation from that client's own baseline, which is exactly what the client already measures.\n * This detector's contribution is the part no endpoint can see — that many of the affected clients\n * are in the same place at the same time.\n *\n * ```ts\n * observer.addObserverDetector('client-population-issue-detector', {\n * issueTypes: [ 'congestion', 'ice-disconnected' ],\n * groupBy: 'location',\n * locationPrecision: 3, // ~156 km cells\n * resolveClientLocation: (client) => client.attachments?.geo as { latitude: number, longitude: number },\n * });\n * ```\n *\n * Coordinates are not in `ClientSample`, so `resolveClientLocation` is required — see\n * {@link ClientLocationResolver}. Only the cell key reaches the issue payload, never the\n * coordinates, which matters because these payloads are archived into call summaries.\n *\n * **The limitation to state plainly: geography is confounded with your topology.** The control group\n * is \"everyone outside this cell\", which cannot separate *\"the path into this region degraded\"* from\n * *\"the SFU that happens to serve this region degraded\"*. If a region maps largely onto one\n * deployment, both hypotheses fit the same evidence. The discriminator is whether clients elsewhere\n * on the same server also degraded, which is what `SfuCongestionDetector` and\n * `TurnServerHealthDetector` answer — so the conclusion here points at them rather than claiming an\n * attribution it cannot support.\n *\n * ### Clients that never reported their metadata\n *\n * `browser` / `engine` / `platform` / `operationSystem` arrive as client metadata and may be absent —\n * a client that closed before sending them, or an application that does not collect them. Those\n * clients are excluded from **both** the population and the control group rather than bucketed as\n * `'unknown'`. The same applies to a client whose location `resolveClientLocation` cannot supply. A synthetic `'unknown'` population would be a mixture of every real one, so any rate\n * computed for it means nothing, and leaving those clients in the control group would dilute the\n * comparison with clients whose kind we cannot verify.\n */\nexport class ClientPopulationIssueDetector implements Detector, ActiveIssueTracker {\n\tpublic static readonly NAME = 'client-population-issue-detector' as const;\n\n\tpublic readonly name = ClientPopulationIssueDetector.NAME;\n\n\tprivate readonly _config: ClientPopulationIssueDetectorConfig;\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\tprivate readonly _issues = new Set<ActiveClientIssue>();\n\n\t/** The populations that qualified on the most recent `update()`. Exposed for tests/dashboards. */\n\tpublic lastPopulations: ClientPopulation[] = [];\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tconfig: Partial<ClientPopulationIssueDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tissueTypes: [],\n\t\t\tgroupBy: 'browser',\n\t\t\tlocationPrecision: 3,\n\t\t\tincludeVersion: true,\n\t\t\tminPopulationSize: 20,\n\t\t\tminAffectedClients: 5,\n\t\t\taffectedRatioThreshold: 0.3,\n\t\t\tminRelativeRisk: 3,\n\t\t\tminControlSize: 20,\n\t\t\tcooldownMs: 300_000,\n\t\t\t...config,\n\t\t};\n\n\t\t// Misconfiguration must not look like \"nothing is wrong\". Without a resolver every client\n\t\t// returns `undefined`, the detector finds no populations, and it would sit there silently\n\t\t// reporting nothing for the life of the process.\n\t\tif (this._config.groupBy === 'location' && !this._config.resolveClientLocation) {\n\t\t\tlogger.warn(\n\t\t\t\t'%s is configured with groupBy: \\'location\\' but no resolveClientLocation, so no client can be placed and nothing will ever be detected',\n\t\t\t\tthis.name,\n\t\t\t);\n\t\t}\n\n\t\tfor (const type of this._config.issueTypes) {\n\t\t\tthis._observer.activeIssuesRegistry.addIssueTracker(type, this);\n\t\t}\n\t}\n\n\tpublic get size(): number {\n\t\treturn this._issues.size;\n\t}\n\n\tpublic has(issue: ActiveClientIssue): boolean {\n\t\treturn this._issues.has(issue);\n\t}\n\n\tpublic add(issue: ActiveClientIssue): void {\n\t\tthis._issues.add(issue);\n\t}\n\n\tpublic delete(issue: ActiveClientIssue): boolean {\n\t\treturn this._issues.delete(issue);\n\t}\n\n\tpublic clear(): void {\n\t\tthis._issues.clear();\n\t\tthis.lastPopulations = [];\n\t}\n\n\tpublic update(): void {\n\t\tthis.lastPopulations = [];\n\n\t\tif (this._issues.size === 0) return;\n\n\t\tconst now = Date.now();\n\t\t// The denominators: how many clients of each population exist at all. Built from the fleet\n\t\t// rather than from the affected set, because a rate needs everyone, not just the unhappy.\n\t\tconst populationSizes = new Map<string, number>();\n\n\t\tfor (const call of this._observer.observedCalls.values()) {\n\t\t\tfor (const client of call.observedClients.values()) {\n\t\t\t\tconst population = this._populationOf(client);\n\n\t\t\t\tif (population === undefined) continue;\n\n\t\t\t\tpopulationSizes.set(population, (populationSizes.get(population) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\n\t\tif (populationSizes.size === 0) return;\n\n\t\t// (issueType, population) -> the affected client ids. Built from the issues we hold, so the\n\t\t// cost is the number of open issues rather than the size of the fleet.\n\t\tconst affected = new Map<string, Map<string, Set<string>>>();\n\n\t\tfor (const issue of this._issues) {\n\t\t\tconst client = this._observer.observedCalls.get(issue.callId)?.observedClients.get(issue.clientId);\n\n\t\t\tif (!client) continue;\n\n\t\t\tconst population = this._populationOf(client);\n\n\t\t\tif (population === undefined) continue;\n\n\t\t\tlet byPopulation = affected.get(issue.type);\n\n\t\t\tif (!byPopulation) {\n\t\t\t\tbyPopulation = new Map();\n\t\t\t\taffected.set(issue.type, byPopulation);\n\t\t\t}\n\n\t\t\tconst clientIds = byPopulation.get(population) ?? new Set<string>();\n\n\t\t\tclientIds.add(issue.clientId);\n\t\t\tbyPopulation.set(population, clientIds);\n\t\t}\n\n\t\tfor (const [ issueType, byPopulation ] of affected) {\n\t\t\t// The fleet total for this issue type, so each population can be compared against everyone\n\t\t\t// else rather than against an absolute threshold.\n\t\t\tlet totalAffected = 0;\n\n\t\t\tfor (const clientIds of byPopulation.values()) totalAffected += clientIds.size;\n\n\t\t\tlet totalClients = 0;\n\n\t\t\tfor (const size of populationSizes.values()) totalClients += size;\n\n\t\t\tfor (const [ population, clientIds ] of byPopulation) {\n\t\t\t\tconst clients = populationSizes.get(population) ?? clientIds.size;\n\t\t\t\tconst rollup = this._rollupOf(\n\t\t\t\t\tpopulation,\n\t\t\t\t\tissueType,\n\t\t\t\t\tclientIds,\n\t\t\t\t\tclients,\n\t\t\t\t\ttotalClients - clients,\n\t\t\t\t\ttotalAffected - clientIds.size,\n\t\t\t\t);\n\n\t\t\t\tif (rollup.clients < this._config.minPopulationSize) continue;\n\t\t\t\tif (rollup.affectedClients < this._config.minAffectedClients) continue;\n\t\t\t\tif (rollup.affectedRatio < this._config.affectedRatioThreshold) continue;\n\t\t\t\t// Without a control group there is nothing to be \"worse than\", and a bare share would\n\t\t\t\t// simply indict the most popular browser.\n\t\t\t\tif (rollup.controlClients < this._config.minControlSize) continue;\n\t\t\t\tif (rollup.relativeRisk < this._config.minRelativeRisk) continue;\n\n\t\t\t\tthis.lastPopulations.push(rollup);\n\n\t\t\t\tconst key = `${population}:${issueType}`;\n\n\t\t\t\tif (now - (this._lastRaisedAt.get(key) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\t\tthis._lastRaisedAt.set(key, now);\n\n\t\t\t\tconst isLocation = this._config.groupBy === 'location';\n\n\t\t\t\tthis._observer.addIssue({\n\t\t\t\t\ttype: ClientPopulationIssueTypes.clientPopulationIssue,\n\t\t\t\t\ttimestamp: now,\n\t\t\t\t\tconclusion: {\n\t\t\t\t\t\t// Geography is not something endpoints own. A cluster of network symptoms in one\n\t\t\t\t\t\t// place is a path/transit story, so it is attributed to the infrastructure rather\n\t\t\t\t\t\t// than to a client build.\n\t\t\t\t\t\tfaultDomain: isLocation ? 'infrastructure' : 'client-population',\n\t\t\t\t\t\tsummary: `'${issueType}' is ${this._riskText(rollup.relativeRisk)} more likely ${isLocation ? `in cell ${population}` : `on ${population}`} than across the rest of the fleet (${rollup.affectedClients}/${rollup.clients} vs ${rollup.controlAffectedClients}/${rollup.controlClients})`,\n\t\t\t\t\t\trecommendation: isLocation\n\t\t\t\t\t\t\t// The confounder is real and not separable here: see the class description.\n\t\t\t\t\t\t\t? 'look at the path into that region — ISP, transit or peering — but first rule out the servers those clients share, since a region often maps onto one SFU or TURN deployment'\n\t\t\t\t\t\t\t: 'this is not an SFU symptom — look at what those clients share: a recent release, a browser version, or shared/virtualised hardware',\n\t\t\t\t\t\tconfidence: this._confidenceOf(rollup),\n\t\t\t\t\t},\n\t\t\t\t\tpayload: { ...rollup },\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._observer.activeIssuesRegistry.removeIssueTracker(this);\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.clear();\n\t}\n\n\t/** `undefined` when the client never reported this attribute — see the class description. */\n\tprivate _populationOf(client: ObservedClient): string | undefined {\n\t\tif (this._config.groupBy === 'location') {\n\t\t\tconst location = this._config.resolveClientLocation?.(client);\n\n\t\t\treturn location ? geohash(location, this._config.locationPrecision) : undefined;\n\t\t}\n\n\t\tconst attribute = client[this._config.groupBy];\n\n\t\tif (!attribute?.name) return undefined;\n\n\t\treturn this._config.includeVersion && attribute.version\n\t\t\t? `${attribute.name} ${attribute.version}`\n\t\t\t: attribute.name;\n\t}\n\n\tprivate _rollupOf(\n\t\tpopulation: string,\n\t\tissueType: string,\n\t\tclientIds: Set<string>,\n\t\tclients: number,\n\t\tcontrolClients: number,\n\t\tcontrolAffectedClients: number,\n\t): ClientPopulation {\n\t\tconst affectedRatio = 0 < clients ? clientIds.size / clients : 0;\n\t\tconst controlAffectedRatio = 0 < controlClients ? controlAffectedClients / controlClients : 0;\n\n\t\treturn {\n\t\t\tpopulation,\n\t\t\taxis: this._config.groupBy,\n\t\t\tissueType,\n\t\t\tclients,\n\t\t\taffectedClients: clientIds.size,\n\t\t\taffectedRatio,\n\t\t\taffectedClientIds: [ ...clientIds ],\n\t\t\tcontrolClients,\n\t\t\tcontrolAffectedClients,\n\t\t\tcontrolAffectedRatio,\n\t\t\t// A spotless control group means nobody outside this population has the problem at all.\n\t\t\t// `Infinity` says that plainly rather than dividing by zero or inventing a ceiling.\n\t\t\trelativeRisk: 0 < controlAffectedRatio\n\t\t\t\t? affectedRatio / controlAffectedRatio\n\t\t\t\t: (0 < affectedRatio ? Infinity : 0),\n\t\t};\n\t}\n\n\tprivate _riskText(relativeRisk: number): string {\n\t\treturn Number.isFinite(relativeRisk) ? `${relativeRisk.toFixed(1)}x` : 'exclusively';\n\t}\n\n\t/** Bigger populations and starker contrasts are harder to produce by chance. */\n\tprivate _confidenceOf(rollup: ClientPopulation): number {\n\t\tlet confidence = 0.4;\n\n\t\tif (100 <= rollup.clients) confidence += 0.2;\n\t\telse if (50 <= rollup.clients) confidence += 0.1;\n\n\t\tif (10 <= rollup.relativeRisk) confidence += 0.3;\n\t\telse if (5 <= rollup.relativeRisk) confidence += 0.2;\n\t\telse confidence += 0.1;\n\n\t\treturn Math.min(1, Math.round(confidence * 100) / 100);\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { Observer } from '../Observer';\nimport type { ObservedPeerConnection } from '../ObservedPeerConnection';\n\nexport const TurnServerHealthTypes = {\n\t/** One TURN server's clients are in trouble while other servers' clients are fine. */\n\tturnServerDegraded: 'TURN_SERVER_DEGRADED',\n} as const;\n\nexport type TurnServerHealthDetectorConfig = {\n\n\t/**\n\t * Clients a server must be carrying before its ratio means anything. Default `5`.\n\t *\n\t * With two relayed clients, \"half are degraded\" is one person having a bad time. Sensible range\n\t * `5`–`20`; raise it if you run many small TURN deployments, since each needs enough traffic to be\n\t * measurable on its own.\n\t */\n\tminClientsPerServer: number;\n\n\t/**\n\t * Fraction of a server's clients that must have an open issue, `0`–`1`. Default `0.5`.\n\t *\n\t * Typical `0.4`–`0.7`. Remember each finding also carries the *other* servers' ratios, so the\n\t * threshold is not doing the comparison on its own — a server at 50% next to peers at 45% reads very\n\t * differently from one next to peers at 3%. Below `0.3` you will report servers that are merely\n\t * carrying unlucky clients.\n\t */\n\tdegradedRatioThreshold: number;\n\n\t/**\n\t * Which client issue types count as \"in trouble\". Empty (the default) means **any** open issue.\n\t *\n\t * The permissive default is deliberate and unusual for this library: the question is not *what* is\n\t * wrong with each client but whether trouble clusters on one relay, and a relay problem shows up as\n\t * whatever symptom each client happens to notice first. Narrow it to network types\n\t * (`congestion`, `ice-disconnected`) if endpoint issues like `cpulimitation` are common enough in\n\t * your fleet to blur the comparison between servers.\n\t */\n\tissueTypes: string[];\n\n\t/**\n\t * Consecutive `observer.update()` ticks the condition must hold before raising. Default `2`.\n\t *\n\t * The de-bounce. `1` reacts immediately and will fire on a single tick where several clients\n\t * happened to be mid-reconnect; `2`–`3` costs a tick or two of delay and removes most of that.\n\t * Note this counts *ticks*, not time, so how long it actually waits depends on your sample rate.\n\t */\n\tconsecutiveTicks: number;\n\n\t/**\n\t * Re-arm time per server (ms). Default `60_000`.\n\t *\n\t * Shorter than the outage detector's, because degradation is a condition you may want re-reported as\n\t * it persists or worsens, not a single event. Typical `60_000`–`300_000`.\n\t */\n\tcooldownMs: number;\n};\n\n/** The per-server view this detector builds. */\nexport type TurnServerHealth = {\n\tserverUrl: string;\n\n\t/** Distinct clients whose media is relayed through this server. */\n\tclients: number;\n\n\t/** Of those, how many currently have at least one open issue. */\n\tdegradedClients: number;\n\tdegradedRatio: number;\n\taffectedClientIds: string[];\n\n\t/** The open issue types seen on this server's clients, most common first. */\n\tissueTypes: string[];\n};\n\n/**\n * An **observer-level** detector that groups relayed clients by the TURN server carrying them and\n * compares the servers against each other.\n *\n * Counting TURN usage is not useful on its own; knowing that `turn-eu-1` has 22 of 30 clients in\n * trouble while `turn-eu-2` has 1 of 34 is. Because the comparison spans calls it lives on\n * `observer.detectors` and raises `observer-issue` — one actionable alert instead of fifty\n * per-client ones. Each finding carries the other servers' ratios as context, since \"half the\n * clients here are unhappy\" only means something relative to the rest of the fleet.\n *\n * Whether a client is in trouble comes from **its own reported issues**, not from thresholds applied\n * here. The client already decides that far better than a server-side rule could; the value this\n * adds is the grouping — the dimension no endpoint can see.\n *\n * For a relay that has stopped serving entirely, see `TurnServerOutageDetector`: this detector needs\n * clients *on* the server to ask how many are unhappy, and an outage takes them away.\n */\nexport class TurnServerHealthDetector implements Detector {\n\tpublic static readonly NAME = 'turn-server-health-detector';\n\n\tpublic readonly name = TurnServerHealthDetector.NAME;\n\n\tprivate readonly _config: TurnServerHealthDetectorConfig;\n\tprivate readonly _streaks = new Map<string, number>();\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\t/** The per-server rollup computed on the most recent `update()`. */\n\tpublic lastServers: TurnServerHealth[] = [];\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tconfig: Partial<TurnServerHealthDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tminClientsPerServer: 5,\n\t\t\tdegradedRatioThreshold: 0.5,\n\t\t\t// any open issue: the question is where trouble clusters, not what it is\n\t\t\tissueTypes: [],\n\t\t\tconsecutiveTicks: 2,\n\t\t\tcooldownMs: 60_000,\n\t\t\t...config,\n\t\t};\n\t}\n\n\tpublic update(): void {\n\t\tconst now = Date.now();\n\t\tconst wanted = new Set(this._config.issueTypes);\n\t\tconst issuesByClientId = new Map<string, string[]>();\n\n\t\t// Read straight off the registry rather than subscribing as a tracker: this detector asks\n\t\t// \"any open issue\", so a subscription would just re-derive the registry's own set.\n\t\tfor (const issue of this._observer.activeIssuesRegistry.values()) {\n\t\t\tif (0 < wanted.size && !wanted.has(issue.type)) continue;\n\n\t\t\tconst types = issuesByClientId.get(issue.clientId);\n\n\t\t\tif (types) types.push(issue.type);\n\t\t\telse issuesByClientId.set(issue.clientId, [ issue.type ]);\n\t\t}\n\n\t\t// Compute every server first, so each finding can carry the full cross-server comparison.\n\t\tthis.lastServers = [ ...this._observer.observedTURN.servers ]\n\t\t\t.map(([ serverUrl, server ]) => this._serverHealth(serverUrl, server.observedPeerConnections, issuesByClientId));\n\n\t\tfor (const health of this.lastServers) {\n\t\t\tconst serverUrl = health.serverUrl;\n\n\t\t\tif (health.clients < this._config.minClientsPerServer || health.degradedRatio < this._config.degradedRatioThreshold) {\n\t\t\t\tthis._streaks.delete(serverUrl);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst ticks = (this._streaks.get(serverUrl) ?? 0) + 1;\n\n\t\t\tthis._streaks.set(serverUrl, ticks);\n\n\t\t\tif (ticks < this._config.consecutiveTicks) continue;\n\t\t\tif (now - (this._lastRaisedAt.get(serverUrl) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(serverUrl, now);\n\n\t\t\tconst otherServers = this.lastServers\n\t\t\t\t.filter((server) => server.serverUrl !== serverUrl)\n\t\t\t\t.map((server) => ({ serverUrl: server.serverUrl, clients: server.clients, degradedRatio: server.degradedRatio }));\n\n\t\t\tthis._observer.addIssue({\n\t\t\t\ttype: TurnServerHealthTypes.turnServerDegraded,\n\t\t\t\ttimestamp: now,\n\t\t\t\tpayload: { ...health, otherServers },\n\t\t\t});\n\t\t}\n\n\t\tfor (const serverUrl of [ ...this._streaks.keys() ]) {\n\t\t\tif (!this.lastServers.some((server) => server.serverUrl === serverUrl)) this._streaks.delete(serverUrl);\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._streaks.clear();\n\t\tthis._lastRaisedAt.clear();\n\t\tthis.lastServers = [];\n\t}\n\n\tprivate _serverHealth(\n\t\tserverUrl: string,\n\t\tpeerConnections: Map<string, ObservedPeerConnection>,\n\t\tissuesByClientId: Map<string, string[]>,\n\t): TurnServerHealth {\n\t\t// A client can hold several relayed peer connections; the unit of comparison is the client.\n\t\tconst clientIds = new Set<string>();\n\t\tconst affectedClientIds = new Set<string>();\n\t\tconst typeCounts = new Map<string, number>();\n\n\t\tfor (const peerConnection of peerConnections.values()) {\n\t\t\tconst clientId = peerConnection.client.clientId;\n\n\t\t\tclientIds.add(clientId);\n\n\t\t\tconst types = issuesByClientId.get(clientId);\n\n\t\t\tif (!types) continue;\n\n\t\t\taffectedClientIds.add(clientId);\n\t\t\tfor (const type of types) typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1);\n\t\t}\n\n\t\treturn {\n\t\t\tserverUrl,\n\t\t\tclients: clientIds.size,\n\t\t\tdegradedClients: affectedClientIds.size,\n\t\t\tdegradedRatio: 0 < clientIds.size ? affectedClientIds.size / clientIds.size : 0,\n\t\t\taffectedClientIds: [ ...affectedClientIds ],\n\t\t\tissueTypes: [ ...typeCounts.entries() ].sort((a, b) => b[1] - a[1]).map(([ type ]) => type),\n\t\t};\n\t}\n}\n","import type { Detector } from './Detector';\nimport type { Observer } from '../Observer';\nimport type { ObservedPeerConnection } from '../ObservedPeerConnection';\n\nexport const TurnServerOutageTypes = {\n\t/** One TURN server's relayed population collapsed while the rest of the fleet is fine. */\n\tturnServerOutage: 'TURN_SERVER_OUTAGE',\n} as const;\n\nexport type TurnServerOutageDetectorConfig = {\n\n\t/**\n\t * Clients a server must have been carrying at its peak before its collapse means anything. Default\n\t * `5`.\n\t *\n\t * Below this, one or two people leaving looks like an outage. Sensible range `5`–`50`; the higher it\n\t * is the more confident the finding, and the more small deployments go unwatched.\n\t */\n\tminClientsAtPeak: number;\n\n\t/**\n\t * Fraction of the peak population that must be gone or disrupted. Default `0.8` — an outage is\n\t * near-total by definition; partial degradation is `TurnServerHealthDetector`'s question.\n\t */\n\tlossRatioThreshold: number;\n\n\t/**\n\t * Window the peak population is measured over (ms). Default `120_000`.\n\t *\n\t * Long enough to span a real outage's onset, short enough that yesterday's peak is not held against\n\t * today. Typical `60_000`–`600_000`. Too long and the natural end of a busy period reads as a\n\t * collapse; too short and a gradual failure never shows a peak to fall from.\n\t */\n\tpeakWindowMs: number;\n\n\t/**\n\t * Require a healthy **control group** — clients not relayed through this server that are still\n\t * connected — before blaming the server. Without this, a call ending, a fleet-wide network\n\t * event, or the observer shutting down all look exactly like a TURN outage. Default `true`.\n\t */\n\trequireControlGroup: boolean;\n\n\t/**\n\t * Clients elsewhere before the control group is worth anything. Default `5`.\n\t *\n\t * If you run a single TURN server there is never a control group, so with `requireControlGroup: true`\n\t * this detector can never fire — which is correct rather than unfortunate: with one relay you cannot\n\t * distinguish \"the relay died\" from \"everyone went home\". Sensible range `5`–`20`.\n\t */\n\tminControlGroupClients: number;\n\n\t/**\n\t * Fraction of the control group that must still be healthy, `0`–`1`. Default `0.7`.\n\t *\n\t * The evidence that the rest of the world is fine. Typical `0.6`–`0.9`. Set it too high and a\n\t * concurrent unrelated problem elsewhere masks a real outage; too low and a fleet-wide network event\n\t * gets blamed on whichever server lost clients first.\n\t */\n\tcontrolGroupHealthyRatio: number;\n\n\t/**\n\t * Consecutive `observer.update()` ticks the condition must hold before raising. Default `2`.\n\t *\n\t * Counts ticks, not time. `1` will fire on a single tick where a batch of clients happened to be\n\t * between samples; `2`–`4` is the useful range for something this consequential to declare.\n\t */\n\tconsecutiveTicks: number;\n\n\t/**\n\t * Re-arm time (ms) per server. Long by default (`300_000`) — an outage is one event, not one\n\t * per tick, and a server that stays down would otherwise alert forever.\n\t */\n\tcooldownMs: number;\n};\n\n/** ICE / connection states treated as \"this client is not currently relaying\". */\nconst brokenStates = new Set([ 'disconnected', 'failed', 'closed' ]);\n\ntype PeakEntry = { clients: number, at: number };\n\n/**\n * Detects a **TURN server outage** — a relay that has stopped serving — by watching its client\n * population collapse while the rest of the fleet carries on.\n *\n * This is the case its sibling `TurnServerHealthDetector` structurally *cannot* see, and the\n * distinction is worth being precise about. That detector groups clients by the server relaying them\n * and asks how many are reporting issues. It needs clients on the server to ask the question. When a\n * TURN server goes down completely, allocation fails: existing sessions drop, and new clients never\n * obtain a relay candidate through it at all, so they are never attributed to it. The server's\n * population goes to zero and the health detector falls silent for the worst possible reason — it\n * has nobody left to ask. Degradation makes clients unhappy; an outage makes them *disappear*.\n *\n * So the signal here is absence, measured against the server's own recent peak:\n *\n * - clients gone entirely (their relayed peer connections closed, or they re-negotiated onto a\n * different path), plus\n * - clients still attributed to the server whose ICE or connection state is `disconnected` /\n * `failed` / `closed` — the ones mid-collapse, which is what you catch if you look during the\n * outage rather than after it.\n *\n * ### The control group is the whole design\n *\n * Absence is a dangerous signal: a call ending, everyone going home at 6pm, a fleet-wide network\n * event, and the observer itself shutting down all produce exactly the same collapse. The detector\n * therefore refuses to blame a server unless clients **not** relayed through it are demonstrably\n * still connected — `requireControlGroup`, on by default. \"Everyone on `turn-eu-1` vanished\" is\n * ambiguous; \"everyone on `turn-eu-1` vanished while 200 clients elsewhere are fine\" is an outage.\n *\n * That comparison is only available to something watching every call at once, which is why this is\n * an observer-level detector raising `observer-issue` — one alert for the fleet, not one per\n * abandoned call.\n *\n * ### Caveats worth knowing before you tune it\n *\n * Clients that fail over cleanly to a second TURN server still count as lost here, which is\n * correct — the server did stop serving them — but it means a well-configured fleet with automatic\n * failover reports outages that users never felt. That is the intended behaviour: the failover\n * worked *and* the server is down are both true, and you want to know the second one.\n *\n * A genuinely quiet server (last call of the day ends) is suppressed by the control group, not by\n * the collapse test. If you run a small deployment where the control group is routinely below\n * `minControlGroupClients`, this detector will stay quiet — prefer alerting on your TURN server's\n * own health checks there, since a handful of clients cannot distinguish these cases.\n */\nexport class TurnServerOutageDetector implements Detector {\n\tpublic static readonly NAME = 'turn-server-outage-detector';\n\n\tpublic readonly name = TurnServerOutageDetector.NAME;\n\n\tprivate readonly _config: TurnServerOutageDetectorConfig;\n\n\t/** serverUrl -> recent population observations, used to derive the windowed peak. */\n\tprivate readonly _peaks = new Map<string, PeakEntry[]>();\n\tprivate readonly _streaks = new Map<string, number>();\n\tprivate readonly _lastRaisedAt = new Map<string, number>();\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tconfig: Partial<TurnServerOutageDetectorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tminClientsAtPeak: 5,\n\t\t\t// an outage is near-total by definition; partial degradation is the health detector's question\n\t\t\tlossRatioThreshold: 0.8,\n\t\t\tpeakWindowMs: 120_000,\n\t\t\t// absence without a control group is just quiet\n\t\t\trequireControlGroup: true,\n\t\t\tminControlGroupClients: 5,\n\t\t\tcontrolGroupHealthyRatio: 0.7,\n\t\t\tconsecutiveTicks: 2,\n\t\t\t// one event, not one per tick\n\t\t\tcooldownMs: 300_000,\n\t\t\t...config,\n\t\t};\n\t}\n\n\tpublic update(): void {\n\t\tconst now = Date.now();\n\t\tconst servers = [ ...this._observer.observedTURN.servers ];\n\n\t\t// Per-server populations first: the control group for one server is every *other* server's\n\t\t// clients plus the non-relayed ones, so all of them have to be known up front.\n\t\tconst populations = new Map<string, { healthy: Set<string>, broken: Set<string> }>();\n\n\t\tfor (const [ serverUrl, server ] of servers) {\n\t\t\tpopulations.set(serverUrl, this._populationOf(server.observedPeerConnections));\n\t\t}\n\n\t\tfor (const [ serverUrl, population ] of populations) {\n\t\t\tconst live = population.healthy.size;\n\t\t\tconst broken = population.broken.size;\n\t\t\tconst peak = this._recordAndPeak(serverUrl, live + broken, now);\n\n\t\t\tif (peak < this._config.minClientsAtPeak) {\n\t\t\t\tthis._streaks.delete(serverUrl);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Everything the server has lost: gone entirely, or still attributed but not connected.\n\t\t\tconst lost = Math.max(0, peak - live);\n\t\t\tconst lossRatio = lost / peak;\n\n\t\t\tif (lossRatio < this._config.lossRatioThreshold) {\n\t\t\t\tthis._streaks.delete(serverUrl);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst control = this._controlGroup(serverUrl, populations);\n\n\t\t\tif (this._config.requireControlGroup) {\n\t\t\t\tif (control.total < this._config.minControlGroupClients) {\n\t\t\t\t\tthis._streaks.delete(serverUrl);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (control.healthyRatio < this._config.controlGroupHealthyRatio) {\n\t\t\t\t\t// Everyone is having a bad time; this is not one server's fault.\n\t\t\t\t\tthis._streaks.delete(serverUrl);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst ticks = (this._streaks.get(serverUrl) ?? 0) + 1;\n\n\t\t\tthis._streaks.set(serverUrl, ticks);\n\n\t\t\tif (ticks < this._config.consecutiveTicks) continue;\n\t\t\tif (now - (this._lastRaisedAt.get(serverUrl) ?? 0) < this._config.cooldownMs) continue;\n\n\t\t\tthis._lastRaisedAt.set(serverUrl, now);\n\n\t\t\tthis._observer.addIssue({\n\t\t\t\ttype: TurnServerOutageTypes.turnServerOutage,\n\t\t\t\ttimestamp: now,\n\t\t\t\tpayload: {\n\t\t\t\t\tserverUrl,\n\t\t\t\t\tpeakClients: peak,\n\t\t\t\t\tcurrentClients: live,\n\t\t\t\t\tdisruptedClients: broken,\n\t\t\t\t\tlostClients: lost,\n\t\t\t\t\tlossRatio,\n\t\t\t\t\tdisruptedClientIds: [ ...population.broken ],\n\t\t\t\t\tcontrolGroupClients: control.total,\n\t\t\t\t\tcontrolGroupHealthyClients: control.healthy,\n\t\t\t\t\tcontrolGroupHealthyRatio: control.healthyRatio,\n\t\t\t\t\tpeakWindowMs: this._config.peakWindowMs,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Forget servers that are no longer known at all.\n\t\tfor (const key of [ ...this._peaks.keys() ]) {\n\t\t\tif (populations.has(key)) continue;\n\t\t\tthis._peaks.delete(key);\n\t\t\tthis._streaks.delete(key);\n\t\t}\n\t}\n\n\tpublic close(): void {\n\t\tthis._peaks.clear();\n\t\tthis._streaks.clear();\n\t\tthis._lastRaisedAt.clear();\n\t}\n\n\t/** Distinct clients on a server, split by whether their relayed transport is actually up. */\n\tprivate _populationOf(peerConnections: Map<string, ObservedPeerConnection>) {\n\t\tconst healthy = new Set<string>();\n\t\tconst broken = new Set<string>();\n\n\t\tfor (const peerConnection of peerConnections.values()) {\n\t\t\tconst clientId = peerConnection.client.clientId;\n\t\t\tconst isBroken = peerConnection.closed\n\t\t\t\t|| brokenStates.has(peerConnection.iceConnectionState ?? '')\n\t\t\t\t|| brokenStates.has(peerConnection.connectionState ?? '');\n\n\t\t\tif (isBroken) broken.add(clientId);\n\t\t\telse healthy.add(clientId);\n\t\t}\n\n\t\t// A client with one healthy relayed transport is not a casualty, even if another of its\n\t\t// peer connections is down.\n\t\tfor (const clientId of healthy) broken.delete(clientId);\n\n\t\treturn { healthy, broken };\n\t}\n\n\t/** Record this tick's population and return the peak across `peakWindowMs`. */\n\tprivate _recordAndPeak(serverUrl: string, clients: number, now: number): number {\n\t\tconst entries = this._peaks.get(serverUrl) ?? [];\n\t\tconst cutoff = now - this._config.peakWindowMs;\n\t\tconst kept = entries.filter((e) => cutoff <= e.at);\n\n\t\tkept.push({ clients, at: now });\n\t\tthis._peaks.set(serverUrl, kept);\n\n\t\treturn kept.reduce((max, e) => Math.max(max, e.clients), 0);\n\t}\n\n\t/**\n\t * Everyone *not* relayed through `serverUrl`: clients on other TURN servers plus every client\n\t * the observer knows about that isn't relayed at all. The healthy share of that group is what\n\t * separates \"this server broke\" from \"everything broke\".\n\t */\n\tprivate _controlGroup(serverUrl: string, populations: Map<string, { healthy: Set<string>, broken: Set<string> }>) {\n\t\tconst healthy = new Set<string>();\n\t\tconst broken = new Set<string>();\n\n\t\tfor (const [ otherUrl, population ] of populations) {\n\t\t\tif (otherUrl === serverUrl) continue;\n\t\t\tfor (const clientId of population.healthy) healthy.add(clientId);\n\t\t\tfor (const clientId of population.broken) broken.add(clientId);\n\t\t}\n\n\t\t// Non-relayed clients count as control too: they are the cleanest evidence that the\n\t\t// observer is still receiving samples and the world at large is fine.\n\t\tconst onThisServer = populations.get(serverUrl);\n\n\t\tfor (const call of this._observer.observedCalls.values()) {\n\t\t\tfor (const client of call.observedClients.values()) {\n\t\t\t\tif (onThisServer?.healthy.has(client.clientId) || onThisServer?.broken.has(client.clientId)) continue;\n\t\t\t\tif (broken.has(client.clientId)) continue;\n\t\t\t\thealthy.add(client.clientId);\n\t\t\t}\n\t\t}\n\n\t\tfor (const clientId of healthy) broken.delete(clientId);\n\n\t\tconst total = healthy.size + broken.size;\n\n\t\treturn {\n\t\t\ttotal,\n\t\t\thealthy: healthy.size,\n\t\t\thealthyRatio: 0 < total ? healthy.size / total : 0,\n\t\t};\n\t}\n}\n","/** An entry retained by {@link SlidingWindow}. */\nexport type SlidingWindowEntry<T> = {\n\ttimestamp: number;\n\tvalue: T;\n};\n\n/**\n * A time-bounded buffer used by detectors that reason over a window (\"N of M clients degraded within\n * 10 s\"). Entries older than `windowMs` are evicted on write and on read.\n *\n * ### Ordering is enforced, not assumed\n *\n * Eviction walks from the front and stops at the first entry still inside the window, which is only\n * correct if entries are ordered by timestamp. Callers mostly pass `Date.now()` and are ordered by\n * construction — but not always: a timestamp taken from a client sample, or two calls inside the\n * same millisecond, can arrive out of order, and one such entry would park itself at the head and\n * stop eviction *permanently*, so the window would grow without bound and keep reporting symptoms\n * from hours ago.\n *\n * Rather than trust the caller, {@link add} inserts in timestamp order. Appending (the overwhelmingly\n * common case) stays O(1); an out-of-order insert costs a short backward scan, because such entries\n * are near the tail in practice.\n *\n * ### The window advances on the newest observation\n *\n * Eviction is relative to the largest timestamp seen, not the one just passed. A caller that reads\n * with a `now` behind the newest entry (a replayed sample, a clock that stepped back) would\n * otherwise un-evict nothing and, worse, a caller passing an old `now` to {@link add} would evict\n * everything newer.\n */\nexport class SlidingWindow<T> {\n\tprivate _entries: SlidingWindowEntry<T>[] = [];\n\tprivate _latest = -Infinity;\n\n\tpublic constructor(\n\t\tpublic readonly windowMs: number,\n\n\t\t/** Optional hard cap on retained entries, to bound memory on very chatty inputs. */\n\t\tpublic readonly maxEntries = 1024,\n\t) {\n\t}\n\n\t/** Add an entry (defaults to `Date.now()`), then evict anything outside the window. */\n\tpublic add(value: T, timestamp = Date.now()): void {\n\t\t// A non-finite timestamp cannot be ordered or compared against the window edge; it would make\n\t\t// every subsequent eviction decision meaningless. Drop it rather than corrupt the buffer.\n\t\tif (!Number.isFinite(timestamp)) return;\n\n\t\tconst entry = { timestamp, value };\n\t\tconst last = this._entries[this._entries.length - 1];\n\n\t\tif (last === undefined || last.timestamp <= timestamp) {\n\t\t\tthis._entries.push(entry);\n\t\t} else {\n\t\t\tlet index = this._entries.length - 1;\n\n\t\t\twhile (0 < index && timestamp < this._entries[index - 1].timestamp) --index;\n\t\t\tthis._entries.splice(index, 0, entry);\n\t\t}\n\n\t\tif (this._latest < timestamp) this._latest = timestamp;\n\t\tthis._evict(timestamp);\n\t}\n\n\t/**\n\t * The entries still inside the window, oldest first.\n\t *\n\t * A **copy** — callers routinely map/sort what they get back, and handing out the live array let\n\t * them mutate the window from the outside.\n\t */\n\tpublic entries(now = Date.now()): SlidingWindowEntry<T>[] {\n\t\tthis._evict(now);\n\n\t\treturn [ ...this._entries ];\n\t}\n\n\t/** The values still inside the window, oldest first. */\n\tpublic values(now = Date.now()): T[] {\n\t\tthis._evict(now);\n\n\t\treturn this._entries.map((entry) => entry.value);\n\t}\n\n\t/**\n\t * How many entries are inside the window as of `now`.\n\t *\n\t * Prefer this to `values(now).length`: counting through {@link values} allocates an array of every\n\t * entry only to read its length, which on a hot path is the whole cost of the call.\n\t */\n\tpublic count(now = Date.now()): number {\n\t\tthis._evict(now);\n\n\t\treturn this._entries.length;\n\t}\n\n\t/** Retained entries, without evicting first. See {@link count} for the windowed answer. */\n\tpublic get size(): number {\n\t\treturn this._entries.length;\n\t}\n\n\tpublic clear(): void {\n\t\tthis._entries = [];\n\t\tthis._latest = -Infinity;\n\t}\n\n\tprivate _evict(now: number) {\n\t\tif (Number.isFinite(now) && this._latest < now) this._latest = now;\n\n\t\tconst oldest = this._latest - this.windowMs;\n\t\tlet expired = 0;\n\n\t\twhile (expired < this._entries.length && this._entries[expired].timestamp < oldest) ++expired;\n\n\t\t// One splice instead of N shifts: `shift()` re-indexes the whole array each time, so evicting a\n\t\t// burst of k entries from a window of n cost O(k*n).\n\t\tif (0 < expired) this._entries.splice(0, expired);\n\n\t\tconst excess = this._entries.length - this.maxEntries;\n\n\t\tif (0 < excess) this._entries.splice(0, excess);\n\t}\n}\n","import type { Observer } from '../Observer';\nimport type { ObservedOutboundTrack } from '../ObservedOutboundTrack';\nimport type { Validator, ValidationReport } from './Validator';\nimport { SlidingWindow } from '../utils/SlidingWindow';\nimport { percentile, correlation } from '../utils/stats';\n\n/** Raised once if one receiver turns out to be dragging a publisher down for everyone. */\nexport const LOWEST_COMMON_DENOMINATOR_ISSUE = 'WORST_RECEIVER_CONTAGION';\n\n/** The measurements behind a decided verdict — everything needed to check the call yourself. */\nexport type SimulcastReceiverEvidence = {\n\tcallId: string;\n\ttrackId: string;\n\tpublisherClientId: string;\n\tworstReceiverClientId: string;\n\tpublisherBitrate: number;\n\tworstReceiverBitrate: number;\n\tmedianReceiverBitrate: number;\n\n\t/** How closely the publisher's bitrate followed the **worst** receiver's, `0..1`. */\n\ttrackingWithWorst: number;\n\n\t/** The same against the **median** receiver — the control. */\n\ttrackingWithMedian: number;\n};\n\nexport type SimulcastReceiverReportPayload = ({\n\n\t/** The publisher held up while one receiver lagged: layers are being chosen per consumer. */\n\tverdict: 'layer-decided-per-receiver';\n\tevidence: SimulcastReceiverEvidence;\n} | {\n\n\t/** The publisher tracked its worst receiver: everyone is getting the lowest common denominator. */\n\tverdict: 'layer-decided-lowest-common-denominator';\n\tevidence: SimulcastReceiverEvidence;\n} | {\n\n\t/** Gave up without the conditions needed to judge. **Not a pass.** */\n\tverdict: 'inconclusive';\n\treason: string;\n}) & {\n\tstartedAt: number;\n\n\t/** How many times the check actually ran — i.e. how much the verdict is worth. */\n\tchecks: number;\n};\n\nexport type SimulcastReceiverValidatorConfig = {\n\n\t/**\n\t * Receivers a published track needs before the comparison means anything. Default `3`.\n\t *\n\t * The question is whether *one* receiver drags *the others* down, which needs at least one other to\n\t * be dragged — so `3` gives a worst receiver plus two to compare against. `2` is the technical\n\t * minimum but makes \"median of the others\" a single number. Sensible range `3`–`5`.\n\t */\n\tminReceivers: number;\n\n\t/**\n\t * How long bitrates are correlated over (ms). Default `10_000`.\n\t *\n\t * Long enough to contain a real adaptation response — the publisher's encoder reacting to a\n\t * bandwidth estimate takes seconds, not milliseconds. Typical `10_000`–`30_000`. Too short and you\n\t * catch transient jitter rather than a sustained relationship; too long and a genuine change is\n\t * averaged out by the healthy period around it.\n\t */\n\twindowMs: number;\n\n\t/**\n\t * Samples needed inside the window before it can be judged. Default `5`.\n\t *\n\t * A correlation over two or three points is meaningless. Combined with `windowMs` this implies a\n\t * sampling period: 5 samples in 10 s needs clients reporting at least every ~2 s. If your collector\n\t * is slower, widen `windowMs` rather than lowering this.\n\t */\n\tminSamples: number;\n\n\t/**\n\t * The worst receiver must be at most this share of the median receiver, `0`–`1`. Default `0.5`.\n\t *\n\t * The precondition, not the finding: unless somebody is genuinely doing much worse than the rest,\n\t * there is nothing for the publisher to be dragged *by* and the check has nothing to look at.\n\t * Typical `0.4`–`0.7`. Higher makes the check run more often on weaker evidence; lower means it\n\t * rarely finds a qualifying situation at all.\n\t */\n\toutlierRatioThreshold: number;\n\n\t/**\n\t * How closely the publisher must track the worst receiver to count as dragged, `0`–`1`. Default\n\t * `0.8`.\n\t *\n\t * This is the finding: the publisher sending at ≥80% of the *worst* receiver's rate means it has\n\t * collapsed to the lowest common denominator instead of serving everyone else properly. Typical\n\t * `0.7`–`0.9`. Toward `1` you only catch total collapse; below ~`0.6` normal encoder behaviour can\n\t * look like dragging.\n\t */\n\ttrackingRatioThreshold: number;\n\n\t/**\n\t * Clean checks required before concluding per-receiver adaptation works. Default `3`.\n\t *\n\t * One clean check could be luck — the qualifying moment might simply not have been bad enough. This\n\t * is what stops a lucky sample from being reported as a pass, so raising it strengthens the verdict\n\t * at the cost of taking longer to reach one. Typical `3`–`10`.\n\t */\n\tminChecks: number;\n};\n\ntype Bitrates = { publisher: number, worst: number, median: number };\n\n/**\n * Answers one question: **does this SFU adapt each receiver on its own, or does one bad receiver\n * drag the publisher down for everyone?**\n *\n * That is what simulcast (or SVC) exists to prevent. With several encodings available the server can\n * hand the struggling participant a lower layer and leave everyone else alone. Without it — or with\n * a server that relays RTCP end to end instead of terminating it, so the publisher's bandwidth\n * estimate collapses to the minimum across all receivers — the only way to serve the slowest\n * participant is to make the source send less, and everybody gets the lowest common denominator.\n *\n * The two causes are worth naming because the *observation* cannot separate them: the publisher's\n * bitrate tracking its worst receiver looks identical either way. What the check establishes is\n * whether per-receiver adaptation is happening at all. If the verdict is\n * `layer-decided-lowest-common-denominator`, look at both — is simulcast/SVC actually enabled with\n * layers selected per consumer, and is the SFU terminating receiver reports rather than forwarding\n * them?\n *\n * ### The control matters more than the correlation\n *\n * \"Publisher follows worst receiver\" alone proves nothing: when the whole call degrades together,\n * the publisher follows *everyone*, and that is ordinary adaptation working correctly. The verdict\n * only goes against the deployment when the publisher tracks the worst receiver **more closely than\n * it tracks the median** — the worst receiver is leading, not merely coinciding.\n *\n * Likewise, a window with no outlier in it is not evidence of health, it is an untested SFU: if\n * nobody is struggling, there is nothing for per-receiver adaptation to do. Those windows are\n * skipped and never counted in `checks`.\n *\n * ### Why a validator, not a detector\n *\n * This is a property of the SFU build and configuration, not of this moment: a server doing\n * per-receiver layer selection at 09:00 still is at 17:00. Re-deriving it every tick cannot produce\n * new information — it would only keep a sliding window alive per published track for the life of\n * every call. So it decides once, reports, and releases that state.\n *\n * ```ts\n * observer.on('validation-ready', ({ validator, report }) => {\n * if (validator !== 'simulcast-receivers' || !report.ready) return;\n * console.log(report.verdict); // 'layer-decided-per-receiver' | ... | 'inconclusive'\n * });\n *\n * observer.addValidator('simulcast-receivers');\n * onDeploy(() => observer.addValidator('simulcast-receivers')); // check again\n * ```\n *\n * ### `inconclusive` is not a pass\n *\n * The check only runs when a publisher has several receivers and one of them is far behind the\n * median; plenty of healthy deployments never present that. Concluding from the absence of a failure\n * would verify nothing, so `checks` counts the times the check genuinely ran, and a validator that\n * is cancelled (or whose observer closes) finishes `inconclusive` with the reason why.\n */\nexport class SimulcastReceiverValidator implements Validator<SimulcastReceiverReportPayload> {\n\tpublic static readonly NAME = 'simulcast-receivers' as const;\n\n\tpublic readonly name = SimulcastReceiverValidator.NAME;\n\n\tpublic readonly startedAt = Date.now();\n\n\tpublic report: ValidationReport<SimulcastReceiverReportPayload> = { ready: false };\n\n\tprivate readonly _config: SimulcastReceiverValidatorConfig;\n\tprivate readonly _windows = new Map<string, SlidingWindow<Bitrates>>();\n\tprivate _checks = 0;\n\tprivate _done = false;\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tpublic readonly onDone: (report: ValidationReport<SimulcastReceiverReportPayload>) => void,\n\t\tconfig: Partial<SimulcastReceiverValidatorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tminChecks: 3,\n\t\t\tminReceivers: 3,\n\t\t\twindowMs: 10_000,\n\t\t\tminSamples: 5,\n\t\t\toutlierRatioThreshold: 0.5,\n\t\t\ttrackingRatioThreshold: 0.8,\n\t\t\t...config,\n\t\t};\n\t}\n\n\t/** How many times the comparison actually ran. `0` means nothing was established. */\n\tpublic get checks(): number {\n\t\treturn this._checks;\n\t}\n\n\t/** Give up without a verdict, freeing anything waiting on this validator. */\n\tpublic cancel(reason = 'cancelled'): void {\n\t\tthis._finish({ verdict: 'inconclusive', reason });\n\t}\n\n\tpublic update(): void {\n\t\tif (this._done) return;\n\n\t\tconst now = Date.now();\n\t\tconst live = new Set<string>();\n\n\t\tfor (const call of this._observer.observedCalls.values()) {\n\t\t\t// Without publisher<->subscriber links there is no \"the receivers of this track\" to compare.\n\t\t\tif (!call.remoteTrackResolver) continue;\n\n\t\t\tfor (const client of call.observedClients.values()) {\n\t\t\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\t\t\tfor (const publisher of peerConnection.observedOutboundTracks.values()) {\n\t\t\t\t\t\t// Simulcast/SVC is a video mechanism; an audio track has no layers to select.\n\t\t\t\t\t\tif (publisher.kind !== 'video') continue;\n\n\t\t\t\t\t\tlive.add(publisher.id);\n\n\t\t\t\t\t\tif (this._inspect(call.callId, client.clientId, publisher, now)) return;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Forget windows for tracks that are gone, or a long-lived observer retains one per track\n\t\t// ever published.\n\t\tfor (const trackId of [ ...this._windows.keys() ]) {\n\t\t\tif (!live.has(trackId)) this._windows.delete(trackId);\n\t\t}\n\t}\n\n\t/** Returns `true` when a verdict was reached and the caller should stop iterating. */\n\tprivate _inspect(callId: string, publisherClientId: string, publisher: ObservedOutboundTrack, now: number): boolean {\n\t\tconst publisherBitrate = publisher.bitrate;\n\n\t\tif (publisherBitrate === undefined || publisherBitrate <= 0) return false;\n\t\tif (publisher.remoteInboundTracks.size < this._config.minReceivers) return false;\n\n\t\tconst receivers: { clientId: string, bitrate: number }[] = [];\n\n\t\tfor (const receiver of publisher.remoteInboundTracks) {\n\t\t\tconst bitrate = receiver.getInboundRtp()?.bitrate;\n\n\t\t\t// A receiver with no bitrate this tick is a missing measurement, not a bitrate of zero —\n\t\t\t// counting it as 0 would manufacture the very outlier this check looks for.\n\t\t\tif (bitrate === undefined || bitrate <= 0) continue;\n\n\t\t\treceivers.push({ clientId: receiver.getPeerConnection().client.clientId, bitrate });\n\t\t}\n\n\t\tif (receivers.length < this._config.minReceivers) return false;\n\n\t\tconst bitrates = receivers.map((receiver) => receiver.bitrate);\n\t\tconst worstReceiver = receivers.reduce((a, b) => (a.bitrate <= b.bitrate ? a : b));\n\t\tconst worst = worstReceiver.bitrate;\n\t\tconst median = percentile(bitrates, 0.5) ?? worst;\n\t\tconst window = this._windowOf(publisher.id);\n\n\t\twindow.add({ publisher: publisherBitrate, worst, median }, now);\n\n\t\tconst samples = window.values(now);\n\n\t\tif (samples.length < this._config.minSamples) return false;\n\n\t\tconst latest = samples[samples.length - 1];\n\t\tconst outlierRatio = 0 < latest.median ? latest.worst / latest.median : 1;\n\n\t\t// No outlier means the SFU is not being tested — this window teaches us nothing, and counting\n\t\t// it would let \"nothing ever went wrong\" masquerade as \"it handles things correctly\".\n\t\tif (this._config.outlierRatioThreshold < outlierRatio) return false;\n\n\t\t++this._checks;\n\t\twindow.clear();\n\n\t\tconst trackingWithWorst = correlation(samples.map((s) => s.publisher), samples.map((s) => s.worst));\n\t\tconst trackingWithMedian = correlation(samples.map((s) => s.publisher), samples.map((s) => s.median));\n\t\t// The publisher must follow the WORST receiver specifically. Following the median just as\n\t\t// closely means everyone moved together, which is ordinary adaptation.\n\t\tconst dragged = this._config.trackingRatioThreshold <= trackingWithWorst\n\t\t\t&& trackingWithMedian < trackingWithWorst;\n\n\t\tconst evidence: SimulcastReceiverEvidence = {\n\t\t\tcallId,\n\t\t\ttrackId: publisher.id,\n\t\t\tpublisherClientId,\n\t\t\tworstReceiverClientId: worstReceiver.clientId,\n\t\t\tpublisherBitrate,\n\t\t\tworstReceiverBitrate: latest.worst,\n\t\t\tmedianReceiverBitrate: latest.median,\n\t\t\ttrackingWithWorst,\n\t\t\ttrackingWithMedian,\n\t\t};\n\n\t\tif (dragged) {\n\t\t\tthis._finish({ verdict: 'layer-decided-lowest-common-denominator', evidence });\n\n\t\t\treturn true;\n\t\t}\n\n\t\tif (this._config.minChecks <= this._checks) {\n\t\t\tthis._finish({ verdict: 'layer-decided-per-receiver', evidence });\n\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Settle on a verdict, exactly once.\n\t *\n\t * The guard is not paranoia: `onDone` removes this validator from the observer, and a second call\n\t * would emit a second `validation-ready` for a validator that is no longer registered — e.g. when\n\t * `observer.close()` cancels a validator that decided earlier in the same tick.\n\t */\n\tprivate _finish(outcome:\n\t| { verdict: 'layer-decided-per-receiver', evidence: SimulcastReceiverEvidence }\n\t| { verdict: 'layer-decided-lowest-common-denominator', evidence: SimulcastReceiverEvidence }\n\t| { verdict: 'inconclusive', reason: string },\n\t): void {\n\t\tif (this._done) return;\n\t\tthis._done = true;\n\n\t\tconst decidedAt = Date.now();\n\n\t\tthis._windows.clear();\n\t\tthis.report = {\n\t\t\tready: true,\n\t\t\tdecidedAt,\n\t\t\tstartedAt: this.startedAt,\n\t\t\tchecks: this._checks,\n\t\t\t...outcome,\n\t\t};\n\n\t\tif (outcome.verdict === 'layer-decided-lowest-common-denominator') {\n\t\t\tthis._observer.addIssue({\n\t\t\t\ttype: LOWEST_COMMON_DENOMINATOR_ISSUE,\n\t\t\t\ttimestamp: decidedAt,\n\t\t\t\tconclusion: {\n\t\t\t\t\tfaultDomain: 'infrastructure',\n\t\t\t\t\tsummary: 'one bad receiver is dragging a publisher\\'s bitrate down for everyone — the SFU is not selecting layers per consumer',\n\t\t\t\t\trecommendation: 'check that simulcast/SVC is enabled and layers are chosen per consumer, and that the SFU terminates receiver reports instead of forwarding them; this is a build/config property, not a transient',\n\t\t\t\t\tconfidence: 0.8,\n\t\t\t\t},\n\t\t\t\tpayload: { checks: this._checks, ...outcome.evidence },\n\t\t\t});\n\t\t}\n\n\t\tthis.onDone(this.report);\n\t}\n\n\tprivate _windowOf(trackId: string): SlidingWindow<Bitrates> {\n\t\tlet window = this._windows.get(trackId);\n\n\t\tif (!window) {\n\t\t\twindow = new SlidingWindow<Bitrates>(this._config.windowMs);\n\t\t\tthis._windows.set(trackId, window);\n\t\t}\n\n\t\treturn window;\n\t}\n}\n","import type { Observer } from '../Observer';\nimport type { Validator, ValidationReport } from './Validator';\n\n/** Raised once if the resolver turns out never to link anything. */\nexport const UNRESOLVED_TRACK_LINKS_ISSUE = 'REMOTE_TRACK_LINKS_UNRESOLVED';\n\n/** What the check actually saw, whichever way it went. */\nexport type RemoteTrackLinkEvidence = {\n\n\t/** Calls that presented the conditions for linking: a resolver, ≥2 clients, and inbound tracks. */\n\teligibleCalls: number;\n\n\t/** Inbound tracks seen across those calls. */\n\tinboundTracks: number;\n\n\t/** Of those, how many were linked to the outbound track that published them. */\n\tlinkedInboundTracks: number;\n\n\t/** `linkedInboundTracks / inboundTracks`. */\n\tlinkedRatio: number;\n\n\t/** A call that presented the conditions, for the reader to go and look at. */\n\texampleCallId?: string;\n};\n\nexport type RemoteTrackResolverReportPayload = ({\n\n\t/** The resolver is linking subscribers to publishers. The detectors that need links will work. */\n\tverdict: 'links-resolved';\n\tevidence: RemoteTrackLinkEvidence;\n} | {\n\n\t/** Every condition for linking was met, repeatedly, and nothing was ever linked. */\n\tverdict: 'no-links-resolved';\n\tevidence: RemoteTrackLinkEvidence;\n} | {\n\n\t/** Never saw a call that could have been linked. **Not a pass.** */\n\tverdict: 'inconclusive';\n\treason: string;\n}) & {\n\tstartedAt: number;\n\n\t/** How many times the check genuinely ran — i.e. how much the verdict is worth. */\n\tchecks: number;\n};\n\nexport type RemoteTrackResolverValidatorConfig = {\n\n\t/**\n\t * Participants a call needs before it can plausibly have publisher↔subscriber links. Default `2`.\n\t *\n\t * A one-person call has nobody to subscribe to anyone, so counting it would dilute the ratio with\n\t * calls that *could not* have produced a link. `2` is the true minimum here and there is little\n\t * reason to raise it.\n\t */\n\tminClients: number;\n\n\t/**\n\t * Inbound tracks a call must have before it counts as a check. Default `2`.\n\t *\n\t * Same idea: no subscribed tracks means nothing to link. Sensible range `2`–`5`.\n\t */\n\tminInboundTracks: number;\n\n\t/**\n\t * Share of inbound tracks that must be linked to conclude the resolver works, `0`–`1`. Default\n\t * `0.5`.\n\t *\n\t * Deliberately lenient, because a partially-linked call is normal: tracks arrive before their\n\t * publisher is known, and simulcast layers or probing streams may have no publisher at all. The\n\t * question is \"is this resolver wired up\", not \"is every track linked\". Typical `0.3`–`0.7`. Raising\n\t * it toward `1` turns the check into a strictness audit and it will report failure on healthy\n\t * systems.\n\t */\n\tlinkedRatioThreshold: number;\n\n\t/**\n\t * Eligible calls to observe before concluding either way. Default `3`.\n\t *\n\t * One call could be a race — every track happening to arrive before its publisher. Typical `3`–`10`.\n\t * Note that a low value makes a *pass* less trustworthy than a failure: linking nothing repeatedly is\n\t * conclusive, linking things once might be luck.\n\t */\n\tminChecks: number;\n};\n\n/**\n * Answers one question: **is the `RemoteTrackResolver` actually linking anything?**\n *\n * ### Why this is worth a validator\n *\n * Four things in this library are built on publisher↔subscriber links —\n * `IssueFanOutDetector`, `TrackDeliveryMismatchDetector`, `UnconsumedTrackDetector` and\n * `SimulcastReceiverValidator`. Every one of them checks `call.remoteTrackResolver` and, finding no\n * links, correctly does nothing rather than guessing.\n *\n * That is the right behaviour and it produces a nasty failure mode: a resolver wired to the wrong id\n * field, or a mediasoup `producerId` the application never attaches, leaves all four permanently\n * silent — and **silence is what a healthy deployment looks like too**. You would conclude your\n * calls were clean when in fact nothing was ever examined. This check exists to make that specific\n * mistake loud.\n *\n * ### `inconclusive` is not a pass\n *\n * A verdict is only reached from calls that *could* have been linked: a resolver configured, at\n * least `minClients` participants, and at least `minInboundTracks` inbound tracks present. A\n * one-to-one deployment, a lobby full of audio-only listeners, or a quiet period never presents\n * those conditions — and concluding \"resolver works\" from calls that had nothing to resolve would be\n * the very mistake this validator is here to catch. `checks` counts the eligible calls actually\n * seen; a validator cancelled before reaching `minChecks` finishes `inconclusive` and says so.\n *\n * ```ts\n * observer.on('validation-ready', ({ validator, report }) => {\n * if (validator !== 'remote-track-resolver' || !report.ready) return;\n * if (report.verdict === 'no-links-resolved') alert('resolver misconfigured — 4 detectors are inert');\n * });\n *\n * observer.addValidator('remote-track-resolver');\n * ```\n *\n * Run it once at start-up, and again after changing the resolver or the SFU's id scheme. Like every\n * validator it is one-shot: the answer is a property of the wiring, not of this moment.\n */\nexport class RemoteTrackResolverValidator implements Validator<RemoteTrackResolverReportPayload> {\n\tpublic static readonly NAME = 'remote-track-resolver' as const;\n\n\tpublic readonly name = RemoteTrackResolverValidator.NAME;\n\n\tpublic readonly startedAt = Date.now();\n\n\tpublic report: ValidationReport<RemoteTrackResolverReportPayload> = { ready: false };\n\n\tprivate readonly _config: RemoteTrackResolverValidatorConfig;\n\n\t/** Accumulated across every eligible call seen, so one small call cannot decide alone. */\n\tprivate _eligibleCalls = 0;\n\tprivate _inboundTracks = 0;\n\tprivate _linkedInboundTracks = 0;\n\tprivate _exampleCallId?: string;\n\tprivate _checks = 0;\n\tprivate _done = false;\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tpublic readonly onDone: (report: ValidationReport<RemoteTrackResolverReportPayload>) => void,\n\t\tconfig: Partial<RemoteTrackResolverValidatorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tminClients: 2,\n\t\t\tminInboundTracks: 2,\n\t\t\tlinkedRatioThreshold: 0.5,\n\t\t\tminChecks: 3,\n\t\t\t...config,\n\t\t};\n\t}\n\n\t/** How many eligible calls were actually examined. `0` means nothing was established. */\n\tpublic get checks(): number {\n\t\treturn this._checks;\n\t}\n\n\tpublic cancel(reason = 'cancelled'): void {\n\t\tthis._finish({ verdict: 'inconclusive', reason });\n\t}\n\n\tpublic update(): void {\n\t\tif (this._done) return;\n\n\t\tfor (const call of this._observer.observedCalls.values()) {\n\t\t\t// No resolver configured is not a failing verdict — it is a deployment that opted out.\n\t\t\tif (!call.remoteTrackResolver) continue;\n\t\t\tif (call.observedClients.size < this._config.minClients) continue;\n\n\t\t\tlet inboundTracks = 0;\n\t\t\tlet linked = 0;\n\n\t\t\tfor (const client of call.observedClients.values()) {\n\t\t\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\t\t\tfor (const inboundTrack of peerConnection.observedInboundTracks.values()) {\n\t\t\t\t\t\t++inboundTracks;\n\t\t\t\t\t\tif (inboundTrack.remoteOutboundTrack !== undefined) ++linked;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Too few tracks to distinguish \"not linked\" from \"nothing to link yet\".\n\t\t\tif (inboundTracks < this._config.minInboundTracks) continue;\n\n\t\t\t++this._checks;\n\t\t\tthis._eligibleCalls += 1;\n\t\t\tthis._inboundTracks += inboundTracks;\n\t\t\tthis._linkedInboundTracks += linked;\n\t\t\tthis._exampleCallId ??= call.callId;\n\n\t\t\t// One linked track anywhere settles it: the resolver is wired to something real. Waiting for\n\t\t\t// a ratio would report a healthy resolver as broken during the seconds after a join, when\n\t\t\t// subscribers legitimately have not been linked yet.\n\t\t\tif (0 < this._linkedInboundTracks) {\n\t\t\t\treturn this._finish({ verdict: 'links-resolved', evidence: this._evidence() });\n\t\t\t}\n\t\t}\n\n\t\t// Nothing has ever linked, across enough independent calls that had every chance to.\n\t\tif (this._config.minChecks <= this._checks && this._linkedInboundTracks === 0) {\n\t\t\tthis._finish({ verdict: 'no-links-resolved', evidence: this._evidence() });\n\t\t}\n\t}\n\n\tprivate _evidence(): RemoteTrackLinkEvidence {\n\t\treturn {\n\t\t\teligibleCalls: this._eligibleCalls,\n\t\t\tinboundTracks: this._inboundTracks,\n\t\t\tlinkedInboundTracks: this._linkedInboundTracks,\n\t\t\tlinkedRatio: 0 < this._inboundTracks ? this._linkedInboundTracks / this._inboundTracks : 0,\n\t\t\texampleCallId: this._exampleCallId,\n\t\t};\n\t}\n\n\tprivate _finish(outcome:\n\t| { verdict: 'links-resolved', evidence: RemoteTrackLinkEvidence }\n\t| { verdict: 'no-links-resolved', evidence: RemoteTrackLinkEvidence }\n\t| { verdict: 'inconclusive', reason: string },\n\t): void {\n\t\tif (this._done) return;\n\t\tthis._done = true;\n\n\t\tconst decidedAt = Date.now();\n\n\t\tthis.report = {\n\t\t\tready: true,\n\t\t\tdecidedAt,\n\t\t\tstartedAt: this.startedAt,\n\t\t\tchecks: this._checks,\n\t\t\t...outcome,\n\t\t};\n\n\t\tif (outcome.verdict === 'no-links-resolved') {\n\t\t\tthis._observer.addIssue({\n\t\t\t\ttype: UNRESOLVED_TRACK_LINKS_ISSUE,\n\t\t\t\ttimestamp: decidedAt,\n\t\t\t\tconclusion: {\n\t\t\t\t\tfaultDomain: 'infrastructure',\n\t\t\t\t\tsummary: 'a RemoteTrackResolver is configured but has never linked a subscriber to a publisher',\n\t\t\t\t\trecommendation: 'check the id the resolver joins on (mediasoup producerId, or your own convention) — until it links, IssueFanOutDetector, TrackDeliveryMismatchDetector, UnconsumedTrackDetector and SimulcastReceiverValidator all silently do nothing',\n\t\t\t\t\tconfidence: 0.9,\n\t\t\t\t},\n\t\t\t\tpayload: { checks: this._checks, ...outcome.evidence },\n\t\t\t});\n\t\t}\n\n\t\tthis.onDone(this.report);\n\t}\n}\n","import type { Observer } from '../Observer';\nimport type { Validator, ValidationReport } from './Validator';\n\n/** Raised once if the deployment is not actually delivering the codec it thinks it is. */\nexport const CODEC_MISMATCH_ISSUE = 'CODEC_INCONSISTENCY';\n\n/** What the check saw across a call's participants. */\nexport type CodecEvidence = {\n\tcallId: string;\n\tkind: 'audio' | 'video';\n\n\t/** Every mime type in use in that call, most common first — e.g. `[ 'video/VP8', 'video/H264' ]`. */\n\tmimeTypes: string[];\n\n\t/** How many clients used each, in the same order as {@link mimeTypes}. */\n\tclientsPerMimeType: number[];\n\n\t/** Clients considered — those that reported at least one codec of this kind. */\n\tclients: number;\n\n\t/** The codec the check was told to expect, when it was given one. */\n\texpected?: string;\n};\n\nexport type CodecConsistencyReportPayload = ({\n\n\t/** One codec per media kind, and it is the expected one if an expectation was given. */\n\tverdict: 'codec-consistent';\n\tevidence: CodecEvidence[];\n} | {\n\n\t/** Participants of one call are split across different codecs. */\n\tverdict: 'codec-split';\n\tevidence: CodecEvidence[];\n} | {\n\n\t/** Consistent, but not what the deployment believes it negotiated. */\n\tverdict: 'unexpected-codec';\n\tevidence: CodecEvidence[];\n} | {\n\n\t/** Never saw a call with enough participants reporting codecs. **Not a pass.** */\n\tverdict: 'inconclusive';\n\treason: string;\n}) & {\n\tstartedAt: number;\n\tchecks: number;\n};\n\nexport type CodecConsistencyValidatorConfig = {\n\n\t/**\n\t * The mime type you believe you are delivering, per kind — e.g.\n\t * `{ video: 'video/VP8', audio: 'audio/opus' }`.\n\t *\n\t * Optional. Without it the check still reports a *split* (participants disagreeing with each\n\t * other), which needs no expectation to be a fact. With it, the check can additionally catch the\n\t * case where everyone agrees on the wrong thing — a silent fallback that nothing else notices.\n\t */\n\texpected?: Partial<Record<'audio' | 'video', string>>;\n\n\t/**\n\t * Which kinds to inspect. Default `[ 'audio', 'video' ]`.\n\t *\n\t * Narrow it when only one matters: audio codec splits are the ones that usually cost transcoding,\n\t * while video splits are more often a deliberate per-client decision.\n\t */\n\tkinds: ('audio' | 'video')[];\n\n\t/**\n\t * Participants a call needs before disagreement is meaningful. Default `3`.\n\t *\n\t * In a 1:1 call \"the participants disagree\" is two clients differing, which can be a legitimate\n\t * negotiation outcome rather than a fault. Sensible range `3`–`5`.\n\t */\n\tminClients: number;\n\n\t/**\n\t * Calls to inspect before concluding. Default `3`.\n\t *\n\t * A structural property of your negotiation, so a handful of calls is plenty — but one call could be\n\t * an unusual mix of participants. Typical `3`–`10`. Higher delays the verdict without adding much,\n\t * since the answer does not vary call to call.\n\t */\n\tminChecks: number;\n};\n\ntype KindTally = { mimeTypes: Map<string, Set<string>> };\n\n/**\n * Answers: **is every participant of a call actually using the same codec — and is it the one you\n * think you negotiated?**\n *\n * ### Why the server has to answer this\n *\n * A client knows only its own codec. It cannot tell whether it is the odd one out, and an SFU that\n * forwards without transcoding cannot serve a call where participants disagree — so a split is a\n * real fault with a very confusing symptom: some pairs of participants see each other and some do\n * not, with no error anywhere. Only something holding every participant of a call at once can see\n * the split at all.\n *\n * The second half is the quieter failure. A deployment configured for VP9 or AV1 will fall back to\n * VP8 whenever one endpoint cannot negotiate the preferred codec, and nothing reports that — the\n * call works, the bitrate is higher than it should be, and the team believes it shipped AV1 months\n * ago. Give the check an `expected` mime type and it will say so.\n *\n * ### Why a validator and not a detector\n *\n * The answer is a property of the deployment — SDP munging, codec preferences, the SFU build — not\n * of this moment. A deployment that negotiates VP8 at 09:00 negotiates VP8 at 17:00. Re-deriving it\n * every tick would walk every codec of every peer connection of every call, forever, to re-learn a\n * constant. So it decides once and stops.\n *\n * Start it again after a deploy, or after changing codec preferences:\n *\n * ```ts\n * observer.addValidator('codec-consistency', {\n * expected: { video: 'video/VP9', audio: 'audio/opus' },\n * });\n *\n * observer.on('validation-ready', ({ validator, report }) => {\n * if (validator !== 'codec-consistency' || !report.ready) return;\n * // 'codec-consistent' | 'codec-split' | 'unexpected-codec' | 'inconclusive'\n * });\n * ```\n *\n * ### `inconclusive` is not a pass\n *\n * Only calls with at least `minClients` participants *reporting codecs of that kind* count as a\n * check. An audio-only deployment will never say anything about video, and concluding \"video codecs\n * are consistent\" from calls that carried no video would verify nothing.\n *\n * ### Comparison is by mime type only\n *\n * `sdpFmtpLine` carries profile and level — `profile-level-id` for H.264, `profile-id` for VP9 — and\n * two clients on the same mime type with different profiles are not truly interchangeable. That is\n * deliberately out of scope: fmtp differences are common, usually benign, and would make this check\n * noisy enough to ignore. It answers the coarse question, which is the one that is actually wrong in\n * practice.\n */\nexport class CodecConsistencyValidator implements Validator<CodecConsistencyReportPayload> {\n\tpublic static readonly NAME = 'codec-consistency' as const;\n\n\tpublic readonly name = CodecConsistencyValidator.NAME;\n\n\tpublic readonly startedAt = Date.now();\n\n\tpublic report: ValidationReport<CodecConsistencyReportPayload> = { ready: false };\n\n\tprivate readonly _config: CodecConsistencyValidatorConfig;\n\tprivate readonly _inspectedCallIds = new Set<string>();\n\tprivate _evidence: CodecEvidence[] = [];\n\tprivate _checks = 0;\n\tprivate _done = false;\n\n\tpublic constructor(\n\t\tprivate readonly _observer: Observer,\n\t\tpublic readonly onDone: (report: ValidationReport<CodecConsistencyReportPayload>) => void,\n\t\tconfig: Partial<CodecConsistencyValidatorConfig> = {},\n\t) {\n\t\tthis._config = {\n\t\t\tkinds: [ 'audio', 'video' ],\n\t\t\tminClients: 3,\n\t\t\tminChecks: 3,\n\t\t\t...config,\n\t\t};\n\t}\n\n\tpublic get checks(): number {\n\t\treturn this._checks;\n\t}\n\n\tpublic cancel(reason = 'cancelled'): void {\n\t\tthis._finish({ verdict: 'inconclusive', reason });\n\t}\n\n\tpublic update(): void {\n\t\tif (this._done) return;\n\n\t\tfor (const call of this._observer.observedCalls.values()) {\n\t\t\t// One verdict per call. Without this a long-lived call would be re-counted every tick and\n\t\t\t// `minChecks` would be satisfied by a single call observed three times.\n\t\t\tif (this._inspectedCallIds.has(call.callId)) continue;\n\t\t\tif (call.observedClients.size < this._config.minClients) continue;\n\n\t\t\tconst tallies = this._tallyOf(call.callId);\n\n\t\t\tif (tallies.length === 0) continue;\n\n\t\t\tthis._inspectedCallIds.add(call.callId);\n\t\t\t++this._checks;\n\t\t\tthis._evidence.push(...tallies);\n\n\t\t\t// A split is decisive on its own: participants of one call disagreeing is a fault whatever\n\t\t\t// the other calls do, so there is nothing to be gained by waiting.\n\t\t\tconst split = tallies.find((evidence) => 1 < evidence.mimeTypes.length);\n\n\t\t\tif (split) return this._finish({ verdict: 'codec-split', evidence: this._evidence });\n\t\t}\n\n\t\tif (this._checks < this._config.minChecks) return;\n\n\t\tconst unexpected = this._evidence.filter(\n\t\t\t(evidence) => evidence.expected !== undefined && evidence.mimeTypes[0] !== evidence.expected,\n\t\t);\n\n\t\tif (0 < unexpected.length) {\n\t\t\treturn this._finish({ verdict: 'unexpected-codec', evidence: this._evidence });\n\t\t}\n\n\t\tthis._finish({ verdict: 'codec-consistent', evidence: this._evidence });\n\t}\n\n\t/** One evidence entry per configured kind that the call actually carried. */\n\tprivate _tallyOf(callId: string): CodecEvidence[] {\n\t\tconst call = this._observer.observedCalls.get(callId);\n\n\t\tif (!call) return [];\n\n\t\tconst byKind = new Map<'audio' | 'video', KindTally>();\n\n\t\tfor (const client of call.observedClients.values()) {\n\t\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\t\tfor (const codec of peerConnection.observedCodecs.values()) {\n\t\t\t\t\tconst kind = mediaKindOf(codec.mimeType);\n\n\t\t\t\t\tif (kind === undefined || !this._config.kinds.includes(kind)) continue;\n\n\t\t\t\t\tconst tally = byKind.get(kind) ?? { mimeTypes: new Map<string, Set<string>>() };\n\t\t\t\t\tconst clientIds = tally.mimeTypes.get(codec.mimeType) ?? new Set<string>();\n\n\t\t\t\t\t// Counted per client, not per codec entry: a peer connection lists every negotiated\n\t\t\t\t\t// codec, including ones it never sends with, and counting those would report a split\n\t\t\t\t\t// in every call on earth.\n\t\t\t\t\tclientIds.add(client.clientId);\n\t\t\t\t\ttally.mimeTypes.set(codec.mimeType, clientIds);\n\t\t\t\t\tbyKind.set(kind, tally);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst evidence: CodecEvidence[] = [];\n\n\t\tfor (const [ kind, tally ] of byKind) {\n\t\t\tconst ranked = [ ...tally.mimeTypes.entries() ].sort((a, b) => b[1].size - a[1].size);\n\t\t\tconst clients = new Set<string>();\n\n\t\t\tfor (const [ , clientIds ] of ranked) {\n\t\t\t\tfor (const clientId of clientIds) clients.add(clientId);\n\t\t\t}\n\n\t\t\tif (clients.size < this._config.minClients) continue;\n\n\t\t\tevidence.push({\n\t\t\t\tcallId,\n\t\t\t\tkind,\n\t\t\t\tmimeTypes: ranked.map(([ mimeType ]) => mimeType),\n\t\t\t\tclientsPerMimeType: ranked.map(([ , clientIds ]) => clientIds.size),\n\t\t\t\tclients: clients.size,\n\t\t\t\texpected: this._config.expected?.[kind],\n\t\t\t});\n\t\t}\n\n\t\treturn evidence;\n\t}\n\n\tprivate _finish(outcome:\n\t| { verdict: 'codec-consistent', evidence: CodecEvidence[] }\n\t| { verdict: 'codec-split', evidence: CodecEvidence[] }\n\t| { verdict: 'unexpected-codec', evidence: CodecEvidence[] }\n\t| { verdict: 'inconclusive', reason: string },\n\t): void {\n\t\tif (this._done) return;\n\t\tthis._done = true;\n\n\t\tconst decidedAt = Date.now();\n\n\t\tthis.report = {\n\t\t\tready: true,\n\t\t\tdecidedAt,\n\t\t\tstartedAt: this.startedAt,\n\t\t\tchecks: this._checks,\n\t\t\t...outcome,\n\t\t};\n\n\t\tif (outcome.verdict === 'codec-split' || outcome.verdict === 'unexpected-codec') {\n\t\t\tthis._observer.addIssue({\n\t\t\t\ttype: CODEC_MISMATCH_ISSUE,\n\t\t\t\ttimestamp: decidedAt,\n\t\t\t\tconclusion: {\n\t\t\t\t\tfaultDomain: 'infrastructure',\n\t\t\t\t\tsummary: outcome.verdict === 'codec-split'\n\t\t\t\t\t\t? 'participants of one call are using different codecs — an SFU that forwards without transcoding cannot serve all of them'\n\t\t\t\t\t\t: 'the deployment is consistently negotiating a codec other than the expected one',\n\t\t\t\t\trecommendation: outcome.verdict === 'codec-split'\n\t\t\t\t\t\t? 'check codec preferences and any SDP munging; a split usually means one endpoint could not negotiate the preferred codec and the others were not renegotiated with it'\n\t\t\t\t\t\t: 'check codec preferences and endpoint support — a silent fallback keeps working, at a higher bitrate than you budgeted for',\n\t\t\t\t\tconfidence: 0.85,\n\t\t\t\t},\n\t\t\t\tpayload: {\n\t\t\t\t\tverdict: outcome.verdict,\n\t\t\t\t\tchecks: this._checks,\n\t\t\t\t\tevidence: outcome.evidence,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tthis.onDone(this.report);\n\t}\n}\n\n/** `'video/VP8'` -> `'video'`. `undefined` for anything that is not an audio/video mime type. */\nfunction mediaKindOf(mimeType: string): 'audio' | 'video' | undefined {\n\tif (mimeType.startsWith('video/')) return 'video';\n\tif (mimeType.startsWith('audio/')) return 'audio';\n\n\treturn undefined;\n}\n","\nexport const schemaVersion = \"3.7.0\";\n\n/**\n* The WebRTC app provided custom stats payload\n*/\nexport type ExtensionStat = {\n\t/**\n\t* The type of the extension stats the custom app provides\n\t*/\n\ttype: string;\n\n\t/**\n\t* The payload of the extension stats the custom app provides\n\t*/\n\tpayload?: Record<string, unknown>;\n\n}\n\n/**\n* A list of additional client events.\n*/\nexport type ClientMetaData = {\n\t/**\n\t* The name of the event used as an identifier (e.g., MEDIA_TRACK_MUTED, USER_REJOINED, etc.).\n\t*/\n\ttype: string;\n\n\t/**\n\t* The attributes of the meta data entry, if applicable.\n\t*/\n\tpayload?: Record<string, unknown>;\n\n\t/**\n\t* The unique identifier of the peer connection for which the event was generated.\n\t*/\n\tpeerConnectionId?: string;\n\n\t/**\n\t* The identifier of the media track related to the event, if applicable.\n\t*/\n\ttrackId?: string;\n\n\t/**\n\t* The SSRC (Synchronization Source) identifier associated with the event, if applicable.\n\t*/\n\tssrc?: number;\n\n\t/**\n\t* The timestamp in epoch format when the event was generated.\n\t*/\n\ttimestamp?: number;\n\n}\n\n/**\n* A list of client issues.\n*/\nexport type ClientIssue = {\n\t/**\n\t* The name of the issue\n\t*/\n\ttype: string;\n\n\t/**\n\t* Identifier of the related issue or resolution when it is provided.\n\t*/\n\tkey?: string;\n\n\t/**\n\t* The attributes of the issue, if applicable.\n\t*/\n\tpayload?: Record<string, unknown>;\n\n\t/**\n\t* The timestamp in epoch format when the event was generated.\n\t*/\n\ttimestamp?: number;\n\n}\n\n/**\n* A list of client events.\n*/\nexport type ClientEvent = {\n\t/**\n\t* The name of the event used as an identifier (e.g., MEDIA_TRACK_MUTED, USER_REJOINED, etc.).\n\t*/\n\ttype: string;\n\n\t/**\n\t* The attributes of the event, if applicable.\n\t*/\n\tpayload?: Record<string, unknown>;\n\n\t/**\n\t* The timestamp in epoch format when the event was generated.\n\t*/\n\ttimestamp?: number;\n\n}\n\n/**\n* Certificate Stats\n*/\nexport type CertificateStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The fingerprint of the certificate.\n\t*/\n\tfingerprint?: string;\n\n\t/**\n\t* The algorithm used for the fingerprint (e.g., 'SHA-256').\n\t*/\n\tfingerprintAlgorithm?: string;\n\n\t/**\n\t* The certificate encoded in base64 format.\n\t*/\n\tbase64Certificate?: string;\n\n\t/**\n\t* The certificate ID of the issuer.\n\t*/\n\tissuerCertificateId?: string;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* ICE Candidate Pair Stats\n*/\nexport type IceCandidatePairStats = {\n\t/**\n\t* The unique identifier for this RTCStats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* The timestamp of when the stats were recorded, in milliseconds.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The transport id of the connection this candidate pair belongs to.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* The ID of the local ICE candidate in this pair.\n\t*/\n\tlocalCandidateId?: string;\n\n\t/**\n\t* The ID of the remote ICE candidate in this pair.\n\t*/\n\tremoteCandidateId?: string;\n\n\t/**\n\t* The checklist state of this candidate pair. Values follow the W3C RTCStatsIceCandidatePairState enum (frozen, waiting, in-progress, failed, succeeded). Two further values are accepted for backward compatibility and are not part of the current spec: `new` (never standardised) and `cancelled` (removed from the spec after 2016).\n\t*/\n\tstate?: \"new\" | \"frozen\" | \"in-progress\" | \"waiting\" | \"failed\" | \"succeeded\" | \"cancelled\" | \"inprogress\";\n\n\t/**\n\t* Whether this candidate pair has been nominated.\n\t*/\n\tnominated?: boolean;\n\n\t/**\n\t* The number of packets sent using this candidate pair.\n\t*/\n\tpacketsSent?: number;\n\n\t/**\n\t* The number of packets received using this candidate pair.\n\t*/\n\tpacketsReceived?: number;\n\n\t/**\n\t* The total number of bytes sent using this candidate pair.\n\t*/\n\tbytesSent?: number;\n\n\t/**\n\t* The total number of bytes received using this candidate pair.\n\t*/\n\tbytesReceived?: number;\n\n\t/**\n\t* The timestamp of the last packet sent using this candidate pair.\n\t*/\n\tlastPacketSentTimestamp?: number;\n\n\t/**\n\t* The timestamp of the last packet received using this candidate pair.\n\t*/\n\tlastPacketReceivedTimestamp?: number;\n\n\t/**\n\t* The total round trip time (RTT) for this candidate pair in seconds.\n\t*/\n\ttotalRoundTripTime?: number;\n\n\t/**\n\t* The current round trip time (RTT) for this candidate pair in seconds.\n\t*/\n\tcurrentRoundTripTime?: number;\n\n\t/**\n\t* The available outgoing bitrate (in bits per second) for this candidate pair.\n\t*/\n\tavailableOutgoingBitrate?: number;\n\n\t/**\n\t* The available incoming bitrate (in bits per second) for this candidate pair.\n\t*/\n\tavailableIncomingBitrate?: number;\n\n\t/**\n\t* The number of ICE connection requests received by this candidate pair.\n\t*/\n\trequestsReceived?: number;\n\n\t/**\n\t* The number of ICE connection requests sent by this candidate pair.\n\t*/\n\trequestsSent?: number;\n\n\t/**\n\t* The number of ICE connection responses received by this candidate pair.\n\t*/\n\tresponsesReceived?: number;\n\n\t/**\n\t* The number of ICE connection responses sent by this candidate pair.\n\t*/\n\tresponsesSent?: number;\n\n\t/**\n\t* The number of ICE connection consent requests sent by this candidate pair.\n\t*/\n\tconsentRequestsSent?: number;\n\n\t/**\n\t* The number of packets discarded while attempting to send via this candidate pair.\n\t*/\n\tpacketsDiscardedOnSend?: number;\n\n\t/**\n\t* The total number of bytes discarded while attempting to send via this candidate pair.\n\t*/\n\tbytesDiscardedOnSend?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* ICE Candidate Stats\n*/\nexport type IceCandidateStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The transport ID associated with the ICE candidate.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* The IP address of the ICE candidate.\n\t*/\n\taddress?: string;\n\n\t/**\n\t* The port number of the ICE candidate.\n\t*/\n\tport?: number;\n\n\t/**\n\t* The transport protocol used by the candidate (e.g., 'udp', 'tcp').\n\t*/\n\tprotocol?: string;\n\n\t/**\n\t* The type of the ICE candidate (e.g., 'host', 'srflx', 'relay').\n\t*/\n\tcandidateType?: string;\n\n\t/**\n\t* The priority of the ICE candidate.\n\t*/\n\tpriority?: number;\n\n\t/**\n\t* The URL of the ICE candidate.\n\t*/\n\turl?: string;\n\n\t/**\n\t* The protocol used for the relay (e.g., 'tcp', 'udp').\n\t*/\n\trelayProtocol?: string;\n\n\t/**\n\t* A string representing the foundation for the ICE candidate.\n\t*/\n\tfoundation?: string;\n\n\t/**\n\t* The related address for the ICE candidate (if any).\n\t*/\n\trelatedAddress?: string;\n\n\t/**\n\t* The related port for the ICE candidate (if any).\n\t*/\n\trelatedPort?: number;\n\n\t/**\n\t* The username fragment for the ICE candidate.\n\t*/\n\tusernameFragment?: string;\n\n\t/**\n\t* The TCP type of the ICE candidate (e.g., 'active', 'passive').\n\t*/\n\ttcpType?: string;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* ICE Transport Stats\n*/\nexport type IceTransportStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The number of packets sent.\n\t*/\n\tpacketsSent?: number;\n\n\t/**\n\t* The number of packets received.\n\t*/\n\tpacketsReceived?: number;\n\n\t/**\n\t* The number of bytes sent.\n\t*/\n\tbytesSent?: number;\n\n\t/**\n\t* The number of bytes received.\n\t*/\n\tbytesReceived?: number;\n\n\t/**\n\t* The ICE role (e.g., 'controlling', 'controlled').\n\t*/\n\ticeRole?: string;\n\n\t/**\n\t* The local username fragment for ICE.\n\t*/\n\ticeLocalUsernameFragment?: string;\n\n\t/**\n\t* The DTLS transport state (e.g., 'new', 'connecting', 'connected').\n\t*/\n\tdtlsState?: string;\n\n\t/**\n\t* The ICE transport state (e.g., 'new', 'checking', 'connected').\n\t*/\n\ticeState?: string;\n\n\t/**\n\t* The ID of the selected ICE candidate pair.\n\t*/\n\tselectedCandidatePairId?: string;\n\n\t/**\n\t* The ID of the local certificate.\n\t*/\n\tlocalCertificateId?: string;\n\n\t/**\n\t* The ID of the remote certificate.\n\t*/\n\tremoteCertificateId?: string;\n\n\t/**\n\t* The TLS version used for encryption.\n\t*/\n\ttlsVersion?: string;\n\n\t/**\n\t* The DTLS cipher suite used.\n\t*/\n\tdtlsCipher?: string;\n\n\t/**\n\t* The role in the DTLS handshake (e.g., 'client', 'server').\n\t*/\n\tdtlsRole?: string;\n\n\t/**\n\t* The SRTP cipher used for encryption.\n\t*/\n\tsrtpCipher?: string;\n\n\t/**\n\t* The number of changes to the selected ICE candidate pair.\n\t*/\n\tselectedCandidatePairChanges?: number;\n\n\t/**\n\t* Number of congestion control feedback (CCFB) messages sent on this transport.\n\t*/\n\tccfbMessagesSent?: number;\n\n\t/**\n\t* Number of congestion control feedback (CCFB) messages received on this transport.\n\t*/\n\tccfbMessagesReceived?: number;\n\n\t/**\n\t* Additional information attached to this stats.\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Data Channels Stats\n*/\nexport type DataChannelStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The label of the data channel.\n\t*/\n\tlabel?: string;\n\n\t/**\n\t* The protocol of the data channel.\n\t*/\n\tprotocol?: string;\n\n\t/**\n\t* The identifier for the data channel.\n\t*/\n\tdataChannelIdentifier?: number;\n\n\t/**\n\t* The state of the data channel (e.g., 'open', 'closed').\n\t*/\n\tstate?: string;\n\n\t/**\n\t* The number of messages sent on the data channel.\n\t*/\n\tmessagesSent?: number;\n\n\t/**\n\t* The number of bytes sent on the data channel.\n\t*/\n\tbytesSent?: number;\n\n\t/**\n\t* The number of messages received on the data channel.\n\t*/\n\tmessagesReceived?: number;\n\n\t/**\n\t* The number of bytes received on the data channel.\n\t*/\n\tbytesReceived?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* PeerConnection Transport Stats\n*/\nexport type PeerConnectionTransportStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The number of data channels opened.\n\t*/\n\tdataChannelsOpened?: number;\n\n\t/**\n\t* The number of data channels closed.\n\t*/\n\tdataChannelsClosed?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Media Playout Stats\n*/\nexport type MediaPlayoutStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The kind of media (audio/video).\n\t*/\n\tkind: string;\n\n\t/**\n\t* The duration of synthesized audio samples.\n\t*/\n\tsynthesizedSamplesDuration?: number;\n\n\t/**\n\t* The number of synthesized audio samples events.\n\t*/\n\tsynthesizedSamplesEvents?: number;\n\n\t/**\n\t* The total duration of all audio samples.\n\t*/\n\ttotalSamplesDuration?: number;\n\n\t/**\n\t* The total delay experienced during audio playout.\n\t*/\n\ttotalPlayoutDelay?: number;\n\n\t/**\n\t* The total count of audio samples.\n\t*/\n\ttotalSamplesCount?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Audio Source Stats\n*/\nexport type MediaSourceStats = {\n\t/**\n\t* The timestamp of the stat.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* A unique identifier for the stat.\n\t*/\n\tid: string;\n\n\t/**\n\t* The type of media ('audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* The identifier of the media track.\n\t*/\n\ttrackIdentifier?: string;\n\n\t/**\n\t* The current audio level.\n\t*/\n\taudioLevel?: number;\n\n\t/**\n\t* The total audio energy.\n\t*/\n\ttotalAudioEnergy?: number;\n\n\t/**\n\t* The total duration of audio samples.\n\t*/\n\ttotalSamplesDuration?: number;\n\n\t/**\n\t* The echo return loss.\n\t*/\n\techoReturnLoss?: number;\n\n\t/**\n\t* The enhancement of echo return loss.\n\t*/\n\techoReturnLossEnhancement?: number;\n\n\t/**\n\t* The width of the video.\n\t*/\n\twidth?: number;\n\n\t/**\n\t* The height of the video.\n\t*/\n\theight?: number;\n\n\t/**\n\t* The total number of frames.\n\t*/\n\tframes?: number;\n\n\t/**\n\t* The frames per second of the video.\n\t*/\n\tframesPerSecond?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Remote Outbound RTP Stats\n*/\nexport type RemoteOutboundRtpStats = {\n\t/**\n\t* The timestamp for this stats object in DOMHighResTimeStamp format.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The unique identifier for this stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* The SSRC identifier of the RTP stream.\n\t*/\n\tssrc: number;\n\n\t/**\n\t* The type of media ('audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* The ID of the transport used for this stream.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* The ID of the codec used for this stream.\n\t*/\n\tcodecId?: string;\n\n\t/**\n\t* The total number of packets sent on this stream.\n\t*/\n\tpacketsSent?: number;\n\n\t/**\n\t* The total number of bytes sent on this stream.\n\t*/\n\tbytesSent?: number;\n\n\t/**\n\t* The ID of the local object corresponding to this stream.\n\t*/\n\tlocalId?: string;\n\n\t/**\n\t* The remote timestamp for this stats object in DOMHighResTimeStamp format.\n\t*/\n\tremoteTimestamp?: number;\n\n\t/**\n\t* The total number of reports sent on this stream.\n\t*/\n\treportsSent?: number;\n\n\t/**\n\t* The current estimated round-trip time for this stream in seconds.\n\t*/\n\troundTripTime?: number;\n\n\t/**\n\t* The total round-trip time for this stream in seconds.\n\t*/\n\ttotalRoundTripTime?: number;\n\n\t/**\n\t* The total number of round-trip time measurements for this stream.\n\t*/\n\troundTripTimeMeasurements?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* The duration of quality limitation reasons categorized by type.\n*/\nexport type QualityLimitationDurations = {\n\t/**\n\t* Duration of no quality limitation in seconds.\n\t*/\n\tnone: number;\n\n\t/**\n\t* Duration of CPU-based quality limitation in seconds.\n\t*/\n\tcpu: number;\n\n\t/**\n\t* Duration of bandwidth-based quality limitation in seconds.\n\t*/\n\tbandwidth: number;\n\n\t/**\n\t* Duration of other quality limitation reasons in seconds.\n\t*/\n\tother: number;\n\n}\n\n/**\n* Cumulative PSNR measurements for Y, U, V components.\n*/\nexport type PsnrSum = {\n\t/**\n\t* PSNR value for the Y (luminance) component.\n\t*/\n\ty: number;\n\n\t/**\n\t* PSNR value for the U (chrominance) component.\n\t*/\n\tu: number;\n\n\t/**\n\t* PSNR value for the V (chrominance) component.\n\t*/\n\tv: number;\n\n}\n\n/**\n* Outbound RTP Stats\n*/\nexport type OutboundRtpStats = {\n\t/**\n\t* The timestamp for this stats object in DOMHighResTimeStamp format.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The unique identifier for this stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* The SSRC identifier of the RTP stream.\n\t*/\n\tssrc: number;\n\n\t/**\n\t* The type of media ('audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* The ID of the transport used for this stream.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* The ID of the codec used for this stream.\n\t*/\n\tcodecId?: string;\n\n\t/**\n\t* The total number of packets sent on this stream.\n\t*/\n\tpacketsSent?: number;\n\n\t/**\n\t* The total number of bytes sent on this stream.\n\t*/\n\tbytesSent?: number;\n\n\t/**\n\t* The media ID associated with this RTP stream.\n\t*/\n\tmid?: string;\n\n\t/**\n\t* The ID of the media source associated with this stream.\n\t*/\n\tmediaSourceId?: string;\n\n\t/**\n\t* The ID of the remote object corresponding to this stream.\n\t*/\n\tremoteId?: string;\n\n\t/**\n\t* The RID value of the RTP stream.\n\t*/\n\trid?: string;\n\n\t/**\n\t* Index of the encoding in the encoding array.\n\t*/\n\tencodingIndex?: number;\n\n\t/**\n\t* The total number of header bytes sent on this stream.\n\t*/\n\theaderBytesSent?: number;\n\n\t/**\n\t* The number of retransmitted packets sent on this stream.\n\t*/\n\tretransmittedPacketsSent?: number;\n\n\t/**\n\t* The number of retransmitted bytes sent on this stream.\n\t*/\n\tretransmittedBytesSent?: number;\n\n\t/**\n\t* The SSRC for the RTX stream, if applicable.\n\t*/\n\trtxSsrc?: number;\n\n\t/**\n\t* The target bitrate for this RTP stream in bits per second.\n\t*/\n\ttargetBitrate?: number;\n\n\t/**\n\t* The total target encoded bytes for this stream.\n\t*/\n\ttotalEncodedBytesTarget?: number;\n\n\t/**\n\t* The width of the frames sent in pixels.\n\t*/\n\tframeWidth?: number;\n\n\t/**\n\t* The height of the frames sent in pixels.\n\t*/\n\tframeHeight?: number;\n\n\t/**\n\t* The number of frames sent per second.\n\t*/\n\tframesPerSecond?: number;\n\n\t/**\n\t* The total number of frames sent on this stream.\n\t*/\n\tframesSent?: number;\n\n\t/**\n\t* The total number of huge frames sent on this stream.\n\t*/\n\thugeFramesSent?: number;\n\n\t/**\n\t* The total number of frames encoded on this stream.\n\t*/\n\tframesEncoded?: number;\n\n\t/**\n\t* The total number of key frames encoded on this stream.\n\t*/\n\tkeyFramesEncoded?: number;\n\n\t/**\n\t* The sum of QP values for all frames encoded on this stream.\n\t*/\n\tqpSum?: number;\n\n\t/**\n\t* Cumulative PSNR measurements for Y, U, V components.\n\t*/\n\tpsnrSum?: PsnrSum;\n\n\t/**\n\t* Total number of PSNR measurements collected.\n\t*/\n\tpsnrMeasurements?: number;\n\n\t/**\n\t* The total time spent encoding frames on this stream in seconds.\n\t*/\n\ttotalEncodeTime?: number;\n\n\t/**\n\t* The total delay for packets sent on this stream in seconds.\n\t*/\n\ttotalPacketSendDelay?: number;\n\n\t/**\n\t* The reason for any quality limitation on this stream (e.g., 'cpu', 'bandwidth', 'other').\n\t*/\n\tqualityLimitationReason?: string;\n\n\t/**\n\t* The duration of quality limitation reasons categorized by type.\n\t*/\n\tqualityLimitationDurations?: QualityLimitationDurations;\n\n\t/**\n\t* The number of resolution changes due to quality limitations.\n\t*/\n\tqualityLimitationResolutionChanges?: number;\n\n\t/**\n\t* The total number of NACK packets sent on this stream.\n\t*/\n\tnackCount?: number;\n\n\t/**\n\t* The total number of FIR packets sent on this stream.\n\t*/\n\tfirCount?: number;\n\n\t/**\n\t* The total number of PLI packets sent on this stream.\n\t*/\n\tpliCount?: number;\n\n\t/**\n\t* The implementation of the encoder used for this stream.\n\t*/\n\tencoderImplementation?: string;\n\n\t/**\n\t* Indicates whether the encoder is power-efficient.\n\t*/\n\tpowerEfficientEncoder?: boolean;\n\n\t/**\n\t* Indicates whether this stream is actively sending data.\n\t*/\n\tactive?: boolean;\n\n\t/**\n\t* The scalability mode of the encoder used for this stream.\n\t*/\n\tscalabilityMode?: string;\n\n\t/**\n\t* Number of packets sent with ECT(1) congestion marking.\n\t*/\n\tpacketsSentWithEct1?: number;\n\n\t/**\n\t* Additional information attached to this stats.\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Remote Inbound RTP Stats\n*/\nexport type RemoteInboundRtpStats = {\n\t/**\n\t* The timestamp for this stats object in DOMHighResTimeStamp format.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The unique identifier for this stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* The SSRC identifier of the RTP stream.\n\t*/\n\tssrc: number;\n\n\t/**\n\t* The type of media ('audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* The ID of the transport used for this stream.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* The ID of the codec used for this stream.\n\t*/\n\tcodecId?: string;\n\n\t/**\n\t* The total number of packets received on this stream.\n\t*/\n\tpacketsReceived?: number;\n\n\t/**\n\t* Total number of RTP packets received for this SSRC marked with the ECT(1) marking.\n\t*/\n\tpacketsReceivedWithEct1?: number;\n\n\t/**\n\t* Total number of RTP packets received for this SSRC marked with the CE marking.\n\t*/\n\tpacketsReceivedWithCe?: number;\n\n\t/**\n\t* Total number of RTP packets for which an RFC8888 report has been sent with a zero R bit.\n\t*/\n\tpacketsReportedAsLost?: number;\n\n\t/**\n\t* Total number of RTP packets reported as lost but later recovered in a subsequent RFC8888 report.\n\t*/\n\tpacketsReportedAsLostButRecovered?: number;\n\n\t/**\n\t* The total number of packets lost on this stream.\n\t*/\n\tpacketsLost?: number;\n\n\t/**\n\t* The jitter value for this stream in seconds.\n\t*/\n\tjitter?: number;\n\n\t/**\n\t* The ID of the local object corresponding to this remote stream.\n\t*/\n\tlocalId?: string;\n\n\t/**\n\t* The most recent RTT measurement for this stream in seconds.\n\t*/\n\troundTripTime?: number;\n\n\t/**\n\t* The cumulative RTT for all packets on this stream in seconds.\n\t*/\n\ttotalRoundTripTime?: number;\n\n\t/**\n\t* The fraction of packets lost on this stream, calculated over a time interval.\n\t*/\n\tfractionLost?: number;\n\n\t/**\n\t* The total number of RTT measurements for this stream.\n\t*/\n\troundTripTimeMeasurements?: number;\n\n\t/**\n\t* Number of packets with ECT(1) marking that were bleached by a middlebox.\n\t*/\n\tpacketsWithBleachedEct1Marking?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Inbound RTP Stats\n*/\nexport type InboundRtpStats = {\n\t/**\n\t* The time the stats were collected, in high-resolution time.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* Unique identifier of the stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* Synchronization source identifier of the RTP stream.\n\t*/\n\tssrc: number;\n\n\t/**\n\t* Kind of the media (e.g., 'audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* Identifier for the media track associated with the RTP stream.\n\t*/\n\ttrackIdentifier: string;\n\n\t/**\n\t* ID of the transport associated with the RTP stream.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* ID of the codec used for the RTP stream.\n\t*/\n\tcodecId?: string;\n\n\t/**\n\t* Number of packets received on the RTP stream.\n\t*/\n\tpacketsReceived?: number;\n\n\t/**\n\t* Total number of RTP packets received for this SSRC marked with the ECT(1) marking.\n\t*/\n\tpacketsReceivedWithEct1?: number;\n\n\t/**\n\t* Total number of RTP packets received for this SSRC marked with the CE marking.\n\t*/\n\tpacketsReceivedWithCe?: number;\n\n\t/**\n\t* Total number of RTP packets for which an RFC8888 report has been sent with a zero R bit.\n\t*/\n\tpacketsReportedAsLost?: number;\n\n\t/**\n\t* Total number of RTP packets reported as lost but later recovered in a subsequent RFC8888 report.\n\t*/\n\tpacketsReportedAsLostButRecovered?: number;\n\n\t/**\n\t* Number of packets lost on the RTP stream.\n\t*/\n\tpacketsLost?: number;\n\n\t/**\n\t* Jitter of the RTP stream in seconds.\n\t*/\n\tjitter?: number;\n\n\t/**\n\t* The media stream identification tag from the SDP media section.\n\t*/\n\tmid?: string;\n\n\t/**\n\t* Remote stats object ID associated with the RTP stream.\n\t*/\n\tremoteId?: string;\n\n\t/**\n\t* Number of frames decoded.\n\t*/\n\tframesDecoded?: number;\n\n\t/**\n\t* Number of keyframes decoded.\n\t*/\n\tkeyFramesDecoded?: number;\n\n\t/**\n\t* Number of frames rendered.\n\t*/\n\tframesRendered?: number;\n\n\t/**\n\t* Number of frames dropped.\n\t*/\n\tframesDropped?: number;\n\n\t/**\n\t* Width of the decoded video frames.\n\t*/\n\tframeWidth?: number;\n\n\t/**\n\t* Height of the decoded video frames.\n\t*/\n\tframeHeight?: number;\n\n\t/**\n\t* Frame rate in frames per second.\n\t*/\n\tframesPerSecond?: number;\n\n\t/**\n\t* Sum of the Quantization Parameter values for decoded frames.\n\t*/\n\tqpSum?: number;\n\n\t/**\n\t* Total time spent decoding in seconds.\n\t*/\n\ttotalDecodeTime?: number;\n\n\t/**\n\t* Sum of inter-frame delays in seconds.\n\t*/\n\ttotalInterFrameDelay?: number;\n\n\t/**\n\t* Sum of squared inter-frame delays in seconds.\n\t*/\n\ttotalSquaredInterFrameDelay?: number;\n\n\t/**\n\t* Number of times playback was paused.\n\t*/\n\tpauseCount?: number;\n\n\t/**\n\t* Total duration of pauses in seconds.\n\t*/\n\ttotalPausesDuration?: number;\n\n\t/**\n\t* Number of times playback was frozen.\n\t*/\n\tfreezeCount?: number;\n\n\t/**\n\t* Total duration of freezes in seconds.\n\t*/\n\ttotalFreezesDuration?: number;\n\n\t/**\n\t* Timestamp of the last packet received.\n\t*/\n\tlastPacketReceivedTimestamp?: number;\n\n\t/**\n\t* Total header bytes received.\n\t*/\n\theaderBytesReceived?: number;\n\n\t/**\n\t* Total packets discarded.\n\t*/\n\tpacketsDiscarded?: number;\n\n\t/**\n\t* Total bytes received from FEC.\n\t*/\n\tfecBytesReceived?: number;\n\n\t/**\n\t* Total packets received from FEC.\n\t*/\n\tfecPacketsReceived?: number;\n\n\t/**\n\t* Total FEC packets discarded.\n\t*/\n\tfecPacketsDiscarded?: number;\n\n\t/**\n\t* Total bytes received on the RTP stream.\n\t*/\n\tbytesReceived?: number;\n\n\t/**\n\t* Number of NACKs received.\n\t*/\n\tnackCount?: number;\n\n\t/**\n\t* Number of Full Intra Requests received.\n\t*/\n\tfirCount?: number;\n\n\t/**\n\t* Number of Picture Loss Indications received.\n\t*/\n\tpliCount?: number;\n\n\t/**\n\t* Total processing delay in seconds.\n\t*/\n\ttotalProcessingDelay?: number;\n\n\t/**\n\t* Estimated timestamp of playout.\n\t*/\n\testimatedPlayoutTimestamp?: number;\n\n\t/**\n\t* Total jitter buffer delay in seconds.\n\t*/\n\tjitterBufferDelay?: number;\n\n\t/**\n\t* Target delay for the jitter buffer in seconds.\n\t*/\n\tjitterBufferTargetDelay?: number;\n\n\t/**\n\t* Number of packets emitted from the jitter buffer.\n\t*/\n\tjitterBufferEmittedCount?: number;\n\n\t/**\n\t* Minimum delay of the jitter buffer in seconds.\n\t*/\n\tjitterBufferMinimumDelay?: number;\n\n\t/**\n\t* Total audio samples received.\n\t*/\n\ttotalSamplesReceived?: number;\n\n\t/**\n\t* Number of concealed audio samples.\n\t*/\n\tconcealedSamples?: number;\n\n\t/**\n\t* Number of silent audio samples concealed.\n\t*/\n\tsilentConcealedSamples?: number;\n\n\t/**\n\t* Number of audio concealment events.\n\t*/\n\tconcealmentEvents?: number;\n\n\t/**\n\t* Number of audio samples inserted for deceleration.\n\t*/\n\tinsertedSamplesForDeceleration?: number;\n\n\t/**\n\t* Number of audio samples removed for acceleration.\n\t*/\n\tremovedSamplesForAcceleration?: number;\n\n\t/**\n\t* Audio level in the range [0.0, 1.0].\n\t*/\n\taudioLevel?: number;\n\n\t/**\n\t* Total audio energy in the stream.\n\t*/\n\ttotalAudioEnergy?: number;\n\n\t/**\n\t* Total duration of all received audio samples in seconds.\n\t*/\n\ttotalSamplesDuration?: number;\n\n\t/**\n\t* Total number of frames received.\n\t*/\n\tframesReceived?: number;\n\n\t/**\n\t* Decoder implementation used for decoding frames.\n\t*/\n\tdecoderImplementation?: string;\n\n\t/**\n\t* Playout identifier for the RTP stream.\n\t*/\n\tplayoutId?: string;\n\n\t/**\n\t* Indicates if the decoder is power-efficient.\n\t*/\n\tpowerEfficientDecoder?: boolean;\n\n\t/**\n\t* Number of frames assembled from multiple packets.\n\t*/\n\tframesAssembledFromMultiplePackets?: number;\n\n\t/**\n\t* Total assembly time for frames in seconds.\n\t*/\n\ttotalAssemblyTime?: number;\n\n\t/**\n\t* Number of retransmitted packets received.\n\t*/\n\tretransmittedPacketsReceived?: number;\n\n\t/**\n\t* Number of retransmitted bytes received.\n\t*/\n\tretransmittedBytesReceived?: number;\n\n\t/**\n\t* SSRC of the retransmission stream.\n\t*/\n\trtxSsrc?: number;\n\n\t/**\n\t* SSRC of the FEC stream.\n\t*/\n\tfecSsrc?: number;\n\n\t/**\n\t* Total corruption probability of packets.\n\t*/\n\ttotalCorruptionProbability?: number;\n\n\t/**\n\t* Total squared corruption probability of packets.\n\t*/\n\ttotalSquaredCorruptionProbability?: number;\n\n\t/**\n\t* Number of corruption measurements.\n\t*/\n\tcorruptionMeasurements?: number;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Codec items\n*/\nexport type CodecStats = {\n\t/**\n\t* The timestamp when the stats were generated.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The unique identifier for the stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* The MIME type of the codec.\n\t*/\n\tmimeType: string;\n\n\t/**\n\t* The payload type of the codec.\n\t*/\n\tpayloadType?: number;\n\n\t/**\n\t* The identifier of the transport associated with the codec.\n\t*/\n\ttransportId?: string;\n\n\t/**\n\t* The clock rate of the codec in Hz.\n\t*/\n\tclockRate?: number;\n\n\t/**\n\t* The number of audio channels for the codec, if applicable.\n\t*/\n\tchannels?: number;\n\n\t/**\n\t* The SDP format-specific parameters line for the codec.\n\t*/\n\tsdpFmtpLine?: string;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Outbound Track Stats items\n*/\nexport type OutboundTrackSample = {\n\t/**\n\t* The timestamp when the stats were generated.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The unique identifier for the stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* Kind of the media (e.g., 'audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* Calculated score for track (details should be added to scoreReasons)\n\t*/\n\tscore?: number;\n\n\t/**\n\t* Reasons for the score calculation, mapping each reason to how much it contributed to the score\n\t*/\n\tscoreReasons?: Record<string, number>;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* Inbound Track Stats items\n*/\nexport type InboundTrackSample = {\n\t/**\n\t* The timestamp when the stats were generated.\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* The unique identifier for the stats object.\n\t*/\n\tid: string;\n\n\t/**\n\t* Kind of the media (e.g., 'audio' or 'video').\n\t*/\n\tkind: string;\n\n\t/**\n\t* Calculated score for track (details should be added to scoreReasons)\n\t*/\n\tscore?: number;\n\n\t/**\n\t* Reasons for the score calculation, mapping each reason to how much it contributed to the score\n\t*/\n\tscoreReasons?: Record<string, number>;\n\n\t/**\n\t* Additional information attached to this stats\n\t*/\n\tattachments?: Record<string, unknown>;\n\n}\n\n/**\n* A sample containing statistics and metrics for a WebRTC peer connection\n*/\nexport type PeerConnectionSample = {\n\t/**\n\t* Unique identifier of the stats object.\n\t*/\n\tpeerConnectionId: string;\n\n\t/**\n\t* Additional information attached to this sample\n\t*/\n\tattachments?: Record<string, unknown>;\n\n\t/**\n\t* Calculated score for peer connection (details should be added to scoreReasons)\n\t*/\n\tscore?: number;\n\n\t/**\n\t* Reasons for the score calculation, mapping each reason to how much it contributed to the score\n\t*/\n\tscoreReasons?: Record<string, number>;\n\n\t/**\n\t* Inbound Track Stats items\n\t*/\n\tinboundTracks?: InboundTrackSample[];\n\n\t/**\n\t* Outbound Track Stats items\n\t*/\n\toutboundTracks?: OutboundTrackSample[];\n\n\t/**\n\t* Codec items\n\t*/\n\tcodecs?: CodecStats[];\n\n\t/**\n\t* Inbound RTP Stats\n\t*/\n\tinboundRtps?: InboundRtpStats[];\n\n\t/**\n\t* Remote Inbound RTP Stats\n\t*/\n\tremoteInboundRtps?: RemoteInboundRtpStats[];\n\n\t/**\n\t* Outbound RTP Stats\n\t*/\n\toutboundRtps?: OutboundRtpStats[];\n\n\t/**\n\t* Remote Outbound RTP Stats\n\t*/\n\tremoteOutboundRtps?: RemoteOutboundRtpStats[];\n\n\t/**\n\t* Audio Source Stats\n\t*/\n\tmediaSources?: MediaSourceStats[];\n\n\t/**\n\t* Media Playout Stats\n\t*/\n\tmediaPlayouts?: MediaPlayoutStats[];\n\n\t/**\n\t* PeerConnection Transport Stats\n\t*/\n\tpeerConnectionTransports?: PeerConnectionTransportStats[];\n\n\t/**\n\t* Data Channels Stats\n\t*/\n\tdataChannels?: DataChannelStats[];\n\n\t/**\n\t* ICE Transport Stats\n\t*/\n\ticeTransports?: IceTransportStats[];\n\n\t/**\n\t* ICE Candidate Stats\n\t*/\n\ticeCandidates?: IceCandidateStats[];\n\n\t/**\n\t* ICE Candidate Pair Stats\n\t*/\n\ticeCandidatePairs?: IceCandidatePairStats[];\n\n\t/**\n\t* Certificate Stats\n\t*/\n\tcertificates?: CertificateStats[];\n\n}\n\n/**\n* docs\n*/\nexport type ClientSample = {\n\t/**\n\t* The timestamp the sample is created in GMT\n\t*/\n\ttimestamp: number;\n\n\t/**\n\t* the unique identifier of the call or session\n\t*/\n\tcallId?: string;\n\n\t/**\n\t* Unique id of the client providing samples.\n\t*/\n\tclientId?: string;\n\n\t/**\n\t* Additional information attached to this sample (e.g.: roomId, userId, displayName, etc...)\n\t*/\n\tattachments?: Record<string, unknown>;\n\n\t/**\n\t* Calculated score for client (details should be added to scoreReasons)\n\t*/\n\tscore?: number;\n\n\t/**\n\t* Reasons for the score calculation, mapping each reason to how much it contributed to the score\n\t*/\n\tscoreReasons?: Record<string, number>;\n\n\t/**\n\t* Samples taken PeerConnections\n\t*/\n\tpeerConnections?: PeerConnectionSample[];\n\n\t/**\n\t* A list of client events.\n\t*/\n\tclientEvents?: ClientEvent[];\n\n\t/**\n\t* A list of client issues.\n\t*/\n\tclientIssues?: ClientIssue[];\n\n\t/**\n\t* A list of additional client events.\n\t*/\n\tclientMetaItems?: ClientMetaData[];\n\n\t/**\n\t* The WebRTC app provided custom stats payload\n\t*/\n\textensionStats?: ExtensionStat[];\n\n}\n","import type { ObservedCall } from '../ObservedCall';\nimport type { ObservedClient } from '../ObservedClient';\nimport { StatsSummary, summarize } from './stats';\n\n/** Thresholds deciding when a client counts as degraded on the receiving / sending side. */\nexport type ClientHealthThresholds = {\n\n\t/** Inbound loss fraction across the client's received streams (0..1). */\n\tinboundFractionLost: number;\n\n\t/** Loss fraction reported back about the client's sent streams via RTCP (0..1). */\n\toutboundFractionLost: number;\n\n\t/** Round-trip time (ms). */\n\trttInMs: number;\n\n\t/** Freezes observed across the client's inbound video in the tick. */\n\tfreezeCount: number;\n\n\t/** Concealment fraction across the client's inbound audio (0..1). */\n\tconcealmentRatio: number;\n};\n\nexport const defaultClientHealthThresholds: ClientHealthThresholds = {\n\tinboundFractionLost: 0.03,\n\toutboundFractionLost: 0.03,\n\trttInMs: 400,\n\tfreezeCount: 1,\n\tconcealmentRatio: 0.1,\n};\n\n/** The per-client health view, split by direction. */\nexport type ClientHealth = {\n\tobservedClient: ObservedClient;\n\tclientId: string;\n\n\t/** Receiving (download) side is impaired. */\n\tinboundDegraded: boolean;\n\n\t/** Sending (upload) side is impaired. */\n\toutboundDegraded: boolean;\n\n\t/** `inboundDegraded || outboundDegraded`. */\n\tdegraded: boolean;\n\treasons: string[];\n\n\tinboundFractionLost?: number;\n\toutboundFractionLost?: number;\n\trttInMs?: number;\n\tdeltaFreezeCount: number;\n\tconcealmentRatio?: number;\n\n\t/** Quality-limitation reasons seen on this client's outbound video ('cpu' | 'bandwidth' | …). */\n\tqualityLimitationReasons: string[];\n\n\tusingTURN: boolean;\n\tusingTCP: boolean;\n};\n\n/** Call-level rollup of the per-client health, using percentiles rather than means. */\nexport type CallHealth = {\n\tcallId: string;\n\tclients: ClientHealth[];\n\n\tnumberOfClients: number;\n\tnumberOfDegradedClients: number;\n\tnumberOfInboundDegradedClients: number;\n\tnumberOfOutboundDegradedClients: number;\n\n\t/** degradedClients / clients (0..1). */\n\tdegradedRatio: number;\n\tinboundDegradedRatio: number;\n\toutboundDegradedRatio: number;\n\n\trttInMs?: StatsSummary;\n\tinboundFractionLost?: StatsSummary;\n\tconcealmentRatio?: StatsSummary;\n\n\t/** How many clients reported each quality-limitation reason on their outbound video. */\n\tqualityLimitation: { cpu: number, bandwidth: number, other: number };\n\n\tfreezes: { affectedClients: number, total: number };\n};\n\n/**\n * Aggregates a call along the **client** axis (as `TrackDistributionAggregator` does along the\n * publisher→subscriber axis): per-client health split into sending vs receiving, plus percentile\n * rollups and \"affected ratio\" counts for the whole call.\n *\n * Build once per call and call `aggregate()` on each `call.update()`.\n */\nexport class CallHealthAggregator {\n\tpublic constructor(\n\t\tprivate readonly _call: ObservedCall,\n\t\tpublic readonly thresholds: ClientHealthThresholds = defaultClientHealthThresholds,\n\t) {\n\t}\n\n\tpublic aggregate(): CallHealth {\n\t\tconst clients: ClientHealth[] = [];\n\n\t\tfor (const client of this._call.observedClients.values()) {\n\t\t\tclients.push(this._clientHealth(client));\n\t\t}\n\n\t\tconst degraded = clients.filter((c) => c.degraded);\n\t\tconst inboundDegraded = clients.filter((c) => c.inboundDegraded);\n\t\tconst outboundDegraded = clients.filter((c) => c.outboundDegraded);\n\t\tconst collect = (pick: (c: ClientHealth) => number | undefined) =>\n\t\t\tsummarize(clients.map(pick).filter((v): v is number => typeof v === 'number' && Number.isFinite(v)));\n\n\t\tconst qualityLimitation = { cpu: 0, bandwidth: 0, other: 0 };\n\n\t\tfor (const client of clients) {\n\t\t\tif (client.qualityLimitationReasons.includes('cpu')) qualityLimitation.cpu += 1;\n\t\t\tif (client.qualityLimitationReasons.includes('bandwidth')) qualityLimitation.bandwidth += 1;\n\t\t\tif (client.qualityLimitationReasons.some((r) => r !== 'cpu' && r !== 'bandwidth')) qualityLimitation.other += 1;\n\t\t}\n\n\t\tconst ratio = (n: number) => (0 < clients.length ? n / clients.length : 0);\n\n\t\treturn {\n\t\t\tcallId: this._call.callId,\n\t\t\tclients,\n\t\t\tnumberOfClients: clients.length,\n\t\t\tnumberOfDegradedClients: degraded.length,\n\t\t\tnumberOfInboundDegradedClients: inboundDegraded.length,\n\t\t\tnumberOfOutboundDegradedClients: outboundDegraded.length,\n\t\t\tdegradedRatio: ratio(degraded.length),\n\t\t\tinboundDegradedRatio: ratio(inboundDegraded.length),\n\t\t\toutboundDegradedRatio: ratio(outboundDegraded.length),\n\t\t\trttInMs: collect((c) => c.rttInMs),\n\t\t\tinboundFractionLost: collect((c) => c.inboundFractionLost),\n\t\t\tconcealmentRatio: collect((c) => c.concealmentRatio),\n\t\t\tqualityLimitation,\n\t\t\tfreezes: {\n\t\t\t\taffectedClients: clients.filter((c) => 0 < c.deltaFreezeCount).length,\n\t\t\t\ttotal: clients.reduce((sum, c) => sum + c.deltaFreezeCount, 0),\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate _clientHealth(client: ObservedClient): ClientHealth {\n\t\tlet deltaFreezeCount = 0;\n\t\tlet concealedSamples = 0;\n\t\tlet receivedSamples = 0;\n\t\tlet remoteLostPackets = 0;\n\t\tlet remoteSentPackets = 0;\n\t\tconst qualityLimitationReasons = new Set<string>();\n\n\t\tfor (const peerConnection of client.observedPeerConnections.values()) {\n\t\t\tfor (const rtp of peerConnection.observedInboundRtps.values()) {\n\t\t\t\tdeltaFreezeCount += rtp.deltaFreezeCount;\n\t\t\t\tconcealedSamples += rtp.deltaConcealedSamples;\n\t\t\t\treceivedSamples += rtp.deltaReceivedSamples;\n\t\t\t}\n\t\t\tfor (const rtp of peerConnection.observedOutboundRtps.values()) {\n\t\t\t\tif (rtp.qualityLimitationReason && rtp.qualityLimitationReason !== 'none') {\n\t\t\t\t\tqualityLimitationReasons.add(rtp.qualityLimitationReason);\n\t\t\t\t}\n\t\t\t\t// Loss about our *sent* streams comes back in the receiver report.\n\t\t\t\tif (rtp.remoteFractionLost !== undefined) {\n\t\t\t\t\tremoteLostPackets += rtp.remoteFractionLost * rtp.deltaPacketsSent;\n\t\t\t\t\tremoteSentPackets += rtp.deltaPacketsSent;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst inboundTotal = client.deltaInboundPacketsLost + client.deltaInboundPacketsReceived;\n\t\tconst inboundFractionLost = 0 < inboundTotal ? client.deltaInboundPacketsLost / inboundTotal : undefined;\n\t\tconst outboundFractionLost = 0 < remoteSentPackets ? remoteLostPackets / remoteSentPackets : undefined;\n\t\tconst concealmentRatio = 0 < receivedSamples ? concealedSamples / receivedSamples : undefined;\n\t\tconst rttInMs = client.currentAvgRttInMs;\n\n\t\tconst reasons: string[] = [];\n\n\t\tif (this.thresholds.inboundFractionLost < (inboundFractionLost ?? 0)) reasons.push('inbound-fraction-lost');\n\t\tif (this.thresholds.freezeCount <= deltaFreezeCount) reasons.push('freezes');\n\t\tif (this.thresholds.concealmentRatio < (concealmentRatio ?? 0)) reasons.push('concealment');\n\t\tif (this.thresholds.rttInMs < (rttInMs ?? 0)) reasons.push('rtt');\n\n\t\tconst outboundReasons: string[] = [];\n\n\t\tif (this.thresholds.outboundFractionLost < (outboundFractionLost ?? 0)) outboundReasons.push('outbound-fraction-lost');\n\t\tif (0 < qualityLimitationReasons.size) {\n\t\t\toutboundReasons.push(...[ ...qualityLimitationReasons ].map((r) => `quality-limited-${r}`));\n\t\t}\n\n\t\tconst inboundDegraded = 0 < reasons.length;\n\t\tconst outboundDegraded = 0 < outboundReasons.length;\n\n\t\treturn {\n\t\t\tobservedClient: client,\n\t\t\tclientId: client.clientId,\n\t\t\tinboundDegraded,\n\t\t\toutboundDegraded,\n\t\t\tdegraded: inboundDegraded || outboundDegraded,\n\t\t\treasons: [ ...reasons, ...outboundReasons ],\n\t\t\tinboundFractionLost,\n\t\t\toutboundFractionLost,\n\t\t\trttInMs,\n\t\t\tdeltaFreezeCount,\n\t\t\tconcealmentRatio,\n\t\t\tqualityLimitationReasons: [ ...qualityLimitationReasons ],\n\t\t\tusingTURN: client.usingTURN,\n\t\t\tusingTCP: client.usingTCP,\n\t\t};\n\t}\n}\n","import type { IssueConclusion } from '../detectors/IssueConclusion';\n\n/**\n * What every server-raised finding carries, whatever raised it.\n *\n * ### Why this is not `ClientIssue`\n *\n * `ClientIssue` is a **wire** type: it arrives inside a `ClientSample`, so its `payload` is bound to\n * what the schema can carry — a record of primitives since schema 3.5.0, a JSON string on samples\n * from earlier clients. Server-raised findings were reusing it, which forced every detector to `JSON.stringify` a\n * perfectly good object on the way out and every handler to `JSON.parse` it back on the way in —\n * paying serialisation on a path where nothing is ever serialised, and losing type information in\n * both directions. These go straight to an in-process handler, so they carry the object.\n */\nexport type IssueBase = {\n\n\t/** What was found, e.g. `'CROSS_CALL_ISSUE_ONSET_BURST'`. */\n\ttype: string;\n\n\t/** Observer clock, when the finding was raised. */\n\ttimestamp: number;\n\n\t/**\n\t * What the finding *means* — where to look, and how much the evidence justifies it.\n\t *\n\t * A first-class field rather than a key inside {@link payload}, because it is the one part every\n\t * finding has in common and the one part an alerting rule reads. Burying it in the evidence made\n\t * `payload.conclusion.faultDomain` the path to the most important thing in the object.\n\t */\n\tconclusion?: IssueConclusion;\n\n\t/**\n\t * The evidence, and **only** the evidence.\n\t *\n\t * Deliberately does not repeat `type`, `scope`, or the ids already present on the event that\n\t * delivers it. A payload that restates its envelope invites the two to disagree — and they did,\n\t * because nothing kept them in step.\n\t */\n\tpayload?: Record<string, unknown>;\n};\n\n/**\n * A finding about **one call**, raised by `observedCall.addIssue()` and delivered as `call-issue`.\n *\n * The call is the event's scope (`{ observedCall, observer }`), so the payload does not carry a\n * `callId` — read it from the event.\n */\nexport type CallIssue = IssueBase & {\n\tscope: 'call';\n};\n\n/**\n * A finding about the **fleet**, raised by `observer.addIssue()` and delivered as `observer-issue`.\n *\n * Raised by detectors and validators that reason across calls, so no single call owns it. Where the\n * finding does concern specific calls — a cross-call correlation, say — they are named in the\n * evidence, because that *is* the evidence.\n */\nexport type ObserverIssue = IssueBase & {\n\tscope: 'observer';\n};\n\n/**\n * Either kind, discriminated by {@link IssueBase} + `scope`.\n *\n * `scope` is on the issue and not merely implied by which event fired, so a finding stays\n * self-describing once it leaves the bus — funnelled into one handler, a log line, or a queue.\n */\nexport type Issue = CallIssue | ObserverIssue;\n\n/**\n * The payload as a JSON string, for the boundaries that genuinely need text — a log line, an HTTP\n * body, a message queue.\n *\n * Returns `undefined` for a missing payload, or for one that cannot be serialised (a circular\n * reference from something an application attached): the caller wanted text, not an exception.\n */\nexport function issuePayloadAsString(issue: Pick<IssueBase, 'payload'>): string | undefined {\n\tif (issue.payload === undefined) return undefined;\n\n\ttry {\n\t\treturn JSON.stringify(issue.payload);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n","import { pageHinkley, mannKendallVerdict, type PageHinkleyResult, type MannKendallResult } from './stats';\n\nexport type TrendTesterConfig = {\n\n\t/**\n\t * How many of the most recent values to keep. Default `30`, floored at `2`.\n\t *\n\t * The one knob controlling how far back \"trend\" looks, for both tests — there is deliberately no\n\t * separate window per test. This counts *samples*, so the time it spans depends on how often you\n\t * push. Mann-Kendall needs roughly 8–10 points before its significance test is worth anything, so\n\t * below ~`10` it will mostly answer `no-trend`. Typical `20`–`60`: long enough for a stable\n\t * baseline, short enough that a sustained change eventually becomes the new normal instead of being\n\t * flagged forever.\n\t */\n\tsize?: number;\n\n\t/**\n\t * Page-Hinkley's **drift tolerance** — change smaller than this is treated as noise and never\n\t * accumulated. Default `0`. See {@link pageHinkley}.\n\t *\n\t * Expressed in the units of whatever you push, so there is no universally good value: for RTT in ms\n\t * a few ms is a reasonable tolerance. The default `0` accumulates *every* deviation, which is the\n\t * most sensitive setting. Too low and ordinary fluctuation accumulates into a false step change; too\n\t * high and a real but gradual shift never accumulates at all.\n\t */\n\tpageHinkleyDelta?: number;\n\n\t/**\n\t * Page-Hinkley's **detection threshold** — how much accumulated drift counts as a step change.\n\t * Default `50`. See {@link pageHinkley}.\n\t *\n\t * Also in your units, and the direct sensitivity control: lower detects smaller or earlier steps and\n\t * false-positives more; higher waits for unmistakable ones. Worth tuning against a recorded series\n\t * rather than by intuition, because the right value depends entirely on the scale and noisiness of\n\t * the metric you feed it.\n\t */\n\tpageHinkleyLambda?: number;\n\n\t/**\n\t * Mann-Kendall's significance level. Default `0.05`. See {@link mannKendallVerdict}.\n\t *\n\t * Conventional values are `0.01`, `0.05` and `0.1`. This is the probability of claiming a monotonic\n\t * trend that is not really there: `0.01` is stricter and slower to call a trend, `0.1` more\n\t * sensitive and noisier.\n\t */\n\tmannKendallAlpha?: number;\n};\n\n/**\n * Streaming home for `stats.ts`'s two trend tests: feed it one value at a time via {@link add}\n * instead of re-running the batch functions over an array you manage yourself.\n *\n * ### The two tests answer different questions\n *\n * Take a client's RTT, sampled every couple of seconds. Two things can go wrong with it, and only\n * one of them looks like a spike:\n *\n * - **Mann-Kendall** asks *\"is this drifting?\"* — a monotonic trend, regardless of shape or scale.\n * `40, 45, 52, 61, 70, 84 ms` is a rising path with no single dramatic step; every jump is small\n * and plausible on its own. Mann-Kendall counts how many later samples exceed earlier ones and\n * reports whether that lopsidedness could plausibly be chance. It is rank-based, so one absurd\n * reading (a 4000 ms outlier from a stalled event loop) moves it by exactly one pair, not by the\n * 4000.\n * - **Page-Hinkley** asks *\"did it change, and when?\"* — a step. `40, 42, 39, 41, 180, 176, 182 ms`\n * is not a trend at all; it is one level followed by a different level, which is what a route\n * change or a TURN failover looks like. It accumulates the deviation from the running mean and\n * fires when the cumulative excess passes `lambda`.\n *\n * Neither subsumes the other, which is why both live here on one window. A slow climb toward\n * unusability shows up in Mann-Kendall and never trips Page-Hinkley; a hard failover trips\n * Page-Hinkley immediately while Mann-Kendall may read `no-trend`, because after the step the series\n * is flat again. `tests/trendTester.spec.ts` builds both RTT series and shows exactly this.\n *\n * ```ts\n * const rtt = new TrendTester({ size: 30, mannKendallAlpha: 0.05, pageHinkleyLambda: 50 });\n *\n * peerConnection.on('update', () => {\n * if (peerConnection.currentRttInMs === undefined) return; // no measurement is not a measurement\n * rtt.add(peerConnection.currentRttInMs);\n *\n * if (rtt.mannKendall().trend === 'increasing') warn('RTT is drifting up');\n * if (rtt.pageHinkley()?.changeDetected) {\n * warn('RTT stepped');\n * rtt.clear(); // the old level is no longer the baseline — judge the new one on its own\n * }\n * });\n * ```\n *\n * ### Both read the same window\n *\n * `size` is the one knob controlling how far back either test looks. They are kept incremental\n * differently, because they don't tolerate an evicted point the same way:\n *\n * - **Mann-Kendall**'s statistic is a sum over *pairs*, so evicting the oldest value only touches\n * the pairs it was part of — one pass over the (bounded) window corrects it in O(size) instead of\n * the O(size²) a full recompute costs.\n * - **Page-Hinkley**'s statistic is a running minimum of a cumulative sum, which has no cheap\n * correction for \"forget this one old point\" — the minimum may have depended on it. It is\n * recomputed from the window on every {@link add} rather than hand-rolling an incremental version\n * that would be easy to get subtly wrong. That recompute is O(size), the same order as above.\n *\n * ### Non-finite input is rejected, not absorbed\n *\n * See {@link add}. A single `NaN` would otherwise destroy the instance permanently.\n */\nexport class TrendTester {\n\tprivate readonly _size: number;\n\tprivate readonly _values: number[] = [];\n\tprivate readonly _tieCounts = new Map<number, number>();\n\n\tprivate readonly _pageHinkleyDelta: number;\n\tprivate readonly _pageHinkleyLambda: number;\n\tprivate readonly _mannKendallAlpha: number;\n\n\tprivate _s = 0;\n\tprivate _rejected = 0;\n\tprivate _pageHinkleyResult?: PageHinkleyResult;\n\n\tpublic constructor(config: TrendTesterConfig = {}) {\n\t\t// A window shorter than 2 can hold no pair, so Mann-Kendall has nothing to compare and eviction\n\t\t// (guarded on `length === _size`) would never fire for size 0 — the array would grow forever.\n\t\tthis._size = Math.max(2, Math.floor(config.size ?? 30));\n\t\tthis._pageHinkleyDelta = config.pageHinkleyDelta ?? 0;\n\t\tthis._pageHinkleyLambda = config.pageHinkleyLambda ?? 50;\n\t\tthis._mannKendallAlpha = config.mannKendallAlpha ?? 0.05;\n\t}\n\n\t/** Values rejected by {@link add} for being non-finite. Non-zero means the caller has a bug. */\n\tpublic get rejected(): number {\n\t\treturn this._rejected;\n\t}\n\n\t/** How many values are currently in the window (`<= size`). */\n\tpublic get length(): number {\n\t\treturn this._values.length;\n\t}\n\n\t/** The configured window length. */\n\tpublic get size(): number {\n\t\treturn this._size;\n\t}\n\n\t/**\n\t * Add the next value in the stream, evicting the oldest once the window is full.\n\t *\n\t * **Non-finite values are rejected** rather than stored, and the rejection is counted in\n\t * {@link rejected}. This is not defensive noise — it is the difference between a bad reading and\n\t * a bad instance. `Math.sign(NaN)` is `NaN`, so a single `NaN` would poison the incremental\n\t * Mann-Kendall sum `_s` **permanently**: every later `add` and `_evictOldest` adds or subtracts\n\t * `NaN`, the z-score is `NaN`, every comparison against it is `false`, and the tester silently\n\t * reports `no-trend` forever after. It would also take a `NaN` key in `_tieCounts` that can never\n\t * be matched on eviction, since `NaN !== NaN`.\n\t *\n\t * `undefined` RTT (no measurement this tick) must not be coerced to `0` and passed in either —\n\t * \"we didn't measure\" is not \"the trip took no time\", and feeding zeros manufactures a downward\n\t * trend. Skip the sample instead.\n\t */\n\tpublic add(value: number): void {\n\t\tif (!Number.isFinite(value)) {\n\t\t\t++this._rejected;\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (this._values.length === this._size) this._evictOldest();\n\n\t\tfor (let i = 0; i < this._values.length; i++) this._s += Math.sign(value - this._values[i]);\n\t\tthis._values.push(value);\n\t\tthis._bumpTie(value, 1);\n\n\t\tthis._pageHinkleyResult = pageHinkley(this._values, this._pageHinkleyDelta, this._pageHinkleyLambda);\n\t}\n\n\t/** The current Page-Hinkley read-out over the window. `undefined` before the first value. */\n\tpublic pageHinkley(): PageHinkleyResult | undefined {\n\t\treturn this._pageHinkleyResult;\n\t}\n\n\t/** The current Mann-Kendall read-out over the window. */\n\tpublic mannKendall(): MannKendallResult {\n\t\tconst n = this._values.length;\n\n\t\tif (n < 2) return { s: 0, variance: 0, z: 0, pValue: 1, trend: 'no-trend' };\n\n\t\t// Tie correction is cheap to recompute at read time (one pass over the distinct groups, at\n\t\t// most `size` of them) rather than kept incremental like `_s` — it's only needed here.\n\t\tlet tieCorrection = 0;\n\n\t\tfor (const groupSize of this._tieCounts.values()) {\n\t\t\tif (1 < groupSize) tieCorrection += groupSize * (groupSize - 1) * ((2 * groupSize) + 5);\n\t\t}\n\n\t\tconst variance = ((n * (n - 1) * ((2 * n) + 5)) - tieCorrection) / 18;\n\n\t\treturn { s: this._s, variance, ...mannKendallVerdict(this._s, variance, this._mannKendallAlpha) };\n\t}\n\n\t/** Drop everything, e.g. after a detected change point, to start judging the trend fresh. */\n\tpublic clear(): void {\n\t\tthis._values.length = 0;\n\t\tthis._tieCounts.clear();\n\t\tthis._s = 0;\n\t\tthis._rejected = 0;\n\t\tthis._pageHinkleyResult = undefined;\n\t}\n\n\t/** Remove the oldest value from the window and correct `_s` for the pairs it was part of. */\n\tprivate _evictOldest(): void {\n\t\tconst removed = this._values.shift();\n\n\t\tif (removed === undefined) return;\n\n\t\tfor (let i = 0; i < this._values.length; i++) this._s -= Math.sign(this._values[i] - removed);\n\t\tthis._bumpTie(removed, -1);\n\t}\n\n\tprivate _bumpTie(value: number, delta: number): void {\n\t\tconst count = (this._tieCounts.get(value) ?? 0) + delta;\n\n\t\tif (count <= 0) this._tieCounts.delete(value);\n\t\telse this._tieCounts.set(value, count);\n\t}\n}\n","import { EventEmitter } from 'events';\nimport type { ClientSample } from '../schema/ClientSample';\nimport type { ObservedCall } from '../ObservedCall';\n\n/** The lifecycle events a sink may emit (a subset of Node's writable-stream events). */\nexport type ClientSampleSinkEvents = {\n\n\t/** The destination is fully written and closed (e.g. a file flushed and its fd closed). */\n\tclose: [];\n\n\t/** The destination failed. */\n\terror: [Error];\n\n\t/** `end()` was processed and all queued data was flushed (before `close`). */\n\tfinish: [];\n\n\t/** The buffer drained after backpressure; safe to write more. */\n\tdrain: [];\n};\n\nexport declare interface ClientSampleSink {\n\ton<E extends keyof ClientSampleSinkEvents>(event: E, listener: (...args: ClientSampleSinkEvents[E]) => void): this;\n\tonce<E extends keyof ClientSampleSinkEvents>(event: E, listener: (...args: ClientSampleSinkEvents[E]) => void): this;\n\toff<E extends keyof ClientSampleSinkEvents>(event: E, listener: (...args: ClientSampleSinkEvents[E]) => void): this;\n\temit<E extends keyof ClientSampleSinkEvents>(event: E, ...args: ClientSampleSinkEvents[E]): boolean;\n}\n\n/**\n * A per-client destination for accepted samples — a typed `EventEmitter` base class. One sink is\n * created per `ObservedClient` (via `ObserverConfig.createClientSink`). It is **object-mode**:\n * `write` receives the `ClientSample` itself, so each sink decides how to serialize it (JSON\n * line, protobuf, msgpack, a remote payload, …).\n *\n * The library calls `write(sample)` synchronously per accepted sample (not awaited), `end()`s the\n * sink when the client closes, and attaches an `error` listener so a failing sink can't crash the\n * process. The application — which created the sink — listens for `close` (destination ready,\n * e.g. a file flushed and its fd closed) and `error`.\n *\n * Subclass this and implement `write`/`end` (and `emit` your lifecycle events). Built-in sinks\n * (`JsonlFileSink`, `InMemorySink`) and their factories are exported from the package root.\n */\nexport abstract class ClientSampleSink extends EventEmitter {\n\t/** Accept one sample; the sink serializes it however it likes. `false` signals backpressure. */\n\tpublic abstract write(sample: ClientSample): boolean;\n\n\t/** Finish writing; the sink flushes and (if applicable) emits `close`. */\n\tpublic abstract end(): void;\n}\n\n/**\n * Produces a sink for a newly-created client, or `undefined` for no sink. Receives the\n * client id and its (already-created) parent call, so the destination can be derived\n * from `callId` / `clientId`.\n */\nexport type ClientSampleSinkFactory = (params: {\n\tclientId: string,\n\tobservedCall: ObservedCall,\n}) => ClientSampleSink | undefined;\n","import * as fs from 'fs';\nimport * as path from 'path';\nimport { ClientSampleSink, ClientSampleSinkFactory } from './ClientSampleSink';\nimport type { ClientSample } from '../schema/ClientSample';\n\nexport type JsonlFileSinkOptions = {\n\n\t/** Absolute or relative file path to append JSONL lines to. */\n\tpath: string,\n\n\t/** Optional custom serializer; defaults to `JSON.stringify`. */\n\tserializeSample?: (sample: ClientSample) => string,\n};\n\n/**\n * A sink that serializes each sample to a JSON line and appends it to a file. It wraps an\n * `fs.WriteStream` and re-emits its lifecycle events, so `close` fires once the file is flushed\n * and its descriptor is closed (file ready) and `error` surfaces file errors.\n */\nexport class JsonlFileSink extends ClientSampleSink {\n\t/** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */\n\tpublic readonly path: string;\n\n\tprivate readonly _stream: fs.WriteStream;\n\tprivate readonly _serializeSample: (sample: ClientSample) => string;\n\n\tpublic constructor(options: JsonlFileSinkOptions) {\n\t\tsuper();\n\t\tthis.path = options.path;\n\t\tthis._stream = fs.createWriteStream(this.path, { flags: 'a' });\n\n\t\tthis._stream.on('close', () => this.emit('close'));\n\t\tthis._stream.on('finish', () => this.emit('finish'));\n\t\tthis._stream.on('drain', () => this.emit('drain'));\n\t\tthis._stream.on('error', (err) => this.emit('error', err));\n\n\t\tthis._serializeSample = options.serializeSample ?? ((sample) => JSON.stringify(sample));\n\t}\n\n\tpublic write(sample: ClientSample): boolean {\n\t\treturn this._stream.write(`${this._serializeSample(sample)}\\n`);\n\t}\n\n\tpublic end(): void {\n\t\tthis._stream.end();\n\t}\n}\n\n/** Build a JSONL file sink for a single path. */\nexport function createJsonlFileSink(options: JsonlFileSinkOptions): ClientSampleSink {\n\treturn new JsonlFileSink(options);\n}\n\nexport type JsonlFileSinkFactoryOptions = {\n\n\t/** Directory the per-client files are written into (must exists). */\n\tdirectory: string,\n\n\t/** Builds the file name from the call/client ids. Defaults to `${callId}__${clientId}.jsonl`. */\n\tgetFileName?: (params: { callId: string, clientId: string }) => string,\n\n\t/** Optional custom serializer; defaults to `JSON.stringify`. */\n\tserializeSample?: (sample: ClientSample) => string,\n};\n\n/**\n * Convenience factory that writes one JSONL file per client, with the path derived from\n * `callId` / `clientId`. Plug into `ObserverConfig.createClientSink`.\n */\nexport function createJsonlFileSinkFactory(options: JsonlFileSinkFactoryOptions): ClientSampleSinkFactory {\n\n\treturn ({ clientId, observedCall }) => {\n\t\tconst filename = options.getFileName?.({ callId: observedCall.callId, clientId }) ?? `${observedCall.callId}__${clientId}.jsonl`;\n\n\t\treturn createJsonlFileSink({\n\t\t\tpath: path.join(options.directory, filename),\n\t\t\tserializeSample: options.serializeSample,\n\t\t});\n\t};\n}\n","import { ClientSampleSink } from './ClientSampleSink';\nimport type { ClientSample } from '../schema/ClientSample';\n\n/**\n * A `ClientSampleSink` that collects the accepted sample objects into an array. Useful for\n * tests and offline replay. Emits `close` on `end()`.\n */\nexport class InMemorySink extends ClientSampleSink {\n\tpublic constructor(public readonly samples: ClientSample[] = []) {\n\t\tsuper();\n\t}\n\n\tpublic write(sample: ClientSample): boolean {\n\t\tthis.samples.push(sample);\n\n\t\treturn true;\n\t}\n\n\tpublic end(): void {\n\t\tthis.emit('close');\n\t}\n}\n\nexport function createInMemorySink(samples?: ClientSample[]): InMemorySink {\n\treturn new InMemorySink(samples);\n}\n","import { ObservedCall } from '../ObservedCall';\nimport { ObservedInboundTrack } from '../ObservedInboundTrack';\nimport { ObservedOutboundTrack } from '../ObservedOutboundTrack';\nimport type { ObserverEvents } from '../ObserverEvents';\n\nexport type RemoteTrackResolverFactory = (observedCall: ObservedCall) => RemoteTrackResolver;\n\n/**\n * Strategy functions that map a track to its publish/subscribe identity. Two tracks are linked\n * when a subscribed (inbound) track's **publisher id** equals a published (outbound) track's\n * **publisher id** — one publisher → many subscribers. The **subscriber id** is optional and is\n * only used to look an inbound track up via `getInboundTrackBySubscriberId`.\n *\n * The \"publisher id\" is whatever a strategy uses as the link key: mediasoup passes its producerId,\n * a p2p strategy the SSRC, a generic strategy a shared attachment value.\n */\nexport type RemoteTrackResolvers = {\n\n\t/** The publisher id a subscribed (inbound) track receives. Required — this is the link key. */\n\tresolveInboundTrackPublisherId: (inboundTrack: ObservedInboundTrack) => string | undefined,\n\n\t/** The publisher id a published (outbound) track represents. Required — this is the link key. */\n\tresolveOutboundTrackPublisherId: (outboundTrack: ObservedOutboundTrack) => string | undefined,\n\n\t/** Optional: the inbound track's own subscription id (enables `getInboundTrackBySubscriberId`). */\n\tresolveInboundTrackSubscriberId?: (inboundTrack: ObservedInboundTrack) => string | undefined,\n};\n\n/**\n * Generic, strategy-driven base resolver. It subscribes to the Observer bus (filtered to one call),\n * links subscribed↔published tracks by publisher id, and maintains the links directly on the tracks\n * (`inboundTrack.remoteOutboundTrack`, `outboundTrack.remoteInboundTracks`).\n *\n * Concrete strategies (mediasoup, p2p-by-SSRC, generic attachment) are just different\n * {@link RemoteTrackResolvers} passed to this class. Ids are re-resolved on demand rather than\n * cached, so the only state kept is the two lookup indexes that back the public getters.\n */\nexport class RemoteTrackResolver {\n\tprivate readonly _publisherIdToOutboundTrack = new Map<string, ObservedOutboundTrack>();\n\tprivate readonly _subscriberIdToInboundTrack = new Map<string, ObservedInboundTrack>();\n\n\t/**\n\t * Tracks whose publisher id the strategy could not resolve **yet**.\n\t *\n\t * A track announces itself once, but its `attachments` are replaced on every sample, so a key that\n\t * is missing from the first sample can appear on the second — and a strategy backed by an\n\t * application's own mapping (a server-side `ssrc -> producerId` table, say) is inherently racy\n\t * against sample arrival. Resolving only at `*-track-added` meant losing those tracks for their\n\t * entire life, silently: an unresolvable outbound track never even reaches\n\t * `unconsumedOutboundTracks`, so it is invisible to `UnconsumedTrackDetector` too.\n\t *\n\t * So they wait here and are retried on their own `*-track-updated`, i.e. exactly when new stats\n\t * arrived for them. A track leaves on its first successful resolution or on removal, which makes\n\t * a linked track cost one `Set.has` per update and bounds these sets by the unresolved tracks\n\t * alive right now.\n\t */\n\tprivate readonly _pendingInboundTracks = new Set<ObservedInboundTrack>();\n\tprivate readonly _pendingOutboundTracks = new Set<ObservedOutboundTrack>();\n\n\tpublic constructor(\n\t\tpublic readonly observedCall: ObservedCall,\n\t\tprivate readonly resolvers: RemoteTrackResolvers,\n\t) {\n\t\tconst observer = observedCall.observer;\n\n\t\t// Subscribe to the central Observer bus, filtering to this call's tracks.\n\t\tconst onInboundAdded = (p: ObserverEvents['inbound-track-added'][0]) => {\n\t\t\tif (p.observedCall === this.observedCall) this._addInboundTrack(p.observedInboundTrack);\n\t\t};\n\t\tconst onInboundRemoved = (p: ObserverEvents['inbound-track-removed'][0]) => {\n\t\t\tif (p.observedCall === this.observedCall) this._removeInboundTrack(p.observedInboundTrack);\n\t\t};\n\t\tconst onOutboundAdded = (p: ObserverEvents['outbound-track-added'][0]) => {\n\t\t\tif (p.observedCall === this.observedCall) this._addOutboundTrack(p.observedOutboundTrack);\n\t\t};\n\t\tconst onOutboundRemoved = (p: ObserverEvents['outbound-track-removed'][0]) => {\n\t\t\tif (p.observedCall === this.observedCall) this._removeOutboundTrack(p.observedOutboundTrack);\n\t\t};\n\n\t\t// Retry, and only for what is actually waiting — see `_pendingInboundTracks`.\n\t\tconst onInboundUpdated = (p: ObserverEvents['inbound-track-updated'][0]) => {\n\t\t\tif (p.observedCall !== this.observedCall) return;\n\t\t\tif (this._pendingInboundTracks.has(p.observedInboundTrack)) this._addInboundTrack(p.observedInboundTrack);\n\t\t};\n\t\tconst onOutboundUpdated = (p: ObserverEvents['outbound-track-updated'][0]) => {\n\t\t\tif (p.observedCall !== this.observedCall) return;\n\t\t\tif (this._pendingOutboundTracks.has(p.observedOutboundTrack)) this._addOutboundTrack(p.observedOutboundTrack);\n\t\t};\n\n\t\tobservedCall.once('close', () => {\n\t\t\tobserver.off('inbound-track-added', onInboundAdded);\n\t\t\tobserver.off('inbound-track-removed', onInboundRemoved);\n\t\t\tobserver.off('outbound-track-added', onOutboundAdded);\n\t\t\tobserver.off('outbound-track-removed', onOutboundRemoved);\n\t\t\tobserver.off('inbound-track-updated', onInboundUpdated);\n\t\t\tobserver.off('outbound-track-updated', onOutboundUpdated);\n\t\t\tthis._pendingInboundTracks.clear();\n\t\t\tthis._pendingOutboundTracks.clear();\n\t\t});\n\n\t\tobserver.on('inbound-track-added', onInboundAdded);\n\t\tobserver.on('inbound-track-removed', onInboundRemoved);\n\t\tobserver.on('outbound-track-added', onOutboundAdded);\n\t\tobserver.on('outbound-track-removed', onOutboundRemoved);\n\t\tobserver.on('inbound-track-updated', onInboundUpdated);\n\t\tobserver.on('outbound-track-updated', onOutboundUpdated);\n\t}\n\n\t/** Tracks still waiting for a resolvable publisher id. Diagnostics; normally both are empty. */\n\tpublic get pendingTrackCounts(): { inbound: number, outbound: number } {\n\t\treturn { inbound: this._pendingInboundTracks.size, outbound: this._pendingOutboundTracks.size };\n\t}\n\n\t/** The published (outbound) track for a publisher id, if any. */\n\tpublic getOutboundTrackByPublisherId(publisherId: string): ObservedOutboundTrack | undefined {\n\t\treturn this._publisherIdToOutboundTrack.get(publisherId);\n\t}\n\n\t/** The subscribed (inbound) track for a subscriber id, if the strategy resolves subscriber ids. */\n\tpublic getInboundTrackBySubscriberId(subscriberId: string): ObservedInboundTrack | undefined {\n\t\treturn this._subscriberIdToInboundTrack.get(subscriberId);\n\t}\n\n\t// RemoteTrackResolver interface — thin readers over the links maintained on the tracks.\n\tpublic resolveRemoteOutboundTrack(inboundTrack: ObservedInboundTrack): ObservedOutboundTrack | undefined {\n\t\treturn inboundTrack.remoteOutboundTrack;\n\t}\n\n\tpublic resolveRemoteInboundTracks(outboundTrack: ObservedOutboundTrack): ObservedInboundTrack[] | undefined {\n\t\treturn [ ...outboundTrack.remoteInboundTracks ];\n\t}\n\n\tprivate _addInboundTrack(inboundTrack: ObservedInboundTrack) {\n\t\tconst publisherId = this.resolvers.resolveInboundTrackPublisherId(inboundTrack);\n\n\t\tif (!publisherId) {\n\t\t\tthis._pendingInboundTracks.add(inboundTrack);\n\n\t\t\treturn;\n\t\t}\n\n\t\tthis._pendingInboundTracks.delete(inboundTrack);\n\n\t\tconst subscriberId = this.resolvers.resolveInboundTrackSubscriberId?.(inboundTrack);\n\n\t\tif (subscriberId) {\n\t\t\tthis._subscriberIdToInboundTrack.set(subscriberId, inboundTrack);\n\t\t}\n\n\t\tconst outboundTrack = this._publisherIdToOutboundTrack.get(publisherId);\n\n\t\tif (!outboundTrack) return;\n\n\t\toutboundTrack.remoteInboundTracks.add(inboundTrack);\n\t\tinboundTrack.remoteOutboundTrack = outboundTrack;\n\t\t// It now has a subscriber, so it is no longer a candidate for \"published to nobody\".\n\t\tthis.observedCall.unconsumedOutboundTracks.delete(outboundTrack);\n\t}\n\n\tprivate _removeInboundTrack(inboundTrack: ObservedInboundTrack) {\n\t\tthis._pendingInboundTracks.delete(inboundTrack);\n\n\t\t// Use the stored link to unlink; no need to re-resolve the publisher id here.\n\t\tconst outboundTrack = inboundTrack.remoteOutboundTrack;\n\n\t\toutboundTrack?.remoteInboundTracks.delete(inboundTrack);\n\t\tinboundTrack.remoteOutboundTrack = undefined;\n\n\t\t// That may have been its last subscriber — one of the two moments the answer can change.\n\t\tif (outboundTrack && outboundTrack.remoteInboundTracks.size === 0) {\n\t\t\tthis.observedCall.unconsumedOutboundTracks.add(outboundTrack);\n\t\t}\n\n\t\tconst subscriberId = this.resolvers.resolveInboundTrackSubscriberId?.(inboundTrack);\n\n\t\tif (subscriberId) {\n\t\t\tthis._subscriberIdToInboundTrack.delete(subscriberId);\n\t\t}\n\t}\n\n\tprivate _addOutboundTrack(outboundTrack: ObservedOutboundTrack) {\n\t\tconst publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);\n\n\t\tif (!publisherId) {\n\t\t\tthis._pendingOutboundTracks.add(outboundTrack);\n\n\t\t\treturn;\n\t\t}\n\n\t\tthis._pendingOutboundTracks.delete(outboundTrack);\n\n\t\tthis._publisherIdToOutboundTrack.set(publisherId, outboundTrack);\n\t\t// Unconsumed until a subscriber links below — a new publisher legitimately starts this way.\n\t\tthis.observedCall.unconsumedOutboundTracks.add(outboundTrack);\n\n\t\t// Link any already-present subscribers of this publisher.\n\t\tfor (const observedClient of this.observedCall.observedClients.values()) {\n\t\t\tfor (const observedPeerConnection of observedClient.observedPeerConnections.values()) {\n\t\t\t\tfor (const inboundTrack of observedPeerConnection.observedInboundTracks.values()) {\n\t\t\t\t\tif (this.resolvers.resolveInboundTrackPublisherId(inboundTrack) !== publisherId) continue;\n\n\t\t\t\t\toutboundTrack.remoteInboundTracks.add(inboundTrack);\n\t\t\t\t\tinboundTrack.remoteOutboundTrack = outboundTrack;\n\t\t\t\t\tthis.observedCall.unconsumedOutboundTracks.delete(outboundTrack);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate _removeOutboundTrack(outboundTrack: ObservedOutboundTrack) {\n\t\tthis._pendingOutboundTracks.delete(outboundTrack);\n\n\t\tconst publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);\n\n\t\t// Only clear the map entry if it still points at this track (guards republish races).\n\t\tif (publisherId && this._publisherIdToOutboundTrack.get(publisherId) === outboundTrack) {\n\t\t\tthis._publisherIdToOutboundTrack.delete(publisherId);\n\t\t}\n\n\t\tfor (const inboundTrack of outboundTrack.remoteInboundTracks) {\n\t\t\tinboundTrack.remoteOutboundTrack = undefined;\n\t\t}\n\t\toutboundTrack.remoteInboundTracks.clear();\n\t\tthis.observedCall.unconsumedOutboundTracks.delete(outboundTrack);\n\t}\n}\n","import { ObservedCall } from '../ObservedCall';\nimport { RemoteTrackResolver, RemoteTrackResolverFactory } from './RemoteTrackResolver';\n\nexport function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory {\n\treturn (observedCall: ObservedCall) => new RemoteTrackResolver(observedCall, {\n\t\tresolveInboundTrackPublisherId: (inboundTrack) => inboundTrack.attachments?.producerId as string | undefined,\n\t\tresolveInboundTrackSubscriberId: (inboundTrack) => inboundTrack.attachments?.consumerId as string | undefined,\n\t\tresolveOutboundTrackPublisherId: (outboundTrack) => outboundTrack.attachments?.producerId as string | undefined,\n\t});\n}\n\nexport function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory {\n\treturn (observedCall: ObservedCall) => new RemoteTrackResolver(observedCall, {\n\t\tresolveInboundTrackPublisherId: (inboundTrack) => {\n\t\t\tconst ssrc = inboundTrack.getInboundRtp()?.ssrc;\n\n\t\t\treturn ssrc ? `${ssrc}` : undefined;\n\t\t},\n\t\tresolveInboundTrackSubscriberId: (inboundTrack) => {\n\t\t\tconst ssrc = inboundTrack.getInboundRtp()?.ssrc;\n\n\t\t\treturn ssrc ? `${ssrc}` : undefined;\n\t\t},\n\t\tresolveOutboundTrackPublisherId: (outboundTrack) => {\n\t\t\t// In p2p the sender's SSRC is preserved end-to-end, so it matches the receiver's\n\t\t\t// inbound SSRC. (Single-encoding case; simulcast would need per-encoding keys.)\n\t\t\tconst rtps = outboundTrack.getOutboundRtps();\n\n\t\t\tif (!rtps || rtps.length === 0) return undefined;\n\n\t\t\treturn rtps.map((rtp) => rtp.ssrc).join(',');\n\t\t}\n\t});\n}\n"],"mappings":"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","names":["module","logger","import_events","import_events","ascending","ClientEventTypes","ClientMetaTypes","logger","logger","logger","import_events","import_events","logger","import_events","logger","logger","logger","logger","median","logger","import_events"]}