@observertc/observer-js 0.41.7 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ObservedCall.d.ts +10 -3
- package/lib/ObservedCall.d.ts.map +1 -1
- package/lib/ObservedCall.js +28 -0
- package/lib/ObservedClient.d.ts +4 -0
- package/lib/ObservedClient.d.ts.map +1 -1
- package/lib/ObservedClient.js +81 -32
- package/lib/ObservedInboundAudioTrack.d.ts +94 -0
- package/lib/ObservedInboundAudioTrack.d.ts.map +1 -0
- package/lib/ObservedInboundAudioTrack.js +215 -0
- package/lib/ObservedInboundVideoTrack.d.ts +98 -0
- package/lib/ObservedInboundVideoTrack.d.ts.map +1 -0
- package/lib/{ObservedInboundTrack.js → ObservedInboundVideoTrack.js} +53 -44
- package/lib/ObservedOutboundAudioTrack.d.ts +85 -0
- package/lib/ObservedOutboundAudioTrack.d.ts.map +1 -0
- package/lib/{ObservedOutboundTrack.js → ObservedOutboundAudioTrack.js} +35 -58
- package/lib/ObservedOutboundVideoTrack.d.ts +88 -0
- package/lib/ObservedOutboundVideoTrack.d.ts.map +1 -0
- package/lib/ObservedOutboundVideoTrack.js +204 -0
- package/lib/ObservedPeerConnection.d.ts +26 -16
- package/lib/ObservedPeerConnection.d.ts.map +1 -1
- package/lib/ObservedPeerConnection.js +157 -86
- package/lib/RemoteTrackAssigner.d.ts +14 -0
- package/lib/RemoteTrackAssigner.d.ts.map +1 -0
- package/lib/RemoteTrackAssigner.js +52 -0
- package/lib/common/CalculatedScore.d.ts +79 -0
- package/lib/common/CalculatedScore.d.ts.map +1 -0
- package/lib/common/CalculatedScore.js +155 -0
- package/lib/common/QualityScoreUtils.d.ts +1 -0
- package/lib/common/QualityScoreUtils.d.ts.map +1 -0
- package/lib/common/QualityScoreUtils.js +1 -0
- package/lib/common/utils.d.ts +1 -0
- package/lib/common/utils.d.ts.map +1 -1
- package/lib/common/utils.js +14 -1
- package/lib/index.d.ts +4 -2
- package/lib/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ObservedCall.ts +40 -4
- package/src/ObservedClient.ts +77 -17
- package/src/ObservedInboundAudioTrack.ts +336 -0
- package/src/{ObservedInboundTrack.ts → ObservedInboundVideoTrack.ts} +85 -66
- package/src/{ObservedOutboundTrack.ts → ObservedOutboundAudioTrack.ts} +59 -74
- package/src/ObservedOutboundVideoTrack.ts +326 -0
- package/src/ObservedPeerConnection.ts +205 -131
- package/src/RemoteTrackAssigner.ts +68 -0
- package/src/common/CalculatedScore.ts +180 -0
- package/src/common/QualityScoreUtils.ts +0 -0
- package/src/common/utils.ts +17 -1
- package/src/index.ts +4 -2
- package/src/monitors/CallSummaryMonitor.ts +3 -3
- package/lib/ObservedInboundTrack.d.ts +0 -96
- package/lib/ObservedInboundTrack.d.ts.map +0 -1
- package/lib/ObservedOutboundTrack.d.ts +0 -84
- package/lib/ObservedOutboundTrack.d.ts.map +0 -1
package/lib/index.d.ts
CHANGED
|
@@ -9,8 +9,10 @@ export type { ClientIssueMonitorConfig, ClientIssueMonitorEmittedIssueEvent, Cli
|
|
|
9
9
|
export type { ObservedCall, ObservedCallModel } from './ObservedCall';
|
|
10
10
|
export type { ObservedClient, ObservedClientModel } from './ObservedClient';
|
|
11
11
|
export type { ObservedPeerConnection, ObservedPeerConnectionModel, ObservedPeerConnectionEvents } from './ObservedPeerConnection';
|
|
12
|
-
export type {
|
|
13
|
-
export type {
|
|
12
|
+
export type { ObservedInboundAudioTrack, ObservedInboundAudioTrackModel, ObservedInboundAudioTrackEvents } from './ObservedInboundAudioTrack';
|
|
13
|
+
export type { ObservedOutboundAudioTrack, ObservedOutboundAudioTrackModel, ObservedOutboundAudioTrackEvents } from './ObservedOutboundAudioTrack';
|
|
14
|
+
export type { ObservedInboundVideoTrack, ObservedInboundVideoTrackModel, ObservedInboundVideoTrackEvents } from './ObservedInboundVideoTrack';
|
|
15
|
+
export type { ObservedOutboundVideoTrack, ObservedOutboundVideoTrackModel, ObservedOutboundVideoTrackEvents } from './ObservedOutboundVideoTrack';
|
|
14
16
|
export type { CallSummary, ClientSummary, ClientIssue } from './monitors/CallSummary';
|
|
15
17
|
export type { ObserverSinkContext } from './common/types';
|
|
16
18
|
export type { SfuServerMonitorMetricsRecord } from './monitors/SfuServerMonitor';
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAExD,YAAY,EACX,OAAO,EACP,YAAY,EACZ,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,WAAW,GACX,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACX,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,6BAA6B,EAC7B,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACX,wBAAwB,EACxB,mCAAmC,EACnC,kBAAkB,EAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAClI,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,YAAY,MAAM,+BAA+B,CAAC;AAE9D,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAExD,YAAY,EACX,OAAO,EACP,YAAY,EACZ,SAAS,EACT,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,WAAW,GACX,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACX,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,6BAA6B,EAC7B,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACX,wBAAwB,EACxB,mCAAmC,EACnC,kBAAkB,EAClB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,YAAY,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAClI,YAAY,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,+BAA+B,EAAE,MAAM,6BAA6B,CAAC;AAC9I,YAAY,EAAE,0BAA0B,EAAE,+BAA+B,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAClJ,YAAY,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,+BAA+B,EAAE,MAAM,6BAA6B,CAAC;AAC9I,YAAY,EAAE,0BAA0B,EAAE,+BAA+B,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAClJ,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACtF,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACjF,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAClH,OAAO,EACN,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mCAAmC,EACnC,kCAAkC,EAClC,oCAAoC,GACpC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEpG,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACtD,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,YAE9D;AAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAA8B,MAAM,iBAAiB,CAAC;AAC/E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAG3D"}
|
package/package.json
CHANGED
package/src/ObservedCall.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { ObservedClient, ObservedClientEvents, ObservedClientModel } from './ObservedClient';
|
|
3
|
-
import { ObservedOutboundTrack } from './ObservedOutboundTrack';
|
|
4
3
|
import { Observer } from './Observer';
|
|
5
4
|
import { createClientJoinedEventReport } from './common/callEventReports';
|
|
6
|
-
import { PartialBy } from './common/utils';
|
|
5
|
+
import { getMedian, PartialBy } from './common/utils';
|
|
7
6
|
import { CallEventReport } from '@observertc/report-schemas-js';
|
|
8
7
|
import { createProcessor } from './common/Middleware';
|
|
9
8
|
import { ClientSample } from '@observertc/sample-schemas-js';
|
|
10
9
|
import { ClientIssue } from './monitors/CallSummary';
|
|
10
|
+
import { ObservedOutboundAudioTrack } from './ObservedOutboundAudioTrack';
|
|
11
|
+
import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack';
|
|
12
|
+
import { CalculatedScore } from './common/CalculatedScore';
|
|
11
13
|
|
|
12
14
|
type ClientIssueDetectionConfig = Pick<ClientIssue, 'severity'>;
|
|
13
15
|
|
|
@@ -38,8 +40,13 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
|
|
|
38
40
|
|
|
39
41
|
private readonly _clients = new Map<string, ObservedClient>();
|
|
40
42
|
|
|
41
|
-
public
|
|
42
|
-
|
|
43
|
+
public _scoreData?: {
|
|
44
|
+
score: CalculatedScore,
|
|
45
|
+
calculated: number,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
public readonly sfuStreamIdToOutboundAudioTrack = new Map<string | number, ObservedOutboundAudioTrack>();
|
|
49
|
+
public readonly sfuStreamIdToOutboundVideoTrack = new Map<string | number, ObservedOutboundVideoTrack>();
|
|
43
50
|
private _closed = false;
|
|
44
51
|
private _updated = Date.now();
|
|
45
52
|
private _ended?: number;
|
|
@@ -84,6 +91,35 @@ export class ObservedCall<AppData extends Record<string, unknown> = Record<strin
|
|
|
84
91
|
return this._clients;
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
public get score(): CalculatedScore | undefined {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
|
|
97
|
+
if (now - 15000 < (this._scoreData?.calculated ?? 0)) {
|
|
98
|
+
return this._scoreData?.score;
|
|
99
|
+
} else if (this.clients.size < 1) {
|
|
100
|
+
return {
|
|
101
|
+
remarks: [ {
|
|
102
|
+
severity: 'none',
|
|
103
|
+
text: 'No clients',
|
|
104
|
+
} ],
|
|
105
|
+
score: 5.0,
|
|
106
|
+
timestamp: now,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const scores = [ ...this.clients.values() ].map((client) => client.score?.score ?? 5.0);
|
|
110
|
+
|
|
111
|
+
this._scoreData = {
|
|
112
|
+
score: {
|
|
113
|
+
remarks: [],
|
|
114
|
+
score: getMedian(scores),
|
|
115
|
+
timestamp: now,
|
|
116
|
+
},
|
|
117
|
+
calculated: now,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return this._scoreData.score;
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
public get closed() {
|
|
88
124
|
return this._closed;
|
|
89
125
|
}
|
package/src/ObservedClient.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { CallEventType } from './common/CallEventType';
|
|
|
11
11
|
import { ObservedSfu } from './ObservedSfu';
|
|
12
12
|
import { ClientIssue } from './monitors/CallSummary';
|
|
13
13
|
import { CallEventReport } from '@observertc/report-schemas-js';
|
|
14
|
+
import { CalculatedScore } from './common/CalculatedScore';
|
|
14
15
|
|
|
15
16
|
const logger = createLogger('ObservedClient');
|
|
16
17
|
|
|
@@ -59,6 +60,7 @@ export type ObservedClientEvents = {
|
|
|
59
60
|
rejoined: [{
|
|
60
61
|
lastJoined: number,
|
|
61
62
|
}],
|
|
63
|
+
score: [CalculatedScore],
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
export declare interface ObservedClient<AppData extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -84,6 +86,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
84
86
|
private _timeZoneOffsetInHours?: number;
|
|
85
87
|
private _left?: number;
|
|
86
88
|
|
|
89
|
+
public score?: CalculatedScore;
|
|
87
90
|
public usingTURN = false;
|
|
88
91
|
public availableOutgoingBitrate = 0;
|
|
89
92
|
public availableIncomingBitrate = 0;
|
|
@@ -238,7 +241,6 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
238
241
|
if (!this._left) {
|
|
239
242
|
this._addClientLeftReport();
|
|
240
243
|
}
|
|
241
|
-
|
|
242
244
|
Array.from(this._peerConnections.values()).forEach((peerConnection) => peerConnection.close());
|
|
243
245
|
|
|
244
246
|
this.emit('close');
|
|
@@ -302,8 +304,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
302
304
|
logger.warn(`Error adding client issue: ${(err as Error)?.message}`);
|
|
303
305
|
}
|
|
304
306
|
|
|
305
|
-
this.
|
|
306
|
-
this.emit('issue', issue);
|
|
307
|
+
this._addAndEmitIssue(issue);
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
public accept(sample: ClientSample): void {
|
|
@@ -515,9 +516,8 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
515
516
|
trackId: callEvent.mediaTrackId,
|
|
516
517
|
attachments: callEvent.attachments ? JSON.parse(callEvent.attachments) : undefined,
|
|
517
518
|
};
|
|
518
|
-
|
|
519
|
-
this.
|
|
520
|
-
this.emit('issue', issue);
|
|
519
|
+
|
|
520
|
+
this._addAndEmitIssue(issue);
|
|
521
521
|
} catch (err) {
|
|
522
522
|
logger.warn(`Error parsing client issue: ${(err as Error)?.message}`);
|
|
523
523
|
}
|
|
@@ -619,7 +619,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
619
619
|
type: CallMetaType.MEDIA_DEVICE,
|
|
620
620
|
payload: mediaDevice,
|
|
621
621
|
}, this.userId);
|
|
622
|
-
|
|
622
|
+
|
|
623
623
|
this.reports.addCallMetaReport(callMetaReport);
|
|
624
624
|
mediaDevice.label && this.mediaDevices.push(mediaDevice.label);
|
|
625
625
|
}
|
|
@@ -634,7 +634,7 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
634
634
|
type: CallMetaType.MEDIA_SOURCE,
|
|
635
635
|
payload: mediaSource,
|
|
636
636
|
}, this.userId);
|
|
637
|
-
|
|
637
|
+
|
|
638
638
|
this.reports.addCallMetaReport(callMetaReport);
|
|
639
639
|
}
|
|
640
640
|
|
|
@@ -670,10 +670,17 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
670
670
|
|
|
671
671
|
inboundAudioTrack.update(track, sample.timestamp);
|
|
672
672
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
remoteOutboundTrack
|
|
673
|
+
if (!inboundAudioTrack.remoteOutboundTrack) {
|
|
674
|
+
const remoteOutboundTrack = this.call.sfuStreamIdToOutboundAudioTrack.get(inboundAudioTrack.sfuStreamId ?? '');
|
|
675
|
+
|
|
676
|
+
if (remoteOutboundTrack) {
|
|
677
|
+
inboundAudioTrack.remoteOutboundTrack = remoteOutboundTrack;
|
|
678
|
+
|
|
679
|
+
inboundAudioTrack.once('close', () => {
|
|
680
|
+
remoteOutboundTrack?.remoteInboundTracks.delete(inboundAudioTrack.trackId ?? '');
|
|
681
|
+
});
|
|
682
|
+
remoteOutboundTrack.remoteInboundTracks.set(inboundAudioTrack.trackId ?? '', inboundAudioTrack);
|
|
683
|
+
}
|
|
677
684
|
}
|
|
678
685
|
} catch (err) {
|
|
679
686
|
logger.error(`Error creating inbound audio track: ${(err as Error)?.message}`);
|
|
@@ -705,11 +712,18 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
705
712
|
});
|
|
706
713
|
|
|
707
714
|
inboundVideoTrack.update(track, sample.timestamp);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
remoteOutboundTrack
|
|
715
|
+
|
|
716
|
+
if (!inboundVideoTrack.remoteOutboundTrack) {
|
|
717
|
+
const remoteOutboundTrack = this.call.sfuStreamIdToOutboundVideoTrack.get(inboundVideoTrack.sfuStreamId ?? '');
|
|
718
|
+
|
|
719
|
+
if (remoteOutboundTrack) {
|
|
720
|
+
inboundVideoTrack.remoteOutboundTrack = remoteOutboundTrack;
|
|
721
|
+
|
|
722
|
+
inboundVideoTrack.once('close', () => {
|
|
723
|
+
remoteOutboundTrack?.remoteInboundTracks.delete(inboundVideoTrack.trackId ?? '');
|
|
724
|
+
});
|
|
725
|
+
remoteOutboundTrack.remoteInboundTracks.set(inboundVideoTrack.trackId ?? '', inboundVideoTrack);
|
|
726
|
+
}
|
|
713
727
|
}
|
|
714
728
|
} catch (err) {
|
|
715
729
|
logger.error(`Error creating inbound video track: ${(err as Error)?.message}`);
|
|
@@ -734,6 +748,11 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
734
748
|
});
|
|
735
749
|
|
|
736
750
|
outboundAudioTrack.update(track, sample.timestamp);
|
|
751
|
+
|
|
752
|
+
if (outboundAudioTrack.sfuStreamId && !this.call.sfuStreamIdToOutboundAudioTrack.has(outboundAudioTrack.sfuStreamId)) {
|
|
753
|
+
this.call.sfuStreamIdToOutboundAudioTrack.set(outboundAudioTrack.sfuStreamId, outboundAudioTrack);
|
|
754
|
+
}
|
|
755
|
+
|
|
737
756
|
} catch (err) {
|
|
738
757
|
logger.error(`Error creating outbound audio track: ${(err as Error)?.message}`);
|
|
739
758
|
}
|
|
@@ -759,6 +778,10 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
759
778
|
|
|
760
779
|
outboundVideoTrack.update(track, sample.timestamp);
|
|
761
780
|
|
|
781
|
+
if (outboundVideoTrack.sfuStreamId && !this.call.sfuStreamIdToOutboundVideoTrack.has(outboundVideoTrack.sfuStreamId)) {
|
|
782
|
+
this.call.sfuStreamIdToOutboundVideoTrack.set(outboundVideoTrack.sfuStreamId, outboundVideoTrack);
|
|
783
|
+
}
|
|
784
|
+
|
|
762
785
|
} catch (err) {
|
|
763
786
|
logger.error(`Error creating outbound video track: ${(err as Error)?.message}`);
|
|
764
787
|
}
|
|
@@ -897,6 +920,9 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
897
920
|
let sumRttInMs = 0;
|
|
898
921
|
let anyPeerConnectionUsingTurn = false;
|
|
899
922
|
|
|
923
|
+
let minPcScore = Number.MAX_SAFE_INTEGER;
|
|
924
|
+
let maxPcScore = Number.MIN_SAFE_INTEGER;
|
|
925
|
+
|
|
900
926
|
for (const peerConnection of this._peerConnections.values()) {
|
|
901
927
|
if (peerConnection.closed) continue;
|
|
902
928
|
peerConnection.updateMetrics();
|
|
@@ -916,8 +942,21 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
916
942
|
sumRttInMs += peerConnection.avgRttInMs ?? 0;
|
|
917
943
|
|
|
918
944
|
anyPeerConnectionUsingTurn ||= peerConnection.usingTURN;
|
|
945
|
+
|
|
946
|
+
minPcScore = Math.min(minPcScore, peerConnection.score?.score ?? Number.MAX_SAFE_INTEGER);
|
|
947
|
+
maxPcScore = Math.max(maxPcScore, peerConnection.score?.score ?? Number.MIN_SAFE_INTEGER);
|
|
919
948
|
}
|
|
920
949
|
|
|
950
|
+
this.score = {
|
|
951
|
+
score: minPcScore,
|
|
952
|
+
remarks: [ {
|
|
953
|
+
severity: 'none',
|
|
954
|
+
text: `Min and max score of all peer connections: ${minPcScore}, ${maxPcScore}`,
|
|
955
|
+
} ],
|
|
956
|
+
timestamp: sample.timestamp,
|
|
957
|
+
};
|
|
958
|
+
this.emit('score', this.score);
|
|
959
|
+
|
|
921
960
|
this.usingTURN = anyPeerConnectionUsingTurn === true;
|
|
922
961
|
|
|
923
962
|
if (wasUsingTURN !== this.usingTURN) this.emit('usingturn', this.usingTURN);
|
|
@@ -951,6 +990,27 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
|
|
|
951
990
|
});
|
|
952
991
|
}
|
|
953
992
|
|
|
993
|
+
private _addAndEmitIssue(issue: ClientIssue) {
|
|
994
|
+
this.issues.push(issue);
|
|
995
|
+
|
|
996
|
+
if (issue.peerConnectionId) {
|
|
997
|
+
const peerConnection = this._peerConnections.get(issue.peerConnectionId);
|
|
998
|
+
|
|
999
|
+
if (peerConnection) {
|
|
1000
|
+
|
|
1001
|
+
if (issue.trackId) {
|
|
1002
|
+
const track = peerConnection.inboundAudioTracks.get(issue.trackId) ?? peerConnection.inboundVideoTracks.get(issue.trackId);
|
|
1003
|
+
|
|
1004
|
+
if (track) track.ωpendingIssuesForScores.push(issue);
|
|
1005
|
+
} else {
|
|
1006
|
+
peerConnection.ωpendingIssuesForScores.push(issue);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
this.emit('issue', issue);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
954
1014
|
private _createPeerConnection(peerConnectionId: string, label?: string): ObservedPeerConnection {
|
|
955
1015
|
const result = new ObservedPeerConnection({
|
|
956
1016
|
peerConnectionId,
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { ObservedPeerConnection } from './ObservedPeerConnection';
|
|
3
|
+
import { InboundAudioTrack } from '@observertc/sample-schemas-js';
|
|
4
|
+
import { InboundAudioTrackReport, InboundVideoTrackReport } from '@observertc/report-schemas-js';
|
|
5
|
+
import { calculateBaseAudioScore, CalculatedScore } from './common/CalculatedScore';
|
|
6
|
+
import { ClientIssue } from './monitors/CallSummary';
|
|
7
|
+
import { ObservedOutboundAudioTrack } from './ObservedOutboundAudioTrack';
|
|
8
|
+
|
|
9
|
+
export type ObservedInboundAudioTrackModel = {
|
|
10
|
+
trackId: string;
|
|
11
|
+
sfuStreamId?: string;
|
|
12
|
+
sfuSinkId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ObservedInboundAudioTrackEvents = {
|
|
16
|
+
update: [{
|
|
17
|
+
elapsedTimeInMs: number;
|
|
18
|
+
}],
|
|
19
|
+
score: [CalculatedScore],
|
|
20
|
+
close: [],
|
|
21
|
+
remoteoutboundtrack: [ObservedOutboundAudioTrack | undefined],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ObservedInboundAudioTrackStats = InboundAudioTrack & {
|
|
25
|
+
ssrc: number;
|
|
26
|
+
bitrate: number;
|
|
27
|
+
fractionLost: number;
|
|
28
|
+
rttInMs?: number;
|
|
29
|
+
deltaReceivedBytes?: number;
|
|
30
|
+
deltaLostPackets?: number;
|
|
31
|
+
deltaReceivedPackets?: number;
|
|
32
|
+
deltaReceivedFrames?: number;
|
|
33
|
+
deltaDecodedFrames?: number;
|
|
34
|
+
deltaDroppedFrames?: number;
|
|
35
|
+
deltaReceivedSamples?: number;
|
|
36
|
+
deltaSilentConcealedSamples?: number;
|
|
37
|
+
fractionLoss?: number;
|
|
38
|
+
statsTimestamp: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// {
|
|
42
|
+
// [Property in keyof ObservedInboundAudioTrackStats<K>]: ObservedInboundAudioTrackStats<K>[Property];
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
export declare interface ObservedInboundAudioTrack {
|
|
46
|
+
on<U extends keyof ObservedInboundAudioTrackEvents>(event: U, listener: (...args: ObservedInboundAudioTrackEvents[U]) => void): this;
|
|
47
|
+
off<U extends keyof ObservedInboundAudioTrackEvents>(event: U, listener: (...args: ObservedInboundAudioTrackEvents[U]) => void): this;
|
|
48
|
+
once<U extends keyof ObservedInboundAudioTrackEvents>(event: U, listener: (...args: ObservedInboundAudioTrackEvents[U]) => void): this;
|
|
49
|
+
emit<U extends keyof ObservedInboundAudioTrackEvents>(event: U, ...args: ObservedInboundAudioTrackEvents[U]): boolean;
|
|
50
|
+
update(sample: InboundAudioTrack, timestamp: number): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ObservedInboundAudioTrack extends EventEmitter {
|
|
54
|
+
public readonly created = Date.now();
|
|
55
|
+
public visited = false;
|
|
56
|
+
|
|
57
|
+
private readonly _stats = new Map<number, ObservedInboundAudioTrackStats>();
|
|
58
|
+
|
|
59
|
+
private _closed = false;
|
|
60
|
+
private _updated = Date.now();
|
|
61
|
+
private _remoteOutboundTrack?: ObservedOutboundAudioTrack;
|
|
62
|
+
private _lastMaxStatsTimestamp = 0;
|
|
63
|
+
|
|
64
|
+
public bitrate = 0;
|
|
65
|
+
public rttInMs?: number;
|
|
66
|
+
public jitter?: number;
|
|
67
|
+
public fractionLoss = 0;
|
|
68
|
+
public marker?: string;
|
|
69
|
+
|
|
70
|
+
public totalLostPackets = 0;
|
|
71
|
+
public totalReceivedPackets = 0;
|
|
72
|
+
public totalBytesReceived = 0;
|
|
73
|
+
public totalReceivedSamples = 0;
|
|
74
|
+
public totalSilentConcealedSamples = 0;
|
|
75
|
+
|
|
76
|
+
public deltaLostPackets = 0;
|
|
77
|
+
public deltaReceivedPackets = 0;
|
|
78
|
+
public deltaBytesReceived = 0;
|
|
79
|
+
public deltaReceivedSamples = 0;
|
|
80
|
+
public deltaSilentConcealedSamples = 0;
|
|
81
|
+
|
|
82
|
+
public score?: CalculatedScore;
|
|
83
|
+
public ωpendingIssuesForScores: ClientIssue[] = [];
|
|
84
|
+
|
|
85
|
+
public constructor(
|
|
86
|
+
private readonly _model: ObservedInboundAudioTrackModel,
|
|
87
|
+
public readonly peerConnection: ObservedPeerConnection,
|
|
88
|
+
) {
|
|
89
|
+
super();
|
|
90
|
+
this.setMaxListeners(Infinity);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public get serviceId() {
|
|
94
|
+
return this.peerConnection.serviceId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public get roomId() {
|
|
98
|
+
return this.peerConnection.roomId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public get callId() {
|
|
102
|
+
return this.peerConnection.callId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public get clientId() {
|
|
106
|
+
return this.peerConnection.clientId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public get mediaUnitId() {
|
|
110
|
+
return this.peerConnection.mediaUnitId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public get peerConnectionId() {
|
|
114
|
+
return this.peerConnection.peerConnectionId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public get trackId() {
|
|
118
|
+
return this._model.trackId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public get sfuStreamId() {
|
|
122
|
+
return this._model.sfuStreamId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public get sfuSinkId() {
|
|
126
|
+
return this._model.sfuSinkId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public get updated() {
|
|
130
|
+
return this._updated;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public get statsTimestamp() {
|
|
134
|
+
return this._lastMaxStatsTimestamp;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public get stats(): ReadonlyMap<number, ObservedInboundAudioTrackStats> {
|
|
138
|
+
return this._stats;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public get reports() {
|
|
142
|
+
return this.peerConnection.reports;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public set remoteOutboundTrack(track: ObservedOutboundAudioTrack | undefined) {
|
|
146
|
+
if (this._closed) return;
|
|
147
|
+
if (this._remoteOutboundTrack) return;
|
|
148
|
+
if (!track) return;
|
|
149
|
+
|
|
150
|
+
track.once('close', () => {
|
|
151
|
+
this._remoteOutboundTrack = undefined;
|
|
152
|
+
this.emit('remoteoutboundtrack', undefined);
|
|
153
|
+
});
|
|
154
|
+
this._remoteOutboundTrack = track;
|
|
155
|
+
this.emit('remoteoutboundtrack', track);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public get remoteOutboundTrack() {
|
|
159
|
+
return this._remoteOutboundTrack;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public get closed() {
|
|
163
|
+
return this._closed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public close() {
|
|
167
|
+
if (this._closed) return;
|
|
168
|
+
|
|
169
|
+
this._closed = true;
|
|
170
|
+
|
|
171
|
+
this.emit('close');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public update(sample: InboundAudioTrack, statsTimestamp: number) {
|
|
175
|
+
if (this._closed) return;
|
|
176
|
+
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
const report: InboundAudioTrackReport | InboundVideoTrackReport = {
|
|
179
|
+
serviceId: this.peerConnection.client.call.serviceId,
|
|
180
|
+
roomId: this.peerConnection.client.call.roomId,
|
|
181
|
+
callId: this.peerConnection.client.call.callId,
|
|
182
|
+
clientId: this.peerConnection.client.clientId,
|
|
183
|
+
userId: this.peerConnection.client.userId,
|
|
184
|
+
mediaUnitId: this.peerConnection.client.mediaUnitId,
|
|
185
|
+
peerConnectionId: this.peerConnection.peerConnectionId,
|
|
186
|
+
...sample,
|
|
187
|
+
timestamp: statsTimestamp,
|
|
188
|
+
sampleSeq: -1,
|
|
189
|
+
|
|
190
|
+
remoteClientId: sample.remoteClientId ?? this.remoteOutboundTrack?.peerConnection.client.clientId,
|
|
191
|
+
remoteUserId: this.remoteOutboundTrack?.peerConnection.client.userId,
|
|
192
|
+
remoteTrackId: this.remoteOutboundTrack?.trackId,
|
|
193
|
+
remotePeerConnectionId: this.remoteOutboundTrack?.peerConnection.peerConnectionId,
|
|
194
|
+
marker: this.marker,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.reports.addInboundAudioTrackReport(report);
|
|
198
|
+
|
|
199
|
+
if (!this._model.sfuStreamId && sample.sfuStreamId) {
|
|
200
|
+
this._model.sfuStreamId = sample.sfuStreamId;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!this._model.sfuSinkId && sample.sfuSinkId) {
|
|
204
|
+
this._model.sfuSinkId = sample.sfuSinkId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const elapsedTimeInMs = Math.max(1, now - this._updated);
|
|
208
|
+
const lastStat = this._stats.get(sample.ssrc);
|
|
209
|
+
const rttInMs = sample.roundTripTime ? sample.roundTripTime * 1000 : undefined;
|
|
210
|
+
let bitrate = 0;
|
|
211
|
+
let deltaReceivedBytes = 0;
|
|
212
|
+
let fractionLost = 0;
|
|
213
|
+
let deltaLostPackets = 0;
|
|
214
|
+
let deltaReceivedPackets = 0;
|
|
215
|
+
|
|
216
|
+
if (lastStat?.bytesReceived && sample.bytesReceived && lastStat.bytesReceived < sample.bytesReceived) {
|
|
217
|
+
deltaReceivedBytes = sample.bytesReceived - lastStat.bytesReceived;
|
|
218
|
+
bitrate = deltaReceivedBytes / (elapsedTimeInMs / 1000);
|
|
219
|
+
}
|
|
220
|
+
if (lastStat?.packetsReceived && sample?.packetsReceived && lastStat.packetsReceived < sample.packetsReceived) {
|
|
221
|
+
deltaReceivedPackets = sample.packetsReceived - lastStat.packetsReceived;
|
|
222
|
+
}
|
|
223
|
+
if (lastStat?.packetsLost && sample.packetsLost && lastStat.packetsLost < sample.packetsLost) {
|
|
224
|
+
deltaLostPackets = sample.packetsLost - lastStat.packetsLost;
|
|
225
|
+
if (0 < deltaReceivedPackets) {
|
|
226
|
+
fractionLost = deltaLostPackets / (deltaReceivedPackets + deltaLostPackets);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let deltaDecodedFrames: number | undefined;
|
|
231
|
+
let deltaDroppedFrames: number | undefined;
|
|
232
|
+
let deltaReceivedFrames: number | undefined;
|
|
233
|
+
let deltaSilentConcealedSamples: number | undefined;
|
|
234
|
+
const audioSample = sample as InboundAudioTrack;
|
|
235
|
+
const lastAudioStat = lastStat as InboundAudioTrack | undefined;
|
|
236
|
+
|
|
237
|
+
if (audioSample.silentConcealedSamples && lastAudioStat?.silentConcealedSamples && lastAudioStat.silentConcealedSamples < audioSample.silentConcealedSamples) {
|
|
238
|
+
deltaSilentConcealedSamples = audioSample.silentConcealedSamples - lastAudioStat.silentConcealedSamples;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const stats: ObservedInboundAudioTrackStats = {
|
|
242
|
+
...sample,
|
|
243
|
+
fractionLost,
|
|
244
|
+
rttInMs,
|
|
245
|
+
bitrate,
|
|
246
|
+
ssrc: sample.ssrc,
|
|
247
|
+
deltaReceivedBytes,
|
|
248
|
+
deltaLostPackets,
|
|
249
|
+
deltaReceivedPackets,
|
|
250
|
+
deltaReceivedFrames,
|
|
251
|
+
deltaDecodedFrames,
|
|
252
|
+
deltaDroppedFrames,
|
|
253
|
+
deltaSilentConcealedSamples,
|
|
254
|
+
statsTimestamp,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
this._stats.set(sample.ssrc, stats);
|
|
258
|
+
|
|
259
|
+
this.visited = true;
|
|
260
|
+
this._updated = now;
|
|
261
|
+
|
|
262
|
+
this.emit('update', {
|
|
263
|
+
elapsedTimeInMs,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public updateMetrics() {
|
|
268
|
+
let maxStatsTimestamp = 0;
|
|
269
|
+
let rttInMsSum = 0;
|
|
270
|
+
let jitterSum = 0;
|
|
271
|
+
let size = 0;
|
|
272
|
+
|
|
273
|
+
this.bitrate = 0;
|
|
274
|
+
this.rttInMs = undefined;
|
|
275
|
+
this.deltaBytesReceived = 0;
|
|
276
|
+
this.deltaLostPackets = 0;
|
|
277
|
+
this.deltaReceivedSamples = 0;
|
|
278
|
+
this.deltaSilentConcealedSamples = 0;
|
|
279
|
+
this.fractionLoss = 0;
|
|
280
|
+
|
|
281
|
+
for (const [ , stats ] of this._stats) {
|
|
282
|
+
if (stats.statsTimestamp <= this._lastMaxStatsTimestamp) continue;
|
|
283
|
+
|
|
284
|
+
this.deltaBytesReceived += stats.deltaReceivedBytes ?? 0;
|
|
285
|
+
this.deltaLostPackets += stats.deltaLostPackets ?? 0;
|
|
286
|
+
this.deltaReceivedPackets += stats.deltaReceivedFrames ?? 0;
|
|
287
|
+
this.deltaReceivedSamples += stats.deltaReceivedSamples ?? 0;
|
|
288
|
+
this.deltaSilentConcealedSamples += stats.deltaSilentConcealedSamples ?? 0;
|
|
289
|
+
this.bitrate += stats.bitrate;
|
|
290
|
+
|
|
291
|
+
maxStatsTimestamp = Math.max(maxStatsTimestamp, stats.statsTimestamp);
|
|
292
|
+
|
|
293
|
+
rttInMsSum += stats.rttInMs ?? 0;
|
|
294
|
+
jitterSum += stats.jitter ?? 0;
|
|
295
|
+
++size;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.totalBytesReceived += this.deltaBytesReceived;
|
|
299
|
+
this.totalLostPackets += this.deltaLostPackets;
|
|
300
|
+
this.totalReceivedPackets += this.deltaReceivedPackets;
|
|
301
|
+
this.totalReceivedSamples += this.deltaReceivedSamples;
|
|
302
|
+
this.totalSilentConcealedSamples += this.deltaSilentConcealedSamples;
|
|
303
|
+
|
|
304
|
+
this.rttInMs = rttInMsSum / Math.max(size, 1);
|
|
305
|
+
this.jitter = jitterSum / Math.max(size, 1);
|
|
306
|
+
this.fractionLoss = 0 < this.deltaReceivedPackets && 0 < this.deltaLostPackets ? (this.deltaLostPackets / (this.deltaReceivedPackets + this.deltaLostPackets)) : 0;
|
|
307
|
+
|
|
308
|
+
this._lastMaxStatsTimestamp = maxStatsTimestamp;
|
|
309
|
+
|
|
310
|
+
this._updateQualityScore(maxStatsTimestamp);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _updateQualityScore(timestamp: number) {
|
|
314
|
+
const newIssues = this.ωpendingIssuesForScores;
|
|
315
|
+
const score = calculateBaseAudioScore(this, newIssues);
|
|
316
|
+
|
|
317
|
+
if (0 < newIssues.length) {
|
|
318
|
+
this.ωpendingIssuesForScores = [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!score) return (this.score = undefined);
|
|
322
|
+
|
|
323
|
+
if (0.05 < this.fractionLoss) {
|
|
324
|
+
score.score *= 0.5;
|
|
325
|
+
score.remarks.push({
|
|
326
|
+
severity: 'major',
|
|
327
|
+
text: 'Fraction loss is too high',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
score.timestamp = timestamp;
|
|
332
|
+
this.score = score;
|
|
333
|
+
|
|
334
|
+
this.emit('score', this.score);
|
|
335
|
+
}
|
|
336
|
+
}
|