From 9e589e2d7696d158bbd6777855077aba20306fcd Mon Sep 17 00:00:00 2001 From: balazskreith Date: Tue, 3 Jun 2025 15:36:45 +0300 Subject: [PATCH 1/9] bump version to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4565bab..b28289c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@observertc/observer-js", - "version": "1.0.0-beta.1", + "version": "1.0.0", "description": "Server Side NodeJS Library for processing ObserveRTC Samples", "main": "lib/index.js", "types": "lib/index.d.ts", From 407c18cf158afb020cff6fa5b26a5e1e5f3f477b Mon Sep 17 00:00:00 2001 From: balazskreith Date: Tue, 3 Jun 2025 20:36:10 +0300 Subject: [PATCH 2/9] fix: correct argument handling in onClientUpdated callback --- src/ObservedCallEventMonitor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ObservedCallEventMonitor.ts b/src/ObservedCallEventMonitor.ts index 6d390d5..92ba257 100644 --- a/src/ObservedCallEventMonitor.ts +++ b/src/ObservedCallEventMonitor.ts @@ -152,7 +152,7 @@ export class ObservedCallEventMonitor { const onClientExtensionStats = (extensionStats: ExtensionStat) => this._onClientExtensionStats(observedClient, extensionStats); const onUsingTurn = (usingTurn: boolean) => this._onUsingTurn(observedClient, usingTurn); const onUserMediaError = (error: string) => this._onUserMediaError(observedClient, error); - const onClientUpdated = (...args: ObservedClientEvents['update']) => this.onClientUpdated?.(observedClient, args[0].sample, this.context); + const onClientUpdated = (...args: ObservedClientEvents['update']) => this.onClientUpdated?.(observedClient, args[0], this.context); const onClientEvent = (event: ClientEvent) => this.onClientEvent?.(observedClient, event, this.context); observedClient.once('close', () => { From b1e35364164ccbde1926e68dc904758e324e9ebf Mon Sep 17 00:00:00 2001 From: balazskreith Date: Sun, 22 Jun 2025 09:09:53 +0300 Subject: [PATCH 3/9] feat: enhance ObservedTURN with unlimited event listeners and clean up Observer class by removing unused timer --- src/ObservedTURN.ts | 1 + src/Observer.ts | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ObservedTURN.ts b/src/ObservedTURN.ts index 7ded965..f2c288d 100644 --- a/src/ObservedTURN.ts +++ b/src/ObservedTURN.ts @@ -34,6 +34,7 @@ export class ObservedTURN extends EventEmitter { public constructor( ) { super(); + this.setMaxListeners(Infinity); } public update() { diff --git a/src/Observer.ts b/src/Observer.ts index 7d80049..3b682d4 100644 --- a/src/Observer.ts +++ b/src/Observer.ts @@ -68,8 +68,6 @@ export class Observer = Record; - public constructor(public readonly config: ObserverConfig = { updatePolicy: 'update-when-all-call-updated', updateIntervalInMs: undefined, @@ -154,8 +152,6 @@ export class Observer = Record call.close()); From 3147158f27069941de43b4f2aec174c3ef0949ce Mon Sep 17 00:00:00 2001 From: Balazs Kreith Date: Fri, 11 Jul 2025 05:50:57 +0300 Subject: [PATCH 4/9] feat(webrtc): add ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, and ObservedRemoteOutboundRtp classes - Implemented ObservedPeerConnectionTransport to manage transport stats for peer connections. - Created ObservedRemoteInboundRtp to handle inbound RTP statistics, including packet loss and jitter. - Developed ObservedRemoteOutboundRtp for outbound RTP statistics, tracking sent packets and bytes. - Each class includes methods for updating stats and retrieving associated peer connection and codec information. --- src/ObservedCall.ts | 39 ++++++++++++------- src/ObservedCallEventMonitor.ts | 26 ++++++------- src/ObservedClient.ts | 37 ++++++++++++++---- src/ObservedClientEventMonitor.ts | 26 ++++++------- src/ObservedTURN.ts | 2 +- src/ObservedTurnServer.ts | 4 +- src/ObserverEventMonitor.ts | 26 ++++++------- src/index.ts | 32 +++++++-------- src/mediasoup/ObservedMediaRouter.ts | 10 ----- src/mediasoup/ObservedMediasoupRouter.ts | 12 ++++++ src/utils/MediasoupRemoteTrackResolver.ts | 4 +- src/utils/RemoteTrackResolver.ts | 4 +- src/{ => webrtc}/ObservedCertificate.ts | 2 +- src/{ => webrtc}/ObservedCodec.ts | 2 +- src/{ => webrtc}/ObservedDataChannel.ts | 2 +- src/{ => webrtc}/ObservedIceCandidate.ts | 2 +- src/{ => webrtc}/ObservedIceCandidatePair.ts | 2 +- src/{ => webrtc}/ObservedIceTransport.ts | 2 +- src/{ => webrtc}/ObservedInboundRtp.ts | 4 +- src/{ => webrtc}/ObservedInboundTrack.ts | 8 ++-- src/{ => webrtc}/ObservedMediaPlayout.ts | 4 +- src/{ => webrtc}/ObservedMediaSource.ts | 4 +- src/{ => webrtc}/ObservedOutboundRtp.ts | 4 +- src/{ => webrtc}/ObservedOutboundTrack.ts | 8 ++-- src/{ => webrtc}/ObservedPeerConnection.ts | 30 ++++++++++---- .../ObservedPeerConnectionTransport.ts | 2 +- src/{ => webrtc}/ObservedRemoteInboundRtp.ts | 4 +- src/{ => webrtc}/ObservedRemoteOutboundRtp.ts | 4 +- 28 files changed, 179 insertions(+), 127 deletions(-) delete mode 100644 src/mediasoup/ObservedMediaRouter.ts create mode 100644 src/mediasoup/ObservedMediasoupRouter.ts rename src/{ => webrtc}/ObservedCertificate.ts (94%) rename src/{ => webrtc}/ObservedCodec.ts (95%) rename src/{ => webrtc}/ObservedDataChannel.ts (97%) rename src/{ => webrtc}/ObservedIceCandidate.ts (96%) rename src/{ => webrtc}/ObservedIceCandidatePair.ts (98%) rename src/{ => webrtc}/ObservedIceTransport.ts (98%) rename src/{ => webrtc}/ObservedInboundRtp.ts (98%) rename src/{ => webrtc}/ObservedInboundTrack.ts (88%) rename src/{ => webrtc}/ObservedMediaPlayout.ts (92%) rename src/{ => webrtc}/ObservedMediaSource.ts (94%) rename src/{ => webrtc}/ObservedOutboundRtp.ts (97%) rename src/{ => webrtc}/ObservedOutboundTrack.ts (88%) rename src/{ => webrtc}/ObservedPeerConnection.ts (98%) rename src/{ => webrtc}/ObservedPeerConnectionTransport.ts (92%) rename src/{ => webrtc}/ObservedRemoteInboundRtp.ts (94%) rename src/{ => webrtc}/ObservedRemoteOutboundRtp.ts (94%) diff --git a/src/ObservedCall.ts b/src/ObservedCall.ts index fa68b0e..8642cb5 100644 --- a/src/ObservedCall.ts +++ b/src/ObservedCall.ts @@ -11,6 +11,9 @@ import { Updater } from './updaters/Updater'; import { OnIntervalUpdater } from './updaters/OnIntervalUpdater'; import { OnAnyClientCallUpdater } from './updaters/OnAnyClientCallUpdater'; import { ObservedCallEventMonitor } from './ObservedCallEventMonitor'; +import { createLogger } from './common/logger'; + +const logger = createLogger('ObservedCall'); export type ObservedCallSettings = Record> = { callId: string; @@ -18,6 +21,7 @@ export type ObservedCallSettings = Recor remoteTrackResolvePolicy?: 'p2p' | 'mediasoup-sfu', updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated' | 'update-on-interval', updateIntervalInMs?: number, + maxIdleIfEmptyMs?: number, }; export type ObservedCallEvents = { @@ -86,13 +90,8 @@ export class ObservedCall = Record, - }; - private _callEndedEvent: { - emitted: boolean - }; + public maxIdleIfEmptyMs?: number; + private _autoCloseTimer?: ReturnType; public constructor( settings: ObservedCallSettings, @@ -104,6 +103,7 @@ export class ObservedCall = Record = Record = Record = Record = Record { + if (this.closed) return; + if (this.observedClients.size > 0) return; + + logger.debug(`Call ${this.callId} is empty for ${this.maxIdleIfEmptyMs}ms, closing...`); + this.close(); + }, this.maxIdleIfEmptyMs); + } + // public resetSummaryMetrics() { // this.totalAddedClients = 0; // this.totalRemovedClients = 0; diff --git a/src/ObservedCallEventMonitor.ts b/src/ObservedCallEventMonitor.ts index 92ba257..1a5b5ca 100644 --- a/src/ObservedCallEventMonitor.ts +++ b/src/ObservedCallEventMonitor.ts @@ -1,18 +1,18 @@ import { ObservedCall } from './ObservedCall'; -import { ObservedCertificate } from './ObservedCertificate'; +import { ObservedCertificate } from './webrtc/ObservedCertificate'; import { ObservedClient, ObservedClientEvents } from './ObservedClient'; -import { ObservedCodec } from './ObservedCodec'; -import { ObservedDataChannel } from './ObservedDataChannel'; -import { ObservedIceCandidate } from './ObservedIceCandidate'; -import { ObservedIceCandidatePair } from './ObservedIceCandidatePair'; -import { ObservedIceTransport } from './ObservedIceTransport'; -import { ObservedInboundRtp } from './ObservedInboundRtp'; -import { ObservedInboundTrack } from './ObservedInboundTrack'; -import { ObservedMediaPlayout } from './ObservedMediaPlayout'; -import { ObservedMediaSource } from './ObservedMediaSource'; -import { ObservedOutboundRtp } from './ObservedOutboundRtp'; -import { ObservedOutboundTrack } from './ObservedOutboundTrack'; -import { ObservedPeerConnection } from './ObservedPeerConnection'; +import { ObservedCodec } from './webrtc/ObservedCodec'; +import { ObservedDataChannel } from './webrtc/ObservedDataChannel'; +import { ObservedIceCandidate } from './webrtc/ObservedIceCandidate'; +import { ObservedIceCandidatePair } from './webrtc/ObservedIceCandidatePair'; +import { ObservedIceTransport } from './webrtc/ObservedIceTransport'; +import { ObservedInboundRtp } from './webrtc/ObservedInboundRtp'; +import { ObservedInboundTrack } from './webrtc/ObservedInboundTrack'; +import { ObservedMediaPlayout } from './webrtc/ObservedMediaPlayout'; +import { ObservedMediaSource } from './webrtc/ObservedMediaSource'; +import { ObservedOutboundRtp } from './webrtc/ObservedOutboundRtp'; +import { ObservedOutboundTrack } from './webrtc/ObservedOutboundTrack'; +import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; import { ClientEvent, ClientIssue, ClientMetaData, ClientSample, ExtensionStat } from './schema/ClientSample'; export class ObservedCallEventMonitor { diff --git a/src/ObservedClient.ts b/src/ObservedClient.ts index 1426cd9..ba7b378 100644 --- a/src/ObservedClient.ts +++ b/src/ObservedClient.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { ObservedPeerConnection } from './ObservedPeerConnection'; +import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; import { createLogger } from './common/logger'; // eslint-disable-next-line camelcase import { ClientEvent, ClientMetaData, ClientSample, PeerConnectionSample, ClientIssue, ExtensionStat } from './schema/ClientSample'; @@ -15,6 +15,7 @@ const logger = createLogger('ObservedClient'); export type ObservedClientSettings = Record> = { clientId: string; + maxIdleTimeMs?: number; appData?: AppData; }; @@ -44,14 +45,12 @@ export declare interface ObservedClient { export class ObservedClient = Record> extends EventEmitter { public readonly detectors: Detectors; - public readonly clientId: string; public readonly observedPeerConnections = new Map(); public readonly calculatedScore: CalculatedScore = { weight: 1, value: undefined, }; - public appData: AppData; public attachments?: Record; public updated = Date.now(); @@ -135,16 +134,26 @@ export class ObservedClient = Record = {}; + private _autoCloseTimer?: ReturnType; - public constructor(settings: ObservedClientSettings, public readonly call: ObservedCall) { + public constructor( + public readonly settings: ObservedClientSettings, + public readonly call: ObservedCall + ) { super(); this.setMaxListeners(Infinity); + this._createAutoClose(); - this.clientId = settings.clientId; - this.appData = settings.appData ?? {} as AppData; - this.detectors = new Detectors(); } + + public get clientId() { + return this.settings.clientId; + } + + public get appData(): AppData { + return this.settings.appData ?? {} as AppData; + } public get numberOfPeerConnections() { return this.observedPeerConnections.size; @@ -176,8 +185,22 @@ export class ObservedClient = Record { + if (this.closed) return; + + logger.debug(`Client ${this.clientId} is idle for ${this.settings.maxIdleTimeMs}ms, closing...`); + this.close(); + }, this.settings.maxIdleTimeMs); + } + public accept(sample: ClientSample): void { if (this.closed) throw new Error(`Client ${this.clientId} is closed`); + + this._createAutoClose(); const now = Date.now(); const elapsedInMs = now - this.updated; diff --git a/src/ObservedClientEventMonitor.ts b/src/ObservedClientEventMonitor.ts index 7199779..2488c25 100644 --- a/src/ObservedClientEventMonitor.ts +++ b/src/ObservedClientEventMonitor.ts @@ -1,17 +1,17 @@ -import { ObservedCertificate } from './ObservedCertificate'; +import { ObservedCertificate } from './webrtc/ObservedCertificate'; import { ObservedClient, ObservedClientEvents } from './ObservedClient'; -import { ObservedCodec } from './ObservedCodec'; -import { ObservedDataChannel } from './ObservedDataChannel'; -import { ObservedIceCandidate } from './ObservedIceCandidate'; -import { ObservedIceCandidatePair } from './ObservedIceCandidatePair'; -import { ObservedIceTransport } from './ObservedIceTransport'; -import { ObservedInboundRtp } from './ObservedInboundRtp'; -import { ObservedInboundTrack } from './ObservedInboundTrack'; -import { ObservedMediaPlayout } from './ObservedMediaPlayout'; -import { ObservedMediaSource } from './ObservedMediaSource'; -import { ObservedOutboundRtp } from './ObservedOutboundRtp'; -import { ObservedOutboundTrack } from './ObservedOutboundTrack'; -import { ObservedPeerConnection } from './ObservedPeerConnection'; +import { ObservedCodec } from './webrtc/ObservedCodec'; +import { ObservedDataChannel } from './webrtc/ObservedDataChannel'; +import { ObservedIceCandidate } from './webrtc/ObservedIceCandidate'; +import { ObservedIceCandidatePair } from './webrtc/ObservedIceCandidatePair'; +import { ObservedIceTransport } from './webrtc/ObservedIceTransport'; +import { ObservedInboundRtp } from './webrtc/ObservedInboundRtp'; +import { ObservedInboundTrack } from './webrtc/ObservedInboundTrack'; +import { ObservedMediaPlayout } from './webrtc/ObservedMediaPlayout'; +import { ObservedMediaSource } from './webrtc/ObservedMediaSource'; +import { ObservedOutboundRtp } from './webrtc/ObservedOutboundRtp'; +import { ObservedOutboundTrack } from './webrtc/ObservedOutboundTrack'; +import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; import { ClientIssue, ClientMetaData, ExtensionStat } from './schema/ClientSample'; export class ObservedClientEventMonitor = Record> { diff --git a/src/ObservedTURN.ts b/src/ObservedTURN.ts index f2c288d..7affa60 100644 --- a/src/ObservedTURN.ts +++ b/src/ObservedTURN.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'stream'; -import { ObservedPeerConnection } from './ObservedPeerConnection'; +import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; import { ObservedTurnServer } from './ObservedTurnServer'; import { createLogger } from './common/logger'; diff --git a/src/ObservedTurnServer.ts b/src/ObservedTurnServer.ts index e6cefe5..0127ab2 100644 --- a/src/ObservedTurnServer.ts +++ b/src/ObservedTurnServer.ts @@ -1,5 +1,5 @@ -import { ObservedIceCandidatePair } from './ObservedIceCandidatePair'; -import { ObservedPeerConnection } from './ObservedPeerConnection'; +import { ObservedIceCandidatePair } from './webrtc/ObservedIceCandidatePair'; +import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; import { ObservedTURN } from './ObservedTURN'; export class ObservedTurnServer { diff --git a/src/ObserverEventMonitor.ts b/src/ObserverEventMonitor.ts index b847903..b36a13f 100644 --- a/src/ObserverEventMonitor.ts +++ b/src/ObserverEventMonitor.ts @@ -1,18 +1,18 @@ import { ObservedCall } from './ObservedCall'; -import { ObservedCertificate } from './ObservedCertificate'; +import { ObservedCertificate } from './webrtc/ObservedCertificate'; import { ObservedClient, ObservedClientEvents } from './ObservedClient'; -import { ObservedCodec } from './ObservedCodec'; -import { ObservedDataChannel } from './ObservedDataChannel'; -import { ObservedIceCandidate } from './ObservedIceCandidate'; -import { ObservedIceCandidatePair } from './ObservedIceCandidatePair'; -import { ObservedIceTransport } from './ObservedIceTransport'; -import { ObservedInboundRtp } from './ObservedInboundRtp'; -import { ObservedInboundTrack } from './ObservedInboundTrack'; -import { ObservedMediaPlayout } from './ObservedMediaPlayout'; -import { ObservedMediaSource } from './ObservedMediaSource'; -import { ObservedOutboundRtp } from './ObservedOutboundRtp'; -import { ObservedOutboundTrack } from './ObservedOutboundTrack'; -import { ObservedPeerConnection } from './ObservedPeerConnection'; +import { ObservedCodec } from './webrtc/ObservedCodec'; +import { ObservedDataChannel } from './webrtc/ObservedDataChannel'; +import { ObservedIceCandidate } from './webrtc/ObservedIceCandidate'; +import { ObservedIceCandidatePair } from './webrtc/ObservedIceCandidatePair'; +import { ObservedIceTransport } from './webrtc/ObservedIceTransport'; +import { ObservedInboundRtp } from './webrtc/ObservedInboundRtp'; +import { ObservedInboundTrack } from './webrtc/ObservedInboundTrack'; +import { ObservedMediaPlayout } from './webrtc/ObservedMediaPlayout'; +import { ObservedMediaSource } from './webrtc/ObservedMediaSource'; +import { ObservedOutboundRtp } from './webrtc/ObservedOutboundRtp'; +import { ObservedOutboundTrack } from './webrtc/ObservedOutboundTrack'; +import { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; import { Observer } from './Observer'; import { ClientEvent, ClientIssue, ClientMetaData, ClientSample, ExtensionStat } from './schema/ClientSample'; diff --git a/src/index.ts b/src/index.ts index 2cb6a7a..0327f58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,23 +2,23 @@ export type { ObserverEvents } from './Observer'; export { Observer } from './Observer'; export { ObservedCall } from './ObservedCall'; -export { ObservedInboundTrack } from './ObservedInboundTrack'; -export { ObservedOutboundTrack } from './ObservedOutboundTrack'; +export { ObservedInboundTrack } from './webrtc/ObservedInboundTrack'; +export { ObservedOutboundTrack } from './webrtc/ObservedOutboundTrack'; export { ObservedClient } from './ObservedClient'; -export { ObservedPeerConnection } from './ObservedPeerConnection'; -export { ObservedMediaSource } from './ObservedMediaSource'; -export { ObservedMediaPlayout } from './ObservedMediaPlayout'; -export { ObservedCodec } from './ObservedCodec'; -export { ObservedCertificate } from './ObservedCertificate'; -export { ObservedDataChannel } from './ObservedDataChannel'; -export { ObservedInboundRtp } from './ObservedInboundRtp'; -export { ObservedOutboundRtp } from './ObservedOutboundRtp'; -export { ObservedRemoteInboundRtp } from './ObservedRemoteInboundRtp'; -export { ObservedRemoteOutboundRtp } from './ObservedRemoteOutboundRtp'; -export { ObservedIceCandidatePair } from './ObservedIceCandidatePair'; -export { ObservedIceCandidate } from './ObservedIceCandidate'; -export { ObservedIceTransport } from './ObservedIceTransport'; -export { ObservedPeerConnectionTransport } from './ObservedPeerConnectionTransport'; +export { ObservedPeerConnection } from './webrtc/ObservedPeerConnection'; +export { ObservedMediaSource } from './webrtc/ObservedMediaSource'; +export { ObservedMediaPlayout } from './webrtc/ObservedMediaPlayout'; +export { ObservedCodec } from './webrtc/ObservedCodec'; +export { ObservedCertificate } from './webrtc/ObservedCertificate'; +export { ObservedDataChannel } from './webrtc/ObservedDataChannel'; +export { ObservedInboundRtp } from './webrtc/ObservedInboundRtp'; +export { ObservedOutboundRtp } from './webrtc/ObservedOutboundRtp'; +export { ObservedRemoteInboundRtp } from './webrtc/ObservedRemoteInboundRtp'; +export { ObservedRemoteOutboundRtp } from './webrtc/ObservedRemoteOutboundRtp'; +export { ObservedIceCandidatePair } from './webrtc/ObservedIceCandidatePair'; +export { ObservedIceCandidate } from './webrtc/ObservedIceCandidate'; +export { ObservedIceTransport } from './webrtc/ObservedIceTransport'; +export { ObservedPeerConnectionTransport } from './webrtc/ObservedPeerConnectionTransport'; export { ClientEventTypes } from './schema/ClientEventTypes'; export { ClientMetaTypes } from './schema/ClientMetaTypes'; export { ClientSample, ClientIssue, ClientEvent, ClientMetaData } from './schema/ClientSample'; diff --git a/src/mediasoup/ObservedMediaRouter.ts b/src/mediasoup/ObservedMediaRouter.ts deleted file mode 100644 index 4b3a273..0000000 --- a/src/mediasoup/ObservedMediaRouter.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Item = { - callId: string; - appData?: Record; - region: string; - publishers: string[]; - subscribers: string[]; -} -export class ObservedMediaRouter { - -} \ No newline at end of file diff --git a/src/mediasoup/ObservedMediasoupRouter.ts b/src/mediasoup/ObservedMediasoupRouter.ts new file mode 100644 index 0000000..be15767 --- /dev/null +++ b/src/mediasoup/ObservedMediasoupRouter.ts @@ -0,0 +1,12 @@ +import { Observer } from "../Observer"; + +export class ObservedMediasoupRouter = Record> { + public readonly appData: T; + public constructor( + public readonly parent: Observer, + appData?: T + ) { + this.appData = appData ?? {} as T; + } +} + diff --git a/src/utils/MediasoupRemoteTrackResolver.ts b/src/utils/MediasoupRemoteTrackResolver.ts index 1ae272b..afcc96d 100644 --- a/src/utils/MediasoupRemoteTrackResolver.ts +++ b/src/utils/MediasoupRemoteTrackResolver.ts @@ -1,7 +1,7 @@ import { ObservedCall } from '../ObservedCall'; import { ObservedCallEventMonitor } from '../ObservedCallEventMonitor'; -import { ObservedInboundTrack } from '../ObservedInboundTrack'; -import { ObservedOutboundTrack } from '../ObservedOutboundTrack'; +import { ObservedInboundTrack } from '../webrtc/ObservedInboundTrack'; +import { ObservedOutboundTrack } from '../webrtc/ObservedOutboundTrack'; import { RemoteTrackResolver } from './RemoteTrackResolver'; export class MediasoupRemoteTrackResolver implements RemoteTrackResolver { diff --git a/src/utils/RemoteTrackResolver.ts b/src/utils/RemoteTrackResolver.ts index 1383dcc..f90b423 100644 --- a/src/utils/RemoteTrackResolver.ts +++ b/src/utils/RemoteTrackResolver.ts @@ -1,5 +1,5 @@ -import { ObservedInboundTrack } from '../ObservedInboundTrack'; -import { ObservedOutboundTrack } from '../ObservedOutboundTrack'; +import { ObservedInboundTrack } from '../webrtc/ObservedInboundTrack'; +import { ObservedOutboundTrack } from '../webrtc/ObservedOutboundTrack'; export interface RemoteTrackResolver { resolveRemoteOutboundTrack( diff --git a/src/ObservedCertificate.ts b/src/webrtc/ObservedCertificate.ts similarity index 94% rename from src/ObservedCertificate.ts rename to src/webrtc/ObservedCertificate.ts index d2aa0a8..a7b0cff 100644 --- a/src/ObservedCertificate.ts +++ b/src/webrtc/ObservedCertificate.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { CertificateStats } from './schema/ClientSample'; +import { CertificateStats } from '../schema/ClientSample'; export class ObservedCertificate implements CertificateStats { public appData?: Record; diff --git a/src/ObservedCodec.ts b/src/webrtc/ObservedCodec.ts similarity index 95% rename from src/ObservedCodec.ts rename to src/webrtc/ObservedCodec.ts index 9b96997..1b86fdc 100644 --- a/src/ObservedCodec.ts +++ b/src/webrtc/ObservedCodec.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { CodecStats } from './schema/ClientSample'; +import { CodecStats } from '../schema/ClientSample'; export class ObservedCodec implements CodecStats { private _visited = false; diff --git a/src/ObservedDataChannel.ts b/src/webrtc/ObservedDataChannel.ts similarity index 97% rename from src/ObservedDataChannel.ts rename to src/webrtc/ObservedDataChannel.ts index 0442688..7e15108 100644 --- a/src/ObservedDataChannel.ts +++ b/src/webrtc/ObservedDataChannel.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { DataChannelStats } from './schema/ClientSample'; +import { DataChannelStats } from '../schema/ClientSample'; export type ObservedDataChannelState = 'connecting' | 'open' | 'closing' | 'closed'; diff --git a/src/ObservedIceCandidate.ts b/src/webrtc/ObservedIceCandidate.ts similarity index 96% rename from src/ObservedIceCandidate.ts rename to src/webrtc/ObservedIceCandidate.ts index 4639ab3..5ca9a24 100644 --- a/src/ObservedIceCandidate.ts +++ b/src/webrtc/ObservedIceCandidate.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { IceCandidateStats } from './schema/ClientSample'; +import { IceCandidateStats } from '../schema/ClientSample'; export class ObservedIceCandidate implements IceCandidateStats { private _visited = false; diff --git a/src/ObservedIceCandidatePair.ts b/src/webrtc/ObservedIceCandidatePair.ts similarity index 98% rename from src/ObservedIceCandidatePair.ts rename to src/webrtc/ObservedIceCandidatePair.ts index 8ea3ea6..d3ec1d2 100644 --- a/src/ObservedIceCandidatePair.ts +++ b/src/webrtc/ObservedIceCandidatePair.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { IceCandidatePairStats } from './schema/ClientSample'; +import { IceCandidatePairStats } from '../schema/ClientSample'; export class ObservedIceCandidatePair implements IceCandidatePairStats { private _visited = false; diff --git a/src/ObservedIceTransport.ts b/src/webrtc/ObservedIceTransport.ts similarity index 98% rename from src/ObservedIceTransport.ts rename to src/webrtc/ObservedIceTransport.ts index 0f0c687..d5aa5d7 100644 --- a/src/ObservedIceTransport.ts +++ b/src/webrtc/ObservedIceTransport.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { IceTransportStats } from './schema/ClientSample'; +import { IceTransportStats } from '../schema/ClientSample'; export class ObservedIceTransport implements IceTransportStats { private _visited = false; diff --git a/src/ObservedInboundRtp.ts b/src/webrtc/ObservedInboundRtp.ts similarity index 98% rename from src/ObservedInboundRtp.ts rename to src/webrtc/ObservedInboundRtp.ts index 7239c80..6ec1130 100644 --- a/src/ObservedInboundRtp.ts +++ b/src/webrtc/ObservedInboundRtp.ts @@ -1,6 +1,6 @@ -import { MediaKind } from './common/types'; +import { MediaKind } from '../common/types'; import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { InboundRtpStats } from './schema/ClientSample'; +import { InboundRtpStats } from '../schema/ClientSample'; export class ObservedInboundRtp implements InboundRtpStats { public appData?: Record; diff --git a/src/ObservedInboundTrack.ts b/src/webrtc/ObservedInboundTrack.ts similarity index 88% rename from src/ObservedInboundTrack.ts rename to src/webrtc/ObservedInboundTrack.ts index 2f883b9..0f0d41e 100644 --- a/src/ObservedInboundTrack.ts +++ b/src/webrtc/ObservedInboundTrack.ts @@ -1,7 +1,7 @@ -import { CalculatedScore } from './scores/CalculatedScore'; -import { MediaKind } from './common/types'; -import { InboundTrackSample } from './schema/ClientSample'; -import { Detectors } from './detectors/Detectors'; +import { CalculatedScore } from '../scores/CalculatedScore'; +import { MediaKind } from '../common/types'; +import { InboundTrackSample } from '../schema/ClientSample'; +import { Detectors } from '../detectors/Detectors'; import { ObservedPeerConnection } from './ObservedPeerConnection'; import { ObservedInboundRtp } from './ObservedInboundRtp'; import { ObservedMediaPlayout } from './ObservedMediaPlayout'; diff --git a/src/ObservedMediaPlayout.ts b/src/webrtc/ObservedMediaPlayout.ts similarity index 92% rename from src/ObservedMediaPlayout.ts rename to src/webrtc/ObservedMediaPlayout.ts index 88cd7da..9443cc5 100644 --- a/src/ObservedMediaPlayout.ts +++ b/src/webrtc/ObservedMediaPlayout.ts @@ -1,6 +1,6 @@ -import { MediaKind } from './common/types'; +import { MediaKind } from '../common/types'; import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { MediaPlayoutStats } from './schema/ClientSample'; +import { MediaPlayoutStats } from '../schema/ClientSample'; export class ObservedMediaPlayout implements MediaPlayoutStats { private _visited = false; diff --git a/src/ObservedMediaSource.ts b/src/webrtc/ObservedMediaSource.ts similarity index 94% rename from src/ObservedMediaSource.ts rename to src/webrtc/ObservedMediaSource.ts index 59c861f..8056799 100644 --- a/src/ObservedMediaSource.ts +++ b/src/webrtc/ObservedMediaSource.ts @@ -1,6 +1,6 @@ -import { MediaKind } from './common/types'; +import { MediaKind } from '../common/types'; import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { MediaSourceStats } from './schema/ClientSample'; +import { MediaSourceStats } from '../schema/ClientSample'; export class ObservedMediaSource implements MediaSourceStats { private _visited = false; diff --git a/src/ObservedOutboundRtp.ts b/src/webrtc/ObservedOutboundRtp.ts similarity index 97% rename from src/ObservedOutboundRtp.ts rename to src/webrtc/ObservedOutboundRtp.ts index ac72bec..59d8a5c 100644 --- a/src/ObservedOutboundRtp.ts +++ b/src/webrtc/ObservedOutboundRtp.ts @@ -1,6 +1,6 @@ -import { MediaKind } from './common/types'; +import { MediaKind } from '../common/types'; import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { OutboundRtpStats, QualityLimitationDurations } from './schema/ClientSample'; +import { OutboundRtpStats, QualityLimitationDurations } from '../schema/ClientSample'; export class ObservedOutboundRtp implements OutboundRtpStats { private _visited = false; diff --git a/src/ObservedOutboundTrack.ts b/src/webrtc/ObservedOutboundTrack.ts similarity index 88% rename from src/ObservedOutboundTrack.ts rename to src/webrtc/ObservedOutboundTrack.ts index a841345..63a34e7 100644 --- a/src/ObservedOutboundTrack.ts +++ b/src/webrtc/ObservedOutboundTrack.ts @@ -1,7 +1,7 @@ -import { CalculatedScore } from './scores/CalculatedScore'; -import { MediaKind } from './common/types'; -import { OutboundTrackSample } from './schema/ClientSample'; -import { Detectors } from './detectors/Detectors'; +import { CalculatedScore } from '../scores/CalculatedScore'; +import { MediaKind } from '../common/types'; +import { OutboundTrackSample } from '../schema/ClientSample'; +import { Detectors } from '../detectors/Detectors'; import { ObservedPeerConnection } from './ObservedPeerConnection'; import { ObservedOutboundRtp } from './ObservedOutboundRtp'; import { ObservedMediaSource } from './ObservedMediaSource'; diff --git a/src/ObservedPeerConnection.ts b/src/webrtc/ObservedPeerConnection.ts similarity index 98% rename from src/ObservedPeerConnection.ts rename to src/webrtc/ObservedPeerConnection.ts index 25cb2bc..850d80b 100644 --- a/src/ObservedPeerConnection.ts +++ b/src/webrtc/ObservedPeerConnection.ts @@ -1,9 +1,8 @@ import { EventEmitter } from 'events'; -import { ObservedClient } from './ObservedClient'; -import { CertificateStats, CodecStats, DataChannelStats, IceCandidateStats, InboundRtpStats, InboundTrackSample, MediaPlayoutStats, MediaSourceStats, OutboundRtpStats, OutboundTrackSample, PeerConnectionSample, PeerConnectionTransportStats, RemoteInboundRtpStats, RemoteOutboundRtpStats } from './schema/ClientSample'; +import { ObservedClient } from '../ObservedClient'; import { ObservedInboundRtp } from './ObservedInboundRtp'; -import { createLogger } from './common/logger'; -import { MediaKind } from './common/types'; +import { createLogger } from '../common/logger'; +import { MediaKind } from '../common/types'; import { ObservedOutboundRtp } from './ObservedOutboundRtp'; import { ObservedCertificate } from './ObservedCertificate'; import { ObservedCodec } from './ObservedCodec'; @@ -18,9 +17,26 @@ import { ObservedRemoteInboundRtp } from './ObservedRemoteInboundRtp'; import { ObservedRemoteOutboundRtp } from './ObservedRemoteOutboundRtp'; import { ObservedInboundTrack } from './ObservedInboundTrack'; import { ObservedOutboundTrack } from './ObservedOutboundTrack'; -import { CalculatedScore } from './scores/CalculatedScore'; -import { ObservedTurnServer } from './ObservedTurnServer'; -import { getMedian } from './common/utils'; +import { CalculatedScore } from '../scores/CalculatedScore'; +import { ObservedTurnServer } from '../ObservedTurnServer'; +import { getMedian } from '../common/utils'; +import { + CertificateStats, + CodecStats, + DataChannelStats, + IceCandidateStats, + InboundRtpStats, + InboundTrackSample, + MediaPlayoutStats, + MediaSourceStats, + OutboundRtpStats, + OutboundTrackSample, + PeerConnectionSample, + PeerConnectionTransportStats, + RemoteInboundRtpStats, + RemoteOutboundRtpStats +} from '../schema/ClientSample'; + const logger = createLogger('ObservedPeerConnection'); diff --git a/src/ObservedPeerConnectionTransport.ts b/src/webrtc/ObservedPeerConnectionTransport.ts similarity index 92% rename from src/ObservedPeerConnectionTransport.ts rename to src/webrtc/ObservedPeerConnectionTransport.ts index 8d8d0bf..f458b47 100644 --- a/src/ObservedPeerConnectionTransport.ts +++ b/src/webrtc/ObservedPeerConnectionTransport.ts @@ -1,5 +1,5 @@ import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { PeerConnectionTransportStats } from './schema/ClientSample'; +import { PeerConnectionTransportStats } from '../schema/ClientSample'; export class ObservedPeerConnectionTransport implements PeerConnectionTransportStats { private _visited = false; diff --git a/src/ObservedRemoteInboundRtp.ts b/src/webrtc/ObservedRemoteInboundRtp.ts similarity index 94% rename from src/ObservedRemoteInboundRtp.ts rename to src/webrtc/ObservedRemoteInboundRtp.ts index f0237ec..4857c10 100644 --- a/src/ObservedRemoteInboundRtp.ts +++ b/src/webrtc/ObservedRemoteInboundRtp.ts @@ -1,6 +1,6 @@ -import { MediaKind } from './common/types'; +import { MediaKind } from '../common/types'; import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { RemoteInboundRtpStats } from './schema/ClientSample'; +import { RemoteInboundRtpStats } from '../schema/ClientSample'; export class ObservedRemoteInboundRtp implements RemoteInboundRtpStats { private _visited = false; diff --git a/src/ObservedRemoteOutboundRtp.ts b/src/webrtc/ObservedRemoteOutboundRtp.ts similarity index 94% rename from src/ObservedRemoteOutboundRtp.ts rename to src/webrtc/ObservedRemoteOutboundRtp.ts index 204aa79..92b1177 100644 --- a/src/ObservedRemoteOutboundRtp.ts +++ b/src/webrtc/ObservedRemoteOutboundRtp.ts @@ -1,6 +1,6 @@ -import { MediaKind } from './common/types'; +import { MediaKind } from '../common/types'; import { ObservedPeerConnection } from './ObservedPeerConnection'; -import { RemoteOutboundRtpStats } from './schema/ClientSample'; +import { RemoteOutboundRtpStats } from '../schema/ClientSample'; export class ObservedRemoteOutboundRtp implements RemoteOutboundRtpStats { private _visited = false; From d011e4804ba63b040f8a0efff6888b0b63b5a292 Mon Sep 17 00:00:00 2001 From: Balazs Kreith Date: Sun, 13 Jul 2025 08:31:27 +0300 Subject: [PATCH 5/9] fix: clear auto close timer in ObservedClient to prevent premature closure --- src/ObservedClient.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ObservedClient.ts b/src/ObservedClient.ts index ba7b378..bb7f09a 100644 --- a/src/ObservedClient.ts +++ b/src/ObservedClient.ts @@ -10,6 +10,7 @@ import { ClientMetaTypes } from './schema/ClientMetaTypes'; import { parseJsonAs } from './common/utils'; import { CalculatedScore } from './scores/CalculatedScore'; import { Detectors } from './detectors/Detectors'; +import { clear } from 'console'; const logger = createLogger('ObservedClient'); @@ -189,6 +190,8 @@ export class ObservedClient = Record { if (this.closed) return; From 957c4f1380e9faa44f14aeb9cd190a7dd1c7a392 Mon Sep 17 00:00:00 2001 From: Balazs Kreith Date: Wed, 16 Jul 2025 20:18:05 +0300 Subject: [PATCH 6/9] feat: add ObserverSummary and SummaryMonitor for call and client tracking --- src/{ => common}/ObserverSummary.ts | 0 src/common/SummaryTypes.ts | 91 ++++++++++++++ src/monitors/SummaryMonitor.ts | 181 ++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+) rename src/{ => common}/ObserverSummary.ts (100%) create mode 100644 src/common/SummaryTypes.ts create mode 100644 src/monitors/SummaryMonitor.ts diff --git a/src/ObserverSummary.ts b/src/common/ObserverSummary.ts similarity index 100% rename from src/ObserverSummary.ts rename to src/common/ObserverSummary.ts diff --git a/src/common/SummaryTypes.ts b/src/common/SummaryTypes.ts new file mode 100644 index 0000000..ff159bc --- /dev/null +++ b/src/common/SummaryTypes.ts @@ -0,0 +1,91 @@ +import { MediaKind } from "./types" + +export type CallSummary = { + startedAt: number; // the timestamp when the call started + endedAt?: number; // the timestamp when the call ended + id: string; // the call ID + attachments: Record; // will contain the merged attachments + clients: Record; // client ID -> client report + issues: Record; + totalScore: number; + numberOfScores: number; + scoreReasons: Record; + + liveState?: { + + }; +} + +export type ClientSummary = { + id: string; // the client ID + attachments: Record; // will contain the merged attachments + joinedAt: number; // the timestamp when the client joined the call + leftAt?: number; // the timestamp when the client left the call + peerConnections: Record; // peer connection ID -> peer connection report + issues: Record; + totalScore: number; + numberOfScores: number; + scoreReasons: Record; + + liveState?: { + score?: number; // the current score of the client + + }; +} + +export type PeerConnectionSummary = { + id: string; // the peer connection ID + attachments: Record; // will contain the merged attachments + openedAt: number; // the timestamp when the peer connection was opened + closedAt?: number; // the timestamp when the peer connection was closed + + inboundTracks: Record; // track ID -> inbound track report + outboundTracks: Record; // track ID -> outbound track report + + liveState?: { + score?: number; + }; +} + +type InboundTrackSummary = { + callId: string; // the call ID this track belongs to + clientId: string; // the client ID this track belongs to + peerConnectionId: string; // the peer connection ID this track belongs to + + id: string; + attachments: Record; // will contain the merged attachments + createdAt: number; + closedAt?: number; + kind: MediaKind; + + totalScore: number; + numberOfScores: number; + scoreReasons: Record; + + liveState?: { + score?: number; + remoteTrackMuted?: boolean; + muted?: boolean; + }; +} + +type OutboundTrackSummary = { + callId: string; // the call ID this track belongs to + clientId: string; // the client ID this track belongs to + peerConnectionId: string; // the peer connection ID this track belongs to + + id: string; + attachments: Record; // will contain the merged attachments + createdAt: number; + closedAt?: number; + kind: MediaKind; + + totalScore: number; + numberOfScores: number; + scoreReasons: Record; + + liveState?: { + score?: number; + muted?: boolean; + }; +} diff --git a/src/monitors/SummaryMonitor.ts b/src/monitors/SummaryMonitor.ts new file mode 100644 index 0000000..d5d4189 --- /dev/null +++ b/src/monitors/SummaryMonitor.ts @@ -0,0 +1,181 @@ +import { EventEmitter } from "events"; +import { CallSummary, ClientSummary } from "../common/SummaryTypes"; +import { ObservedCall } from "../ObservedCall"; +import { Observer } from "../Observer"; +import { createLogger } from "../common/logger"; +import { ObservedClient } from "../ObservedClient"; +import { ClientSample } from "../schema/ClientSample"; + +const logger = createLogger("SummaryMonitor"); + +export type SummaryMonitorEvents = { + 'call-summary': [CallSummary] +} + +export interface OngoingCallSumariesMap { + create(call: CallSummary): void | Promise; + update(call: CallSummary): void | Promise; + read(callId: string): Promise; + delete(callId: string): void | Promise; +} + + +export class SummaryMonitor extends EventEmitter { + + public constructor( + public readonly callSummaries = createSimpleOngoingCallSummariesMap(), + ) { + super(); + + } +} + +export function createSummaryMonitor(observer: Observer): SummaryMonitor { + const result = new SummaryMonitor(); + const eventer = observer.createEventMonitor(result); + + eventer.onCallAdded = onCallAdded; + eventer.onCallUpdated = onCallUpdated; + eventer.onCallRemoved = onCallRemoved; + eventer.onClientAdded = onClientAdded; + eventer.onClientUpdated = onClientUpdated; + eventer.onClientClosed = onClientClosed; + + return result; +} + +async function onCallAdded(call: ObservedCall, monitor: SummaryMonitor) { + const summary: CallSummary = { + startedAt: call.startedAt ?? Date.now(), + id: call.callId, + attachments: {}, + clients: {}, + issues: {}, + totalScore: 0, + numberOfScores: 0, + scoreReasons: {} + }; + + await monitor.callSummaries.create(summary); +} + +async function onCallUpdated(call: ObservedCall, monitor: SummaryMonitor) { + const summary = await monitor.callSummaries.read(call.callId); + + if (!summary) return logger.warn(`Call summary not found for call ID ${call.callId}`); + + // Update the summary with the latest call data + if (call.startedAt) { + summary.startedAt = call.startedAt; + } + + if (call.score !== undefined) { + summary.totalScore += call.score; + ++summary.numberOfScores; + + summary.liveState = { + ...summary.liveState, + score: call.score + } + } + + await monitor.callSummaries.update(summary); +} + +async function onCallRemoved(call: ObservedCall, monitor: SummaryMonitor) { + const summary = await monitor.callSummaries.read(call.callId); + + if (!summary) return logger.warn(`Call summary not found for call ID ${call.callId}`); + + summary.liveState = undefined; + + await monitor.callSummaries.delete(call.callId); + + monitor.emit('call-summary', summary); +} + + +async function onClientAdded(client: ObservedClient, monitor: SummaryMonitor) { + const callSummary = await monitor.callSummaries.read(client.call.callId); + + if (!callSummary) return logger.warn(`Call summary not found for call ID ${client.call.callId}`); + + const clientSummary: ClientSummary = { + id: client.clientId, + attachments: {}, + joinedAt: client.joinedAt ?? Date.now(), + leftAt: client.leftAt, + peerConnections: {}, + issues: {}, + totalScore: 0, + numberOfScores: 0, + scoreReasons: {} + }; + + callSummary.clients[client.clientId] = clientSummary; + + await monitor.callSummaries.update(callSummary); +} + +async function onClientUpdated(client: ObservedClient, sample: ClientSample, monitor: SummaryMonitor) { + const callSummary = await monitor.callSummaries.read(client.call.callId); + + if (!callSummary) return logger.warn(`Call summary not found for call ID ${client.call.callId}`); + + const clientSummary = callSummary.clients[client.clientId]; + + if (!clientSummary) return logger.warn(`Client summary not found for client ID ${client.clientId} in call ID ${callSummary.id}`); + + if (client.score !== undefined) { + clientSummary.totalScore += client.score; + ++clientSummary.numberOfScores; + + clientSummary.liveState = { + ...clientSummary.liveState, + score: client.score + } + } + + await monitor.callSummaries.update(callSummary); + + sample; + +} + +async function onClientClosed(client: ObservedClient, monitor: SummaryMonitor) { + const callSummary = await monitor.callSummaries.read(client.call.callId); + + if (!callSummary) return logger.warn(`Call summary not found for call ID ${client.call.callId}`); + + const clientSummary = callSummary.clients[client.clientId]; + + if (!clientSummary) return logger.warn(`Client summary not found for client ID ${client.clientId} in call ID ${callSummary.id}`); + + clientSummary.leftAt = client.leftAt ?? Date.now(); + + await monitor.callSummaries.update(callSummary); +} + + + + +// ----------------------------------------------------- + +function createSimpleOngoingCallSummariesMap(): OngoingCallSumariesMap { + const map = new Map(); + + return { + create(call: CallSummary) { + map.set(call.id, call); + }, + update(call: CallSummary) { + // empty + }, + async read(callId: string) { + return map.get(callId); + }, + delete(callId: string) { + map.delete(callId); + } + }; +} \ No newline at end of file From f540e64722cd5648f69d482a4053034c79f8f9a1 Mon Sep 17 00:00:00 2001 From: Balazs Kreith Date: Wed, 16 Jul 2025 22:17:09 +0300 Subject: [PATCH 7/9] feat: add SummaryTypes and SummaryMonitor exports to index --- src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index 0327f58..001ecf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,3 +26,8 @@ export { ScoreCalculator } from './scores/ScoreCalculator'; export { ObservedClientEventMonitor } from './ObservedClientEventMonitor'; export { ObserverEventMonitor } from './ObserverEventMonitor'; export { Middleware } from './common/Middleware'; +export * as SummaryTypes from './common/SummaryTypes'; +export { + createSummaryMonitor, + OngoingCallSumariesMap, +} from './monitors/SummaryMonitor'; \ No newline at end of file From 30b9c33b347fc532fccd496537375ba3a466ce05 Mon Sep 17 00:00:00 2001 From: Balazs Kreith Date: Sat, 2 Aug 2025 21:35:06 +0300 Subject: [PATCH 8/9] feat: enhance ObservedCall and ObservedClient with additional metrics and user agent data parsing; remove SummaryTypes and SummaryMonitor --- src/ObservedCall.ts | 35 +++++++ src/ObservedClient.ts | 29 ++++++ src/common/SummaryTypes.ts | 91 ----------------- src/common/types.ts | 21 +++- src/index.ts | 7 +- src/monitors/SummaryMonitor.ts | 181 --------------------------------- 6 files changed, 85 insertions(+), 279 deletions(-) delete mode 100644 src/common/SummaryTypes.ts delete mode 100644 src/monitors/SummaryMonitor.ts diff --git a/src/ObservedCall.ts b/src/ObservedCall.ts index 8642cb5..37ca3f1 100644 --- a/src/ObservedCall.ts +++ b/src/ObservedCall.ts @@ -79,16 +79,32 @@ export class ObservedCall = Record; @@ -228,6 +244,7 @@ export class ObservedCall = Record = Record c.lastSampleTimestamp ?? 0)); + this.emit('update'); this.deltaNumberOfIssues = 0; @@ -245,6 +264,20 @@ export class ObservedCall = Record = Record = Record(metadata.payload); + if (userAgentData) { + if (this.operationSystem === undefined) { + this.operationSystem = { + name: userAgentData.os.name.toLowerCase(), + version: userAgentData.os.version, + }; + } + if (this.engine === undefined) { + this.engine = { + name: userAgentData.engine.name.toLowerCase(), + version: userAgentData.engine.version, + }; + } + if (this.browser === undefined) { + this.browser = { + name: userAgentData.browser.name.toLowerCase(), + version: userAgentData.browser.version, + }; + } + } + + } catch (error) { + logger.warn('Failed to parse USER_AGENT_DATA metadata: %o', error); + } + } } this.call.observer.emit('client-metadata', this, metadata); diff --git a/src/common/SummaryTypes.ts b/src/common/SummaryTypes.ts deleted file mode 100644 index ff159bc..0000000 --- a/src/common/SummaryTypes.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { MediaKind } from "./types" - -export type CallSummary = { - startedAt: number; // the timestamp when the call started - endedAt?: number; // the timestamp when the call ended - id: string; // the call ID - attachments: Record; // will contain the merged attachments - clients: Record; // client ID -> client report - issues: Record; - totalScore: number; - numberOfScores: number; - scoreReasons: Record; - - liveState?: { - - }; -} - -export type ClientSummary = { - id: string; // the client ID - attachments: Record; // will contain the merged attachments - joinedAt: number; // the timestamp when the client joined the call - leftAt?: number; // the timestamp when the client left the call - peerConnections: Record; // peer connection ID -> peer connection report - issues: Record; - totalScore: number; - numberOfScores: number; - scoreReasons: Record; - - liveState?: { - score?: number; // the current score of the client - - }; -} - -export type PeerConnectionSummary = { - id: string; // the peer connection ID - attachments: Record; // will contain the merged attachments - openedAt: number; // the timestamp when the peer connection was opened - closedAt?: number; // the timestamp when the peer connection was closed - - inboundTracks: Record; // track ID -> inbound track report - outboundTracks: Record; // track ID -> outbound track report - - liveState?: { - score?: number; - }; -} - -type InboundTrackSummary = { - callId: string; // the call ID this track belongs to - clientId: string; // the client ID this track belongs to - peerConnectionId: string; // the peer connection ID this track belongs to - - id: string; - attachments: Record; // will contain the merged attachments - createdAt: number; - closedAt?: number; - kind: MediaKind; - - totalScore: number; - numberOfScores: number; - scoreReasons: Record; - - liveState?: { - score?: number; - remoteTrackMuted?: boolean; - muted?: boolean; - }; -} - -type OutboundTrackSummary = { - callId: string; // the call ID this track belongs to - clientId: string; // the client ID this track belongs to - peerConnectionId: string; // the peer connection ID this track belongs to - - id: string; - attachments: Record; // will contain the merged attachments - createdAt: number; - closedAt?: number; - kind: MediaKind; - - totalScore: number; - numberOfScores: number; - scoreReasons: Record; - - liveState?: { - score?: number; - muted?: boolean; - }; -} diff --git a/src/common/types.ts b/src/common/types.ts index 3bf8fca..c95cab1 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,4 +1,23 @@ export type MediaKind = 'audio' | 'video'; export type SupportedVideoCodecType = 'vp8' | 'vp9' | 'h264' | 'h265'; - +export type ParsedUserAgent = { + ua: string; + browser: { + name: string; + version: string; + major: string; + }; + engine: { + name: string; + version: string; + }; + os: { + name: string; + version: string; + }; + device: Record; // or `{}` if always empty + cpu: { + architecture: string; + }; +}; // export type EvaluatorMiddleware = Middleware; diff --git a/src/index.ts b/src/index.ts index 001ecf8..ac86583 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,9 +25,4 @@ export { ClientSample, ClientIssue, ClientEvent, ClientMetaData } from './schema export { ScoreCalculator } from './scores/ScoreCalculator'; export { ObservedClientEventMonitor } from './ObservedClientEventMonitor'; export { ObserverEventMonitor } from './ObserverEventMonitor'; -export { Middleware } from './common/Middleware'; -export * as SummaryTypes from './common/SummaryTypes'; -export { - createSummaryMonitor, - OngoingCallSumariesMap, -} from './monitors/SummaryMonitor'; \ No newline at end of file +export { Middleware } from './common/Middleware'; \ No newline at end of file diff --git a/src/monitors/SummaryMonitor.ts b/src/monitors/SummaryMonitor.ts deleted file mode 100644 index d5d4189..0000000 --- a/src/monitors/SummaryMonitor.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { EventEmitter } from "events"; -import { CallSummary, ClientSummary } from "../common/SummaryTypes"; -import { ObservedCall } from "../ObservedCall"; -import { Observer } from "../Observer"; -import { createLogger } from "../common/logger"; -import { ObservedClient } from "../ObservedClient"; -import { ClientSample } from "../schema/ClientSample"; - -const logger = createLogger("SummaryMonitor"); - -export type SummaryMonitorEvents = { - 'call-summary': [CallSummary] -} - -export interface OngoingCallSumariesMap { - create(call: CallSummary): void | Promise; - update(call: CallSummary): void | Promise; - read(callId: string): Promise; - delete(callId: string): void | Promise; -} - - -export class SummaryMonitor extends EventEmitter { - - public constructor( - public readonly callSummaries = createSimpleOngoingCallSummariesMap(), - ) { - super(); - - } -} - -export function createSummaryMonitor(observer: Observer): SummaryMonitor { - const result = new SummaryMonitor(); - const eventer = observer.createEventMonitor(result); - - eventer.onCallAdded = onCallAdded; - eventer.onCallUpdated = onCallUpdated; - eventer.onCallRemoved = onCallRemoved; - eventer.onClientAdded = onClientAdded; - eventer.onClientUpdated = onClientUpdated; - eventer.onClientClosed = onClientClosed; - - return result; -} - -async function onCallAdded(call: ObservedCall, monitor: SummaryMonitor) { - const summary: CallSummary = { - startedAt: call.startedAt ?? Date.now(), - id: call.callId, - attachments: {}, - clients: {}, - issues: {}, - totalScore: 0, - numberOfScores: 0, - scoreReasons: {} - }; - - await monitor.callSummaries.create(summary); -} - -async function onCallUpdated(call: ObservedCall, monitor: SummaryMonitor) { - const summary = await monitor.callSummaries.read(call.callId); - - if (!summary) return logger.warn(`Call summary not found for call ID ${call.callId}`); - - // Update the summary with the latest call data - if (call.startedAt) { - summary.startedAt = call.startedAt; - } - - if (call.score !== undefined) { - summary.totalScore += call.score; - ++summary.numberOfScores; - - summary.liveState = { - ...summary.liveState, - score: call.score - } - } - - await monitor.callSummaries.update(summary); -} - -async function onCallRemoved(call: ObservedCall, monitor: SummaryMonitor) { - const summary = await monitor.callSummaries.read(call.callId); - - if (!summary) return logger.warn(`Call summary not found for call ID ${call.callId}`); - - summary.liveState = undefined; - - await monitor.callSummaries.delete(call.callId); - - monitor.emit('call-summary', summary); -} - - -async function onClientAdded(client: ObservedClient, monitor: SummaryMonitor) { - const callSummary = await monitor.callSummaries.read(client.call.callId); - - if (!callSummary) return logger.warn(`Call summary not found for call ID ${client.call.callId}`); - - const clientSummary: ClientSummary = { - id: client.clientId, - attachments: {}, - joinedAt: client.joinedAt ?? Date.now(), - leftAt: client.leftAt, - peerConnections: {}, - issues: {}, - totalScore: 0, - numberOfScores: 0, - scoreReasons: {} - }; - - callSummary.clients[client.clientId] = clientSummary; - - await monitor.callSummaries.update(callSummary); -} - -async function onClientUpdated(client: ObservedClient, sample: ClientSample, monitor: SummaryMonitor) { - const callSummary = await monitor.callSummaries.read(client.call.callId); - - if (!callSummary) return logger.warn(`Call summary not found for call ID ${client.call.callId}`); - - const clientSummary = callSummary.clients[client.clientId]; - - if (!clientSummary) return logger.warn(`Client summary not found for client ID ${client.clientId} in call ID ${callSummary.id}`); - - if (client.score !== undefined) { - clientSummary.totalScore += client.score; - ++clientSummary.numberOfScores; - - clientSummary.liveState = { - ...clientSummary.liveState, - score: client.score - } - } - - await monitor.callSummaries.update(callSummary); - - sample; - -} - -async function onClientClosed(client: ObservedClient, monitor: SummaryMonitor) { - const callSummary = await monitor.callSummaries.read(client.call.callId); - - if (!callSummary) return logger.warn(`Call summary not found for call ID ${client.call.callId}`); - - const clientSummary = callSummary.clients[client.clientId]; - - if (!clientSummary) return logger.warn(`Client summary not found for client ID ${client.clientId} in call ID ${callSummary.id}`); - - clientSummary.leftAt = client.leftAt ?? Date.now(); - - await monitor.callSummaries.update(callSummary); -} - - - - -// ----------------------------------------------------- - -function createSimpleOngoingCallSummariesMap(): OngoingCallSumariesMap { - const map = new Map(); - - return { - create(call: CallSummary) { - map.set(call.id, call); - }, - update(call: CallSummary) { - // empty - }, - async read(callId: string) { - return map.get(callId); - }, - delete(callId: string) { - map.delete(callId); - } - }; -} \ No newline at end of file From 31f12e20f56d4795eb0f58b383834b5d4c7602e9 Mon Sep 17 00:00:00 2001 From: balazskreith Date: Wed, 13 Aug 2025 09:27:50 +0200 Subject: [PATCH 9/9] refactor: clean up whitespace and improve code readability in ObservedCall class --- src/ObservedCall.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ObservedCall.ts b/src/ObservedCall.ts index 37ca3f1..8551489 100644 --- a/src/ObservedCall.ts +++ b/src/ObservedCall.ts @@ -98,7 +98,6 @@ export class ObservedCall = Record = Record = Record c.lastSampleTimestamp ?? 0)); + this.lastClientTimestamp = Math.max(...Array.from(this.observedClients.values()).map((c) => c.lastSampleTimestamp ?? 0)); this.emit('update'); @@ -295,8 +293,6 @@ export class ObservedCall = Record