@observertc/observer-js 0.30.0-e12fcb4.0 → 0.30.2-beta

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 (39) hide show
  1. package/lib/ObservedCall.d.ts +1 -1
  2. package/lib/ObservedCall.d.ts.map +1 -1
  3. package/lib/ObservedCall.js +0 -7
  4. package/lib/ObservedClient.d.ts +36 -6
  5. package/lib/ObservedClient.d.ts.map +1 -1
  6. package/lib/ObservedClient.js +129 -71
  7. package/lib/ObservedICE.d.ts +47 -0
  8. package/lib/ObservedICE.d.ts.map +1 -0
  9. package/lib/ObservedICE.js +105 -0
  10. package/lib/ObservedInboundTrack.d.ts +16 -0
  11. package/lib/ObservedInboundTrack.d.ts.map +1 -1
  12. package/lib/ObservedInboundTrack.js +39 -2
  13. package/lib/ObservedPeerConnection.d.ts +39 -1
  14. package/lib/ObservedPeerConnection.d.ts.map +1 -1
  15. package/lib/ObservedPeerConnection.js +61 -0
  16. package/lib/Observer.d.ts +1 -4
  17. package/lib/Observer.d.ts.map +1 -1
  18. package/lib/Observer.js +1 -0
  19. package/lib/common/CallEventType.d.ts +22 -0
  20. package/lib/common/CallEventType.d.ts.map +1 -0
  21. package/lib/common/CallEventType.js +26 -0
  22. package/lib/common/callEventReports.d.ts +0 -10
  23. package/lib/common/callEventReports.d.ts.map +1 -1
  24. package/lib/common/callEventReports.js +10 -21
  25. package/lib/models/Models.d.ts +3 -70
  26. package/lib/models/Models.d.ts.map +1 -1
  27. package/lib/models/Models.js +2 -72
  28. package/lib/models/samples_pb.js +1 -1
  29. package/package.json +1 -1
  30. package/src/ObservedCall.ts +3 -29
  31. package/src/ObservedClient.ts +168 -84
  32. package/src/ObservedICE.ts +176 -0
  33. package/src/ObservedInboundTrack.ts +50 -1
  34. package/src/ObservedPeerConnection.ts +79 -2
  35. package/src/Observer.ts +4 -5
  36. package/src/common/CallEventType.ts +23 -0
  37. package/src/common/callEventReports.ts +1 -11
  38. package/src/models/Models.ts +4 -113
  39. package/src/models/samples_pb.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { ClientSample, IceLocalCandidate } from '@observertc/sample-schemas-js';
1
+ import { ClientSample } from '@observertc/sample-schemas-js';
2
2
  import { ObservedCall } from './ObservedCall';
3
3
  import * as Models from './models/Models';
4
4
  import { StorageProvider } from './storages/StorageProvider';
@@ -9,8 +9,9 @@ import { createLogger } from './common/logger';
9
9
  import { CallMetaType, createCallMetaReport } from './common/callMetaReports';
10
10
  // eslint-disable-next-line camelcase
11
11
  import { Samples_ClientSample_Browser, Samples_ClientSample_Engine, Samples_ClientSample_OperationSystem, Samples_ClientSample_Platform } from './models/samples_pb';
12
- import { IceCandidatePairReport } from '@observertc/report-schemas-js';
13
12
  import { isValidUuid } from './common/utils';
13
+ import { createClientJoinedEventReport, createClientLeftEventReport } from './common/callEventReports';
14
+ import { CallEventType } from './common/CallEventType';
14
15
 
15
16
  const logger = createLogger('ObservedClient');
16
17
 
@@ -21,13 +22,17 @@ export type ObservedClientConfig<AppData extends Record<string, unknown> = Recor
21
22
  appData: AppData;
22
23
  overflowingProcessingThreshold?: number;
23
24
  joined?: number,
25
+
26
+ /**
27
+ * before client-monitor-js 3.4.0 the client joined event was generated by the observer, this flag is to keep the compatibility with the old behavior
28
+ */
29
+ addJoinAndDetachClientEvent?: boolean;
24
30
  };
25
31
 
26
32
  export type ObservedClientEvents = {
27
33
  update: [],
28
34
  close: [],
29
35
  'processing-sample-overflow': [],
30
- 'candidate-pair-change': [],
31
36
  newpeerconnection: [ObservedPeerConnection],
32
37
  };
33
38
 
@@ -65,6 +70,21 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
65
70
 
66
71
  result._overflowingProcessingThreshold = config.overflowingProcessingThreshold ?? 0;
67
72
 
73
+ if (config.addJoinAndDetachClientEvent) {
74
+ call.reports.addCallEventReport(createClientJoinedEventReport(
75
+ call.serviceId,
76
+ result.mediaUnitId,
77
+ call.roomId,
78
+ call.callId,
79
+ result.clientId,
80
+ result.joined,
81
+ result.userId,
82
+ result.marker,
83
+ ));
84
+ }
85
+
86
+ result.once('close', () => result._addClientLeftReport());
87
+
68
88
  return result;
69
89
  }
70
90
 
@@ -76,9 +96,39 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
76
96
  private _updated = Date.now();
77
97
  private _acceptedSample = 0;
78
98
  private _processingSample = 0;
79
- private _iceLocalCandidate?: IceLocalCandidate;
80
- private _iceRemoteCandidate?: IceLocalCandidate;
81
-
99
+ private _timeZoneOffsetInHours?: number;
100
+ private _leaveReported = false;
101
+
102
+ public totalInboundPacketsLost = 0;
103
+ public totalInboundPacketsReceived = 0;
104
+ public totalOutboundPacketsLost = 0;
105
+ public totalOutboundPacketsReceived = 0;
106
+ public totalOutboundPacketsSent = 0;
107
+ public totalDataChannelBytesSent = 0;
108
+ public totalDataChannelBytesReceived = 0;
109
+ public totalSentAudioBytes = 0;
110
+ public totalSentVideoBytes = 0;
111
+ public totalReceivedAudioBytes = 0;
112
+ public totalReceivedVideoBytes = 0;
113
+
114
+ public deltaInboundPacketsLost?: number;
115
+ public deltaInboundPacketsReceived?: number;
116
+ public deltaOutboundPacketsLost?: number;
117
+ public deltaOutboundPacketsReceived?: number;
118
+ public deltaOutboundPacketsSent?: number;
119
+ public deltaDataChannelBytesSent?: number;
120
+ public deltaDataChannelBytesReceived?: number;
121
+ public deltaReceivedAudioBytes?: number;
122
+ public deltaReceivedVideoBytes?: number;
123
+ public deltaSentAudioBytes?: number;
124
+ public deltaSentVideoBytes?: number;
125
+
126
+ public avgRttInS?: number;
127
+ public sendingAudioBitrate?: number;
128
+ public sendingVideoBitrate?: number;
129
+ public receivingAudioBitrate?: number;
130
+ public receivingVideoBitrate?: number;
131
+
82
132
  public readonly mediaDevices: string[] = [];
83
133
  public readonly codecs: string[] = [];
84
134
  public readonly userMediaErrors: string[] = [];
@@ -130,12 +180,8 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
130
180
  return this._model.userId;
131
181
  }
132
182
 
133
- public get iceLocalCandidate() {
134
- return this._iceLocalCandidate;
135
- }
136
-
137
- public get iceRemoteCandidate() {
138
- return this._iceRemoteCandidate;
183
+ public get timeZoneOffsetInHours() {
184
+ return this._timeZoneOffsetInHours;
139
185
  }
140
186
 
141
187
  public get marker() {
@@ -209,6 +255,11 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
209
255
  executeSave = true;
210
256
  }
211
257
 
258
+ if (this._timeZoneOffsetInHours !== sample.timeZoneOffsetInHours) {
259
+ this._timeZoneOffsetInHours = sample.timeZoneOffsetInHours;
260
+ executeSave = true;
261
+ }
262
+
212
263
  if (sample.os && (
213
264
  this._model.operationSystem?.name !== sample.os.name ||
214
265
  this._model.operationSystem?.version !== sample.os.version ||
@@ -348,6 +399,23 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
348
399
  }
349
400
 
350
401
  for (const { timestamp = Date.now(), ...callEvent } of sample.customCallEvents ?? []) {
402
+ const peerConnection = this._peerConnections.get(callEvent.peerConnectionId ?? '');
403
+
404
+ switch (callEvent.name) {
405
+ case CallEventType.CLIENT_LEFT:
406
+ this._leaveReported = true;
407
+ break;
408
+ case CallEventType.ICE_CONNECTION_STATE_CHANGED:
409
+ peerConnection && callEvent.value && peerConnection.emit('iceconnectionstatechange', callEvent.value);
410
+ break;
411
+ case CallEventType.ICE_GATHERING_STATE_CHANGED:
412
+ peerConnection && callEvent.value && peerConnection.emit('icegatheringstatechange', callEvent.value);
413
+ break;
414
+ case CallEventType.PEER_CONNECTION_STATE_CHANGED:
415
+ peerConnection && callEvent.value && peerConnection.emit('connectionstatechange', callEvent.value);
416
+ break;
417
+ }
418
+
351
419
  this.reports.addCallEventReport({
352
420
  serviceId: this.serviceId,
353
421
  mediaUnitId: this.mediaUnitId,
@@ -446,75 +514,6 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
446
514
 
447
515
  this.reports.addCallMetaReport(callMetaReport);
448
516
  }
449
- let selectedLocalCandidateId: string | undefined;
450
- let selectedRemoteCandidateId: string | undefined;
451
-
452
- for (const candidatePair of sample.iceCandidatePairs ?? []) {
453
- if (candidatePair.nominated) {
454
- if (!selectedLocalCandidateId) selectedLocalCandidateId = candidatePair.localCandidateId;
455
- if (!selectedRemoteCandidateId) selectedRemoteCandidateId = candidatePair.remoteCandidateId;
456
- }
457
-
458
- const report: IceCandidatePairReport = {
459
- serviceId: this.serviceId,
460
- mediaUnitId: this.mediaUnitId,
461
- roomId: this.roomId,
462
- callId: this.callId,
463
- clientId: this.clientId,
464
- userId: this.userId,
465
- timestamp: sample.timestamp,
466
- ...candidatePair,
467
- sampleSeq: -1, // deprecated
468
- };
469
-
470
- this.reports.addIceCandidatePairReport(report);
471
- }
472
-
473
- let changeIceCandidatePair = false;
474
-
475
- for (const iceLocalCandidate of sample.iceLocalCandidates ?? []) {
476
- if (
477
- iceLocalCandidate.id && selectedLocalCandidateId &&
478
- iceLocalCandidate.id === selectedLocalCandidateId &&
479
- this._iceLocalCandidate?.id !== iceLocalCandidate.id
480
- ) {
481
- this._iceLocalCandidate = iceLocalCandidate;
482
- changeIceCandidatePair = true;
483
- }
484
- const callMetaReport = createCallMetaReport(
485
- this.serviceId,
486
- this.mediaUnitId,
487
- this.roomId,
488
- this.callId,
489
- this.clientId, {
490
- type: CallMetaType.ICE_LOCAL_CANDIDATE,
491
- payload: iceLocalCandidate,
492
- }, this.userId);
493
-
494
- this.reports.addCallMetaReport(callMetaReport);
495
- }
496
-
497
- for (const iceRemoteCandidate of sample.iceRemoteCandidates ?? []) {
498
- if (
499
- iceRemoteCandidate.id && selectedRemoteCandidateId &&
500
- iceRemoteCandidate.id === selectedRemoteCandidateId &&
501
- this._iceRemoteCandidate?.id !== iceRemoteCandidate.id
502
- ) {
503
- this._iceRemoteCandidate = iceRemoteCandidate;
504
- changeIceCandidatePair = true;
505
- }
506
- const callMetaReport = createCallMetaReport(
507
- this.serviceId,
508
- this.mediaUnitId,
509
- this.roomId,
510
- this.callId,
511
- this.clientId, {
512
- type: CallMetaType.ICE_REMOTE_CANDIDATE,
513
- payload: iceRemoteCandidate,
514
- }, this.userId);
515
-
516
- this.reports.addCallMetaReport(callMetaReport);
517
- }
518
517
 
519
518
  for (const transport of sample.pcTransports ?? []) {
520
519
  try {
@@ -640,9 +639,78 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
640
639
  logger.error(`Error creating outbound video track: ${(err as Error)?.message}`);
641
640
  }
642
641
  }
643
-
644
- if (changeIceCandidatePair) {
645
- this.emit('candidate-pair-change');
642
+
643
+ for (const iceLocalCandidate of sample.iceLocalCandidates ?? []) {
644
+ if (!iceLocalCandidate.peerConnectionId) {
645
+ logger.warn(`Local ice candidate without peerConnectionId: ${JSON.stringify(iceLocalCandidate)}`);
646
+ continue;
647
+ }
648
+ const peerConnection = this._peerConnections.get(iceLocalCandidate.peerConnectionId);
649
+
650
+ if (!peerConnection) {
651
+ logger.debug(`Peer connection ${iceLocalCandidate.peerConnectionId} not found for ice local candidate ${iceLocalCandidate.id}`);
652
+ continue;
653
+ }
654
+
655
+ peerConnection.ICE.addLocalCandidate(iceLocalCandidate);
656
+ }
657
+
658
+ for (const iceRemoteCandidate of sample.iceRemoteCandidates ?? []) {
659
+ if (!iceRemoteCandidate.peerConnectionId) {
660
+ logger.warn(`Remote ice candidate without peerConnectionId: ${JSON.stringify(iceRemoteCandidate)}`);
661
+ continue;
662
+ }
663
+ const peerConnection = this._peerConnections.get(iceRemoteCandidate.peerConnectionId);
664
+
665
+ if (!peerConnection) {
666
+ logger.debug(`Peer connection ${iceRemoteCandidate.peerConnectionId} not found for ice remote candidate ${iceRemoteCandidate.id}`);
667
+ continue;
668
+ }
669
+
670
+ peerConnection.ICE.addRemoteCandidate(iceRemoteCandidate);
671
+ }
672
+
673
+ for (const candidatePair of sample.iceCandidatePairs ?? []) {
674
+ const peerConnection = this._peerConnections.get(candidatePair.peerConnectionId);
675
+
676
+ if (!peerConnection) {
677
+ logger.debug(`Peer connection ${candidatePair.peerConnectionId} not found for ice candidate pair ${candidatePair.localCandidateId}, ${candidatePair.remoteCandidateId}`);
678
+
679
+ continue;
680
+ }
681
+
682
+ peerConnection.ICE.update(candidatePair, sample.timestamp);
683
+ }
684
+
685
+ // close entries that are idle for too long
686
+ if (this.call.observer.config.maxEntryIdleTimeInMs && this._updated < sample.timestamp) {
687
+
688
+ for (const peerConnection of this._peerConnections.values()) {
689
+ if (this.call.observer.config.maxEntryIdleTimeInMs < sample.timestamp - peerConnection.updated) {
690
+ logger.debug(`Closing peer connection ${peerConnection.peerConnectionId} due to inactivity`);
691
+ peerConnection.close();
692
+
693
+ continue;
694
+ }
695
+ for (const track of peerConnection.inboundAudioTracks.values()) {
696
+ if (this.call.observer.config.maxEntryIdleTimeInMs < sample.timestamp - track.updated) {
697
+ logger.debug(`Closing inbound audio track ${track.trackId} due to inactivity`);
698
+ track.close();
699
+ }
700
+ }
701
+ for (const track of peerConnection.inboundVideoTracks.values()) {
702
+ if (this.call.observer.config.maxEntryIdleTimeInMs < sample.timestamp - track.updated) {
703
+ logger.debug(`Closing inbound video track ${track.trackId} due to inactivity`);
704
+ track.close();
705
+ }
706
+ }
707
+ for (const track of peerConnection.outboundAudioTracks.values()) {
708
+ if (this.call.observer.config.maxEntryIdleTimeInMs < sample.timestamp - track.updated) {
709
+ logger.debug(`Closing outbound audio track ${track.trackId} due to inactivity`);
710
+ track.close();
711
+ }
712
+ }
713
+ }
646
714
  }
647
715
 
648
716
  if (executeSave) await this._save();
@@ -678,4 +746,20 @@ export class ObservedClient<AppData extends Record<string, unknown> = Record<str
678
746
 
679
747
  return this._storageProvider.clientStorage.set(this.clientId, this._model);
680
748
  }
749
+
750
+ private _addClientLeftReport() {
751
+ if (this._leaveReported) return;
752
+ this._leaveReported = true;
753
+
754
+ this.reports.addCallEventReport(createClientLeftEventReport(
755
+ this.serviceId,
756
+ this.mediaUnitId,
757
+ this.roomId,
758
+ this.callId,
759
+ this.clientId,
760
+ this.joined,
761
+ this.userId,
762
+ this.marker,
763
+ ));
764
+ }
681
765
  }
@@ -0,0 +1,176 @@
1
+ import { IceCandidatePair, IceLocalCandidate, IceRemoteCandidate } from '@observertc/sample-schemas-js';
2
+ import { EventEmitter } from 'events';
3
+ import { ObservedPeerConnection } from './ObservedPeerConnection';
4
+ import { IceCandidatePairReport } from '@observertc/report-schemas-js';
5
+ import { CallMetaType, createCallMetaReport } from './common/callMetaReports';
6
+
7
+ export type ObservedICEEvents = {
8
+ update: [],
9
+ 'new-local-candidate': [IceLocalCandidate],
10
+ 'new-remote-candidate': [IceRemoteCandidate],
11
+ 'new-selected-candidate-pair': [{
12
+ localCandidate: IceLocalCandidate,
13
+ remoteCandidate: IceRemoteCandidate,
14
+ }],
15
+ close: [],
16
+ };
17
+
18
+ export declare interface ObservedICE {
19
+ on<U extends keyof ObservedICEEvents>(event: U, listener: (...args: ObservedICEEvents[U]) => void): this;
20
+ off<U extends keyof ObservedICEEvents>(event: U, listener: (...args: ObservedICEEvents[U]) => void): this;
21
+ once<U extends keyof ObservedICEEvents>(event: U, listener: (...args: ObservedICEEvents[U]) => void): this;
22
+ emit<U extends keyof ObservedICEEvents>(event: U, ...args: ObservedICEEvents[U]): boolean;
23
+ }
24
+
25
+ export class ObservedICE extends EventEmitter {
26
+ public static create(peerConnection: ObservedPeerConnection,) {
27
+ return new ObservedICE(peerConnection);
28
+ }
29
+
30
+ private readonly _localCandidates = new Map<string, IceLocalCandidate>();
31
+ private readonly _remoteCandidates = new Map<string, IceLocalCandidate>();
32
+
33
+ private _selectedLocalCandidateId?: string;
34
+ private _selectedRemoteCandidateId?: string;
35
+ private _updated = Date.now();
36
+ private _stats?: IceCandidatePair;
37
+ private _closed = false;
38
+
39
+ private constructor(
40
+ public readonly peerConnection: ObservedPeerConnection,
41
+ ) {
42
+ super();
43
+ }
44
+
45
+ public get reports() {
46
+ return this.peerConnection.reports;
47
+ }
48
+
49
+ public get localCandidates(): ReadonlyMap<string, IceLocalCandidate> {
50
+ return this._localCandidates;
51
+ }
52
+
53
+ public get remoteCandidates(): ReadonlyMap<string, IceRemoteCandidate> {
54
+ return this._remoteCandidates;
55
+ }
56
+
57
+ public get selectedLocalCandidate() {
58
+ return this._localCandidates.get(this._selectedLocalCandidateId ?? '');
59
+ }
60
+
61
+ public get selectedRemoteCandidate() {
62
+ return this._remoteCandidates.get(this._selectedRemoteCandidateId ?? '');
63
+ }
64
+
65
+ public get stats() {
66
+ return this._stats;
67
+ }
68
+
69
+ public get updated() {
70
+ return this._updated;
71
+ }
72
+
73
+ public get closed() {
74
+ return this._closed;
75
+ }
76
+
77
+ public close() {
78
+ if (this._closed) return;
79
+ this._closed = true;
80
+ this.emit('close');
81
+ }
82
+
83
+ public addLocalCandidate(candidate: IceLocalCandidate) {
84
+ if (!candidate.id) return;
85
+
86
+ const newCandidate = !this._localCandidates.has(candidate.id);
87
+
88
+ this._localCandidates.set(candidate.id, candidate);
89
+
90
+ if (newCandidate) {
91
+ const callMetaReport = createCallMetaReport(
92
+ this.peerConnection.client.serviceId,
93
+ this.peerConnection.client.mediaUnitId,
94
+ this.peerConnection.client.roomId,
95
+ this.peerConnection.client.callId,
96
+ this.peerConnection.client.clientId, {
97
+ type: CallMetaType.ICE_LOCAL_CANDIDATE,
98
+ payload: candidate,
99
+ }, this.peerConnection.client.userId);
100
+
101
+ this.reports.addCallMetaReport(callMetaReport);
102
+
103
+ this.emit('new-local-candidate', candidate);
104
+ }
105
+ }
106
+
107
+ public addRemoteCandidate(candidate: IceRemoteCandidate) {
108
+ if (!candidate.id) return;
109
+
110
+ const newCandidate = !this._remoteCandidates.has(candidate.id);
111
+
112
+ this._remoteCandidates.set(candidate.id, candidate);
113
+
114
+ if (newCandidate) {
115
+ const callMetaReport = createCallMetaReport(
116
+ this.peerConnection.client.serviceId,
117
+ this.peerConnection.client.mediaUnitId,
118
+ this.peerConnection.client.roomId,
119
+ this.peerConnection.client.callId,
120
+ this.peerConnection.client.clientId, {
121
+ type: CallMetaType.ICE_REMOTE_CANDIDATE,
122
+ payload: candidate,
123
+ }, this.peerConnection.client.userId);
124
+
125
+ this.reports.addCallMetaReport(callMetaReport);
126
+
127
+ this.emit('new-remote-candidate', candidate);
128
+ }
129
+ }
130
+
131
+ public update(candidatePair: IceCandidatePair, timestamp: number) {
132
+
133
+ const report: IceCandidatePairReport = {
134
+ serviceId: this.peerConnection.client.serviceId,
135
+ mediaUnitId: this.peerConnection.client.mediaUnitId,
136
+ roomId: this.peerConnection.client.roomId,
137
+ callId: this.peerConnection.client.callId,
138
+ clientId: this.peerConnection.client.clientId,
139
+ userId: this.peerConnection.client.userId,
140
+ timestamp,
141
+ ...candidatePair,
142
+ sampleSeq: -1, // deprecated
143
+ };
144
+
145
+ this.reports.addIceCandidatePairReport(report);
146
+
147
+ if (!candidatePair.nominated) return;
148
+
149
+ this._stats = candidatePair;
150
+
151
+ if (
152
+ candidatePair.localCandidateId &&
153
+ candidatePair.localCandidateId !== this._selectedLocalCandidateId &&
154
+ this._localCandidates.has(candidatePair.localCandidateId) &&
155
+ candidatePair.remoteCandidateId &&
156
+ candidatePair.remoteCandidateId !== this._selectedRemoteCandidateId &&
157
+ this._remoteCandidates.has(candidatePair.remoteCandidateId)
158
+ ) {
159
+ this._selectedLocalCandidateId = candidatePair.localCandidateId;
160
+ this._selectedRemoteCandidateId = candidatePair.remoteCandidateId;
161
+
162
+ const localCandidate = this._localCandidates.get(candidatePair.localCandidateId);
163
+ const remoteCandidate = this._remoteCandidates.get(candidatePair.remoteCandidateId);
164
+
165
+ if (localCandidate && remoteCandidate) {
166
+ this.emit('new-selected-candidate-pair', {
167
+ localCandidate,
168
+ remoteCandidate,
169
+ });
170
+ }
171
+ }
172
+
173
+ this._updated = timestamp;
174
+ this.emit('update');
175
+ }
176
+ }
@@ -28,6 +28,14 @@ export type ObservedInboundTrackStats<K extends MediaKind> = ObservedInboundTrac
28
28
  bitrate: number;
29
29
  fractionLost: number;
30
30
  rttInMs?: number;
31
+ lostPackets?: number;
32
+ receivedPackets?: number;
33
+ receivedFrames?: number;
34
+ decodedFrames?: number;
35
+ droppedFrames?: number;
36
+ receivedSamples?: number;
37
+ silentConcealedSamples?: number;
38
+ fractionLoss?: number;
31
39
  };
32
40
 
33
41
  // {
@@ -76,6 +84,14 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
76
84
  private _remoteOutboundTrack?: ObservedOutboundTrack<Kind>;
77
85
  public bitrate = -1;
78
86
  public rttInMs = -1;
87
+ public lostPackets = 0;
88
+ public receivedPackets = 0;
89
+ public receivedFrames = 0;
90
+ public decodedFrames = 0;
91
+ public droppedFrames = 0;
92
+ public receivedSamples = 0;
93
+ public silentConcealedSamples = 0;
94
+ public fractionLoss = 0;
79
95
 
80
96
  private constructor(
81
97
  private readonly _model: Models.InboundTrack,
@@ -211,19 +227,52 @@ export class ObservedInboundTrack<Kind extends MediaKind> extends EventEmitter {
211
227
  const lostPackets = (sample.packetsLost ?? 0) - (lastStat?.packetsLost ?? 0);
212
228
  const receivedPackets = (sample.packetsReceived ?? 0) - (lastStat?.packetsReceived ?? 0);
213
229
  const fractionLost = 0 < receivedPackets && 0 < lostPackets ? (lostPackets / (receivedPackets + lostPackets)) : 0;
230
+ let decodedFrames: number | undefined;
231
+ let droppedFrames: number | undefined;
232
+ let receivedFrames: number | undefined;
233
+ let silentConcealedSamples: number | undefined;
234
+
235
+ if (this.kind === 'video') {
236
+ const videoSample = sample as InboundVideoTrack;
237
+ const lastVideoStat = lastStat as InboundVideoTrack | undefined;
238
+
239
+ decodedFrames = (videoSample.framesDecoded ?? 0) - (lastVideoStat?.framesDecoded ?? 0);
240
+ droppedFrames = (videoSample.framesDropped ?? 0) - (lastVideoStat?.framesDropped ?? 0);
241
+ receivedFrames = (videoSample.framesReceived ?? 0) - (lastVideoStat?.framesReceived ?? 0);
242
+ } else if (this.kind === 'audio') {
243
+ const audioSample = sample as InboundAudioTrack;
244
+ const lastAudioStat = lastStat as InboundAudioTrack | undefined;
245
+
246
+ silentConcealedSamples = (audioSample.silentConcealedSamples ?? 0) - (lastAudioStat?.silentConcealedSamples ?? 0);
247
+ }
248
+
214
249
  const stats: ObservedInboundTrackStats<Kind> = {
215
250
  ...sample,
216
251
  fractionLost,
217
252
  rttInMs,
218
253
  bitrate,
219
254
  ssrc: sample.ssrc,
255
+ lostPackets,
256
+ receivedPackets,
257
+ receivedFrames,
258
+ decodedFrames,
259
+ droppedFrames,
260
+ silentConcealedSamples,
220
261
  };
221
262
 
222
263
  this._stats.set(sample.ssrc, stats);
223
264
 
224
265
  this.bitrate = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.bitrate, 0);
225
266
  this.rttInMs = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.rttInMs ?? 0), 0) / (this._stats.size || 1);
226
-
267
+ this.lostPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.lostPackets ?? 0), 0);
268
+ this.receivedPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.receivedPackets ?? 0), 0);
269
+ this.receivedFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.receivedFrames ?? 0), 0);
270
+ this.decodedFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.decodedFrames ?? 0), 0);
271
+ this.droppedFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.droppedFrames ?? 0), 0);
272
+ this.receivedSamples = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.receivedSamples ?? 0), 0);
273
+ this.silentConcealedSamples = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.silentConcealedSamples ?? 0), 0);
274
+ this.fractionLoss = 0 < this.receivedPackets && 0 < this.lostPackets ? (this.lostPackets / (this.receivedPackets + this.lostPackets)) : 0;
275
+
227
276
  if (executeSave) await this._save();
228
277
 
229
278
  this._updated = timestamp;