@webex/plugin-meetings 3.3.0 → 3.3.1-next.10

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 (37) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +4 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/mediaQualityMetrics/config.js +20 -16
  8. package/dist/mediaQualityMetrics/config.js.map +1 -1
  9. package/dist/meeting/index.js +30 -13
  10. package/dist/meeting/index.js.map +1 -1
  11. package/dist/meetings/index.js +6 -1
  12. package/dist/meetings/index.js.map +1 -1
  13. package/dist/reachability/index.js +82 -9
  14. package/dist/reachability/index.js.map +1 -1
  15. package/dist/statsAnalyzer/index.js +77 -27
  16. package/dist/statsAnalyzer/index.js.map +1 -1
  17. package/dist/statsAnalyzer/mqaUtil.js +46 -7
  18. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  19. package/dist/types/constants.d.ts +2 -1
  20. package/dist/types/mediaQualityMetrics/config.d.ts +14 -2
  21. package/dist/types/meeting/index.d.ts +8 -0
  22. package/dist/types/reachability/index.d.ts +11 -0
  23. package/dist/types/statsAnalyzer/index.d.ts +14 -6
  24. package/dist/types/statsAnalyzer/mqaUtil.d.ts +17 -4
  25. package/dist/webinar/index.js +1 -1
  26. package/package.json +22 -22
  27. package/src/constants.ts +2 -1
  28. package/src/mediaQualityMetrics/config.ts +22 -10
  29. package/src/meeting/index.ts +29 -14
  30. package/src/meetings/index.ts +7 -2
  31. package/src/reachability/index.ts +57 -0
  32. package/src/statsAnalyzer/index.ts +82 -22
  33. package/src/statsAnalyzer/mqaUtil.ts +68 -4
  34. package/test/unit/spec/meeting/index.js +28 -8
  35. package/test/unit/spec/meetings/index.js +38 -15
  36. package/test/unit/spec/reachability/index.ts +266 -0
  37. package/test/unit/spec/stats-analyzer/index.js +630 -314
@@ -16,6 +16,14 @@ const {assert} = chai;
16
16
  chai.use(chaiAsPromised);
17
17
  sinon.assert.expose(chai.assert, {prefix: ''});
18
18
 
19
+ const startStatsAnalyzer = async ({statsAnalyzer, mediaStatus, lastEmittedEvents = {}, pc}) => {
20
+ statsAnalyzer.updateMediaStatus(mediaStatus);
21
+ statsAnalyzer.startAnalyzer(pc);
22
+ statsAnalyzer.lastEmittedStartStopEvent = lastEmittedEvents;
23
+
24
+ await testUtils.flushPromises();
25
+ };
26
+
19
27
  describe('plugin-meetings', () => {
20
28
  describe('StatsAnalyzer', () => {
21
29
  describe('parseStatsResult', () => {
@@ -29,10 +37,12 @@ describe('plugin-meetings', () => {
29
37
  const networkQualityMonitor = new NetworkQualityMonitor(initialConfig);
30
38
 
31
39
  statsAnalyzer = new StatsAnalyzer(
32
- initialConfig,
33
- () => ({}),
34
- networkQualityMonitor,
35
- defaultStats
40
+ {
41
+ config: initialConfig,
42
+ receiveSlotCallback: () => ({}),
43
+ networkQualityMonitor,
44
+ statsResults: defaultStats,
45
+ },
36
46
  );
37
47
  });
38
48
 
@@ -64,7 +74,7 @@ describe('plugin-meetings', () => {
64
74
  assert(calledSpy.calledOnce);
65
75
  });
66
76
 
67
- it('processOutboundRTPResult should create the correct stats results', () => {
77
+ it('processOutboundRTPResult should create the correct stats results for audio', () => {
68
78
  // establish the `statsResults` object.
69
79
  statsAnalyzer.parseGetStatsResult({type: 'none'}, 'audio-send', true);
70
80
 
@@ -80,8 +90,6 @@ describe('plugin-meetings', () => {
80
90
  nackCount: 1,
81
91
  packetsSent: 3600,
82
92
  remoteId: 'RTCRemoteInboundRtpAudioStream_123456789',
83
- retransmittedBytesSent: 100,
84
- retransmittedPacketsSent: 2,
85
93
  ssrc: 123456789,
86
94
  targetBitrate: 256000,
87
95
  timestamp: 1707341489336,
@@ -91,7 +99,7 @@ describe('plugin-meetings', () => {
91
99
  requestedBitrate: 10000,
92
100
  },
93
101
  'audio-send',
94
- true
102
+ true,
95
103
  );
96
104
 
97
105
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.headerBytesSent, 25000);
@@ -99,17 +107,54 @@ describe('plugin-meetings', () => {
99
107
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.totalNackCount, 1);
100
108
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.totalPacketsSent, 3600);
101
109
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.requestedBitrate, 10000);
110
+ });
111
+
112
+ it('processOutboundRTPResult should create the correct stats results for video', () => {
113
+ // establish the `statsResults` object for video.
114
+ statsAnalyzer.parseGetStatsResult({type: 'none'}, 'video-send', true);
115
+
116
+ statsAnalyzer.processOutboundRTPResult(
117
+ {
118
+ bytesSent: 250000,
119
+ codecId: 'RTCCodec_1_Outbound_107',
120
+ headerBytesSent: 50000,
121
+ id: 'RTCOutboundRTPVideoStream_987654321',
122
+ kind: 'video',
123
+ mediaSourceId: 'RTCVideoSource_3',
124
+ mediaType: 'video',
125
+ nackCount: 5,
126
+ packetsSent: 15000,
127
+ remoteId: 'RTCRemoteInboundRtpVideoStream_987654321',
128
+ retransmittedBytesSent: 500,
129
+ retransmittedPacketsSent: 10,
130
+ ssrc: 987654321,
131
+ targetBitrate: 1024000,
132
+ timestamp: 1707341489336,
133
+ trackId: 'RTCMediaStreamTrack_sender_3',
134
+ transportId: 'RTCTransport_0_2',
135
+ type: 'outbound-rtp',
136
+ requestedBitrate: 50000,
137
+ },
138
+ 'video-send',
139
+ true,
140
+ );
141
+
142
+ assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.headerBytesSent, 50000);
143
+ assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.totalBytesSent, 250000);
144
+ assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.totalNackCount, 5);
145
+ assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.totalPacketsSent, 15000);
146
+ assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.requestedBitrate, 50000);
102
147
  assert.strictEqual(
103
- statsAnalyzer.statsResults['audio-send'].send.retransmittedPacketsSent,
104
- 2
148
+ statsAnalyzer.statsResults['video-send'].send.totalRtxPacketsSent,
149
+ 10,
105
150
  );
106
151
  assert.strictEqual(
107
- statsAnalyzer.statsResults['audio-send'].send.retransmittedBytesSent,
108
- 100
152
+ statsAnalyzer.statsResults['video-send'].send.totalRtxBytesSent,
153
+ 500,
109
154
  );
110
155
  });
111
156
 
112
- it('processInboundRTPResult should create the correct stats results', () => {
157
+ it('processInboundRTPResult should create the correct stats results for audio', () => {
113
158
  // establish the `statsResults` object.
114
159
  statsAnalyzer.parseGetStatsResult({type: 'none'}, 'audio-recv-1', false);
115
160
 
@@ -148,12 +193,12 @@ describe('plugin-meetings', () => {
148
193
  requestedBitrate: 10000,
149
194
  },
150
195
  'audio-recv-1',
151
- false
196
+ false,
152
197
  );
153
198
 
154
199
  assert.strictEqual(
155
200
  statsAnalyzer.statsResults['audio-recv-1'].recv.totalPacketsReceived,
156
- 12
201
+ 12,
157
202
  );
158
203
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.fecPacketsDiscarded, 1);
159
204
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.fecPacketsReceived, 1);
@@ -161,21 +206,67 @@ describe('plugin-meetings', () => {
161
206
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.requestedBitrate, 10000);
162
207
  assert.strictEqual(
163
208
  statsAnalyzer.statsResults['audio-recv-1'].recv.headerBytesReceived,
164
- 250
209
+ 250,
165
210
  );
166
211
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.audioLevel, 0);
167
212
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalAudioEnergy, 133);
168
213
  assert.strictEqual(
169
214
  statsAnalyzer.statsResults['audio-recv-1'].recv.totalSamplesReceived,
170
- 300000
215
+ 300000,
171
216
  );
172
217
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalSamplesDecoded, 0);
173
218
  assert.strictEqual(
174
219
  statsAnalyzer.statsResults['audio-recv-1'].recv.concealedSamples,
175
- 200000
220
+ 200000,
221
+ );
222
+ });
223
+
224
+ it('processInboundRTPResult should create the correct stats results for video', () => {
225
+ // establish the `statsResults` object for video.
226
+ statsAnalyzer.parseGetStatsResult({type: 'none'}, 'video-recv', false);
227
+
228
+ statsAnalyzer.processInboundRTPResult(
229
+ {
230
+ bytesReceived: 100000,
231
+ codecId: 'RTCCodec_6_Inbound_107',
232
+ fecPacketsDiscarded: 2,
233
+ fecPacketsReceived: 2,
234
+ headerBytesReceived: 10000,
235
+ id: 'RTCInboundRTPVideoStream_987654321',
236
+ jitter: 0.05,
237
+ jitterBufferDelay: 5000,
238
+ jitterBufferEmittedCount: 50000,
239
+ kind: 'video',
240
+ lastPacketReceivedTimestamp: 1707341488529,
241
+ mediaType: 'video',
242
+ packetsDiscarded: 5,
243
+ packetsLost: 10,
244
+ packetsReceived: 1500,
245
+ remoteId: 'RTCRemoteOutboundRTPVideoStream_987654321',
246
+ ssrc: 987654321,
247
+ timestamp: 1707341489419,
248
+ trackId: 'RTCMediaStreamTrack_receiver_3',
249
+ transportId: 'RTCTransport_0_2',
250
+ type: 'inbound-rtp',
251
+ requestedBitrate: 50000,
252
+ retransmittedBytesReceived: 500,
253
+ retransmittedPacketsReceived: 10,
254
+ },
255
+ 'video-recv',
256
+ false,
176
257
  );
258
+
259
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalPacketsReceived, 1500);
260
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.fecPacketsDiscarded, 2);
261
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.fecPacketsReceived, 2);
262
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalBytesReceived, 100000);
263
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.requestedBitrate, 50000);
264
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.headerBytesReceived, 10000);
265
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalRtxBytesReceived, 500);
266
+ assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalRtxPacketsReceived, 10);
177
267
  });
178
268
 
269
+
179
270
  it('parseAudioSource should create the correct stats results', () => {
180
271
  // establish the `statsResults` object.
181
272
  statsAnalyzer.parseGetStatsResult({type: 'none'}, 'audio-send', true);
@@ -194,7 +285,7 @@ describe('plugin-meetings', () => {
194
285
  type: 'media-source',
195
286
  },
196
287
  'audio-send',
197
- true
288
+ true,
198
289
  );
199
290
 
200
291
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.audioLevel, 0.03);
@@ -241,15 +332,17 @@ describe('plugin-meetings', () => {
241
332
  const networkQualityMonitor = new NetworkQualityMonitor(initialConfig);
242
333
 
243
334
  statsAnalyzer = new StatsAnalyzer(
244
- initialConfig,
245
- () => ({}),
246
- networkQualityMonitor,
247
- defaultStats
335
+ {
336
+ config: initialConfig,
337
+ receiveSlotCallback: () => ({}),
338
+ networkQualityMonitor,
339
+ statsResults: defaultStats,
340
+ },
248
341
  );
249
342
 
250
343
  sandBoxSpy = sandbox.spy(
251
344
  statsAnalyzer.networkQualityMonitor,
252
- 'determineUplinkNetworkQuality'
345
+ 'determineUplinkNetworkQuality',
253
346
  );
254
347
  });
255
348
 
@@ -266,7 +359,7 @@ describe('plugin-meetings', () => {
266
359
  mediaType: 'video-send-1',
267
360
  remoteRtpResults: statusResult,
268
361
  statsAnalyzerCurrentStats: statsAnalyzer.statsResults,
269
- })
362
+ }),
270
363
  );
271
364
  });
272
365
  });
@@ -300,6 +393,24 @@ describe('plugin-meetings', () => {
300
393
  };
301
394
  };
302
395
 
396
+ const registerStatsAnalyzerEvents = (statsAnalyzer) => {
397
+ statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STARTED, (data) => {
398
+ receivedEventsData.local.started = data;
399
+ });
400
+ statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STOPPED, (data) => {
401
+ receivedEventsData.local.stopped = data;
402
+ });
403
+ statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STARTED, (data) => {
404
+ receivedEventsData.remote.started = data;
405
+ });
406
+ statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STOPPED, (data) => {
407
+ receivedEventsData.remote.stopped = data;
408
+ });
409
+ statsAnalyzer.on(EVENTS.MEDIA_QUALITY, ({data}) => {
410
+ mqeData = data;
411
+ });
412
+ };
413
+
303
414
  before(() => {
304
415
  LoggerConfig.set({enable: false});
305
416
  LoggerProxy.set();
@@ -323,6 +434,7 @@ describe('plugin-meetings', () => {
323
434
  type: 'outbound-rtp',
324
435
  bytesSent: 1,
325
436
  packetsSent: 0,
437
+ isRequested: true,
326
438
  },
327
439
  {
328
440
  type: 'remote-inbound-rtp',
@@ -356,6 +468,8 @@ describe('plugin-meetings', () => {
356
468
  fecPacketsReceived: 0,
357
469
  packetsLost: 0,
358
470
  packetsReceived: 0,
471
+ isRequested: true,
472
+ lastRequestedUpdateTimestamp: 0,
359
473
  },
360
474
  {
361
475
  type: 'remote-outbound-rtp',
@@ -389,6 +503,8 @@ describe('plugin-meetings', () => {
389
503
  bytesSent: 1,
390
504
  framesSent: 0,
391
505
  packetsSent: 0,
506
+ isRequested: true,
507
+ lastRequestedUpdateTimestamp: 0,
392
508
  },
393
509
  {
394
510
  type: 'remote-inbound-rtp',
@@ -424,6 +540,10 @@ describe('plugin-meetings', () => {
424
540
  framesReceived: 0,
425
541
  packetsLost: 0,
426
542
  packetsReceived: 0,
543
+ isRequested: true,
544
+ lastRequestedUpdateTimestamp: 0,
545
+ isActiveSpeaker: false,
546
+ lastActiveSpeakerUpdateTimestamp: 0,
427
547
  },
428
548
  {
429
549
  type: 'remote-outbound-rtp',
@@ -457,6 +577,8 @@ describe('plugin-meetings', () => {
457
577
  bytesSent: 1,
458
578
  framesSent: 0,
459
579
  packetsSent: 0,
580
+ isRequested: true,
581
+ lastRequestedUpdateTimestamp: 0,
460
582
  },
461
583
  {
462
584
  type: 'remote-inbound-rtp',
@@ -492,6 +614,8 @@ describe('plugin-meetings', () => {
492
614
  framesReceived: 0,
493
615
  packetsLost: 0,
494
616
  packetsReceived: 0,
617
+ isRequested: true,
618
+ lastRequestedUpdateTimestamp: 0,
495
619
  },
496
620
  {
497
621
  type: 'remote-outbound-rtp',
@@ -541,23 +665,11 @@ describe('plugin-meetings', () => {
541
665
 
542
666
  networkQualityMonitor = new NetworkQualityMonitor(initialConfig);
543
667
 
544
- statsAnalyzer = new StatsAnalyzer(initialConfig, () => receiveSlot, networkQualityMonitor);
545
-
546
- statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STARTED, (data) => {
547
- receivedEventsData.local.started = data;
548
- });
549
- statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STOPPED, (data) => {
550
- receivedEventsData.local.stopped = data;
551
- });
552
- statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STARTED, (data) => {
553
- receivedEventsData.remote.started = data;
554
- });
555
- statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STOPPED, (data) => {
556
- receivedEventsData.remote.stopped = data;
557
- });
558
- statsAnalyzer.on(EVENTS.MEDIA_QUALITY, ({data}) => {
559
- mqeData = data;
668
+ statsAnalyzer = new StatsAnalyzer({
669
+ config: initialConfig, receiveSlotCallback: () => receiveSlot, networkQualityMonitor,
560
670
  });
671
+
672
+ registerStatsAnalyzerEvents(statsAnalyzer);
561
673
  });
562
674
 
563
675
  afterEach(() => {
@@ -565,20 +677,12 @@ describe('plugin-meetings', () => {
565
677
  clock.restore();
566
678
  });
567
679
 
568
- const startStatsAnalyzer = async (mediaStatus, lastEmittedEvents) => {
569
- statsAnalyzer.updateMediaStatus(mediaStatus);
570
- statsAnalyzer.startAnalyzer(pc);
571
- statsAnalyzer.lastEmittedStartStopEvent = lastEmittedEvents || {};
572
-
573
- await testUtils.flushPromises();
574
- };
575
-
576
680
  const mergeProperties = (
577
681
  target,
578
682
  properties,
579
683
  keyValue = 'fake-candidate-id',
580
684
  matchKey = 'type',
581
- matchValue = 'local-candidate'
685
+ matchValue = 'local-candidate',
582
686
  ) => {
583
687
  for (let key in target) {
584
688
  if (target.hasOwnProperty(key)) {
@@ -623,7 +727,15 @@ describe('plugin-meetings', () => {
623
727
  };
624
728
 
625
729
  it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for audio', async () => {
626
- await startStatsAnalyzer({expected: {sendAudio: true}});
730
+ await startStatsAnalyzer({
731
+ statsAnalyzer,
732
+ pc,
733
+ mediaStatus: {
734
+ expected: {
735
+ sendAudio: true,
736
+ },
737
+ },
738
+ });
627
739
 
628
740
  // check that we haven't received any events yet
629
741
  checkReceivedEvent({expected: {}});
@@ -643,7 +755,7 @@ describe('plugin-meetings', () => {
643
755
  });
644
756
 
645
757
  it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for video', async () => {
646
- await startStatsAnalyzer({expected: {sendVideo: true}});
758
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {sendVideo: true}}});
647
759
 
648
760
  // check that we haven't received any events yet
649
761
  checkReceivedEvent({expected: {}});
@@ -663,7 +775,7 @@ describe('plugin-meetings', () => {
663
775
  });
664
776
 
665
777
  it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for share', async () => {
666
- await startStatsAnalyzer({expected: {sendShare: true}});
778
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {sendShare: true}}});
667
779
 
668
780
  // check that we haven't received any events yet
669
781
  checkReceivedEvent({expected: {}});
@@ -683,7 +795,7 @@ describe('plugin-meetings', () => {
683
795
  });
684
796
 
685
797
  it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for audio', async () => {
686
- await startStatsAnalyzer({expected: {receiveAudio: true}});
798
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveAudio: true}}});
687
799
 
688
800
  // check that we haven't received any events yet
689
801
  checkReceivedEvent({expected: {}});
@@ -703,7 +815,7 @@ describe('plugin-meetings', () => {
703
815
  });
704
816
 
705
817
  it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for video', async () => {
706
- await startStatsAnalyzer({expected: {receiveVideo: true}});
818
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
707
819
 
708
820
  // check that we haven't received any events yet
709
821
  checkReceivedEvent({expected: {}});
@@ -723,7 +835,7 @@ describe('plugin-meetings', () => {
723
835
  });
724
836
 
725
837
  it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for share', async () => {
726
- await startStatsAnalyzer({expected: {receiveShare: true}});
838
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveShare: true}}});
727
839
 
728
840
  // check that we haven't received any events yet
729
841
  checkReceivedEvent({expected: {}});
@@ -743,7 +855,7 @@ describe('plugin-meetings', () => {
743
855
  });
744
856
 
745
857
  it('emits the correct MEDIA_QUALITY events', async () => {
746
- await startStatsAnalyzer({expected: {receiveVideo: true}});
858
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
747
859
 
748
860
  await progressTime();
749
861
 
@@ -752,7 +864,7 @@ describe('plugin-meetings', () => {
752
864
  });
753
865
 
754
866
  it('emits the correct transportType in MEDIA_QUALITY events', async () => {
755
- await startStatsAnalyzer({expected: {receiveVideo: true}});
867
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
756
868
 
757
869
  await progressTime();
758
870
 
@@ -766,7 +878,7 @@ describe('plugin-meetings', () => {
766
878
  fakeStats.audio.receivers[0].report[4].relayProtocol = 'tls';
767
879
  fakeStats.video.receivers[0].report[4].relayProtocol = 'tls';
768
880
 
769
- await startStatsAnalyzer({expected: {receiveVideo: true}});
881
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
770
882
 
771
883
  await progressTime();
772
884
 
@@ -775,19 +887,19 @@ describe('plugin-meetings', () => {
775
887
  });
776
888
 
777
889
  it('emits the correct peripherals in MEDIA_QUALITY events', async () => {
778
- await startStatsAnalyzer({expected: {receiveVideo: true}});
890
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
779
891
 
780
892
  await progressTime();
781
893
 
782
894
  assert.strictEqual(
783
895
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.MICROPHONE)
784
896
  .information,
785
- 'fake-microphone'
897
+ 'fake-microphone',
786
898
  );
787
899
  assert.strictEqual(
788
900
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.CAMERA)
789
901
  .information,
790
- 'fake-camera'
902
+ 'fake-camera',
791
903
  );
792
904
  });
793
905
 
@@ -795,30 +907,33 @@ describe('plugin-meetings', () => {
795
907
  fakeStats.audio.senders[0].localTrackLabel = undefined;
796
908
  fakeStats.video.senders[0].localTrackLabel = undefined;
797
909
 
798
- await startStatsAnalyzer({expected: {receiveVideo: true}});
910
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
799
911
 
800
912
  await progressTime();
801
913
 
802
914
  assert.strictEqual(
803
915
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.MICROPHONE)
804
916
  .information,
805
- _UNKNOWN_
917
+ _UNKNOWN_,
806
918
  );
807
919
  assert.strictEqual(
808
920
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.CAMERA)
809
921
  .information,
810
- _UNKNOWN_
922
+ _UNKNOWN_,
811
923
  );
812
924
  });
813
925
 
814
- it('emits the correct transmittedFrameRate/receivedFrameRate', async () => {
815
- it('at the start of the stats analyzer', async () => {
816
- await startStatsAnalyzer();
926
+ describe('frame rate reporting in stats analyzer', () => {
927
+ beforeEach(async () => {
928
+ await startStatsAnalyzer({pc, statsAnalyzer});
929
+ });
930
+
931
+ it('should report a zero frame rate for both transmitted and received video at the start', async () => {
817
932
  assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.transmittedFrameRate, 0);
818
933
  assert.strictEqual(mqeData.videoReceive[0].streams[0].common.receivedFrameRate, 0);
819
934
  });
820
935
 
821
- it('after frames are sent and received', async () => {
936
+ it('should accurately report the transmitted and received frame rate after video frames are processed', async () => {
822
937
  fakeStats.video.senders[0].report[0].framesSent += 300;
823
938
  fakeStats.video.receivers[0].report[0].framesReceived += 300;
824
939
  await progressTime(MQA_INTERVAL);
@@ -829,9 +944,12 @@ describe('plugin-meetings', () => {
829
944
  });
830
945
  });
831
946
 
832
- it('emits the correct rtpPackets', async () => {
833
- it('at the start of the stats analyzer', async () => {
834
- await startStatsAnalyzer();
947
+ describe('RTP packets count in stats analyzer', () => {
948
+ beforeEach(async () => {
949
+ await startStatsAnalyzer({pc, statsAnalyzer});
950
+ });
951
+
952
+ it('should report zero RTP packets for all streams at the start of the stats analyzer', async () => {
835
953
  assert.strictEqual(mqeData.audioTransmit[0].common.rtpPackets, 0);
836
954
  assert.strictEqual(mqeData.audioTransmit[0].streams[0].common.rtpPackets, 0);
837
955
  assert.strictEqual(mqeData.audioReceive[0].common.rtpPackets, 0);
@@ -842,7 +960,7 @@ describe('plugin-meetings', () => {
842
960
  assert.strictEqual(mqeData.videoReceive[0].streams[0].common.rtpPackets, 0);
843
961
  });
844
962
 
845
- it('after packets are sent', async () => {
963
+ it('should update the RTP packets count correctly after audio and video packets are sent', async () => {
846
964
  fakeStats.audio.senders[0].report[0].packetsSent += 5;
847
965
  fakeStats.video.senders[0].report[0].packetsSent += 5;
848
966
  await progressTime(MQA_INTERVAL);
@@ -853,7 +971,7 @@ describe('plugin-meetings', () => {
853
971
  assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.rtpPackets, 5);
854
972
  });
855
973
 
856
- it('after packets are received', async () => {
974
+ it('should update the RTP packets count correctly after audio and video packets are received', async () => {
857
975
  fakeStats.audio.senders[0].report[0].packetsSent += 10;
858
976
  fakeStats.video.senders[0].report[0].packetsSent += 10;
859
977
  fakeStats.audio.receivers[0].report[0].packetsReceived += 10;
@@ -867,20 +985,23 @@ describe('plugin-meetings', () => {
867
985
  });
868
986
  });
869
987
 
870
- it('emits the correct fecPackets', async () => {
871
- it('at the start of the stats analyzer', async () => {
872
- await startStatsAnalyzer();
988
+ describe('FEC packet reporting in stats analyzer', () => {
989
+ beforeEach(async () => {
990
+ await startStatsAnalyzer({pc, statsAnalyzer});
991
+ });
992
+
993
+ it('should initially report zero FEC packets at the start of the stats analyzer', async () => {
873
994
  assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 0);
874
995
  });
875
996
 
876
- it('after FEC packets are received', async () => {
997
+ it('should accurately report the count of FEC packets received', async () => {
877
998
  fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 5;
878
999
  await progressTime(MQA_INTERVAL);
879
1000
 
880
1001
  assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 5);
881
1002
  });
882
1003
 
883
- it('after FEC packets are received and some FEC packets are discarded', async () => {
1004
+ it('should correctly adjust the FEC packet count when packets are discarded', async () => {
884
1005
  fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 15;
885
1006
  fakeStats.audio.receivers[0].report[0].fecPacketsDiscarded += 5;
886
1007
  await progressTime(MQA_INTERVAL);
@@ -889,16 +1010,19 @@ describe('plugin-meetings', () => {
889
1010
  });
890
1011
  });
891
1012
 
892
- it('emits the correct mediaHopByHopLost/rtpHopByHopLost', async () => {
893
- it('at the start of the stats analyzer', async () => {
894
- await startStatsAnalyzer();
1013
+ describe('packet loss metrics reporting in stats analyzer', () => {
1014
+ beforeEach(async () => {
1015
+ await startStatsAnalyzer({pc, statsAnalyzer});
1016
+ });
1017
+
1018
+ it('should report zero packet loss for both audio and video at the start of the stats analyzer', async () => {
895
1019
  assert.strictEqual(mqeData.audioReceive[0].common.mediaHopByHopLost, 0);
896
1020
  assert.strictEqual(mqeData.audioReceive[0].common.rtpHopByHopLost, 0);
897
1021
  assert.strictEqual(mqeData.videoReceive[0].common.mediaHopByHopLost, 0);
898
1022
  assert.strictEqual(mqeData.videoReceive[0].common.rtpHopByHopLost, 0);
899
1023
  });
900
1024
 
901
- it('after packets are lost', async () => {
1025
+ it('should update packet loss metrics correctly for both audio and video after packet loss is detected', async () => {
902
1026
  fakeStats.audio.receivers[0].report[0].packetsLost += 5;
903
1027
  fakeStats.video.receivers[0].report[0].packetsLost += 5;
904
1028
  await progressTime(MQA_INTERVAL);
@@ -910,14 +1034,17 @@ describe('plugin-meetings', () => {
910
1034
  });
911
1035
  });
912
1036
 
913
- it('emits the correct remoteLossRate', async () => {
914
- it('at the start of the stats analyzer', async () => {
915
- await startStatsAnalyzer();
1037
+ describe('remote loss rate reporting in stats analyzer', () => {
1038
+ beforeEach(async () => {
1039
+ await startStatsAnalyzer({pc, statsAnalyzer});
1040
+ });
1041
+
1042
+ it('should report a zero remote loss rate for both audio and video at the start', async () => {
916
1043
  assert.strictEqual(mqeData.audioTransmit[0].common.remoteLossRate, 0);
917
1044
  assert.strictEqual(mqeData.videoTransmit[0].common.remoteLossRate, 0);
918
1045
  });
919
1046
 
920
- it('after packets are sent', async () => {
1047
+ it('should maintain a zero remote loss rate for both audio and video after packets are sent without loss', async () => {
921
1048
  fakeStats.audio.senders[0].report[0].packetsSent += 100;
922
1049
  fakeStats.video.senders[0].report[0].packetsSent += 100;
923
1050
  await progressTime(MQA_INTERVAL);
@@ -926,7 +1053,7 @@ describe('plugin-meetings', () => {
926
1053
  assert.strictEqual(mqeData.videoTransmit[0].common.remoteLossRate, 0);
927
1054
  });
928
1055
 
929
- it('after packets are sent and some packets are lost', async () => {
1056
+ it('should accurately calculate the remote loss rate for both audio and video after packet loss is detected', async () => {
930
1057
  fakeStats.audio.senders[0].report[0].packetsSent += 200;
931
1058
  fakeStats.audio.senders[0].report[1].packetsLost += 10;
932
1059
  fakeStats.video.senders[0].report[0].packetsSent += 200;
@@ -939,7 +1066,7 @@ describe('plugin-meetings', () => {
939
1066
  });
940
1067
 
941
1068
  it('has the correct localIpAddress set when the candidateType is host', async () => {
942
- await startStatsAnalyzer();
1069
+ await startStatsAnalyzer({pc, statsAnalyzer});
943
1070
 
944
1071
  await progressTime();
945
1072
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -949,7 +1076,7 @@ describe('plugin-meetings', () => {
949
1076
  });
950
1077
 
951
1078
  it('has the correct localIpAddress set when the candidateType is prflx and relayProtocol is set', async () => {
952
- await startStatsAnalyzer();
1079
+ await startStatsAnalyzer({pc, statsAnalyzer});
953
1080
 
954
1081
  await progressTime();
955
1082
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -963,7 +1090,7 @@ describe('plugin-meetings', () => {
963
1090
  });
964
1091
 
965
1092
  it('has the correct localIpAddress set when the candidateType is prflx and relayProtocol is not set', async () => {
966
- await startStatsAnalyzer();
1093
+ await startStatsAnalyzer({pc, statsAnalyzer});
967
1094
 
968
1095
  await progressTime();
969
1096
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -977,7 +1104,7 @@ describe('plugin-meetings', () => {
977
1104
  });
978
1105
 
979
1106
  it('has no localIpAddress set when the candidateType is invalid', async () => {
980
- await startStatsAnalyzer();
1107
+ await startStatsAnalyzer({pc, statsAnalyzer});
981
1108
 
982
1109
  await progressTime();
983
1110
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -988,8 +1115,10 @@ describe('plugin-meetings', () => {
988
1115
 
989
1116
  it('logs a message when audio send packets do not increase', async () => {
990
1117
  await startStatsAnalyzer(
991
- {expected: {sendAudio: true}},
992
- {audio: {local: EVENTS.LOCAL_MEDIA_STARTED}}
1118
+ {
1119
+ statsAnalyzer, pc, mediaStatus: {expected: {sendAudio: true}},
1120
+ lastEmittedEvents: {audio: {local: EVENTS.LOCAL_MEDIA_STARTED}},
1121
+ },
993
1122
  );
994
1123
 
995
1124
  // don't increase the packets when time progresses.
@@ -997,15 +1126,17 @@ describe('plugin-meetings', () => {
997
1126
 
998
1127
  assert(
999
1128
  loggerSpy.calledWith(
1000
- 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent'
1001
- )
1129
+ 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent',
1130
+ ),
1002
1131
  );
1003
1132
  });
1004
1133
 
1005
1134
  it('does not log a message when audio send packets increase', async () => {
1006
- await startStatsAnalyzer(
1007
- {expected: {sendAudio: true}},
1008
- {audio: {local: EVENTS.LOCAL_MEDIA_STOPPED}}
1135
+ await startStatsAnalyzer({
1136
+ statsAnalyzer, pc,
1137
+ mediaStatus: {expected: {sendAudio: true}},
1138
+ lastEmittedEvents: {audio: {local: EVENTS.LOCAL_MEDIA_STOPPED}},
1139
+ },
1009
1140
  );
1010
1141
 
1011
1142
  fakeStats.audio.senders[0].report[0].packetsSent += 5;
@@ -1013,15 +1144,16 @@ describe('plugin-meetings', () => {
1013
1144
 
1014
1145
  assert(
1015
1146
  loggerSpy.neverCalledWith(
1016
- 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent'
1017
- )
1147
+ 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent',
1148
+ ),
1018
1149
  );
1019
1150
  });
1020
1151
 
1021
1152
  it('logs a message when video send packets do not increase', async () => {
1022
- await startStatsAnalyzer(
1023
- {expected: {sendVideo: true}},
1024
- {video: {local: EVENTS.LOCAL_MEDIA_STARTED}}
1153
+ await startStatsAnalyzer({
1154
+ statsAnalyzer, pc, mediaStatus: {expected: {sendVideo: true}},
1155
+ lastEmittedEvents: {video: {local: EVENTS.LOCAL_MEDIA_STARTED}},
1156
+ },
1025
1157
  );
1026
1158
 
1027
1159
  // don't increase the packets when time progresses.
@@ -1029,31 +1161,42 @@ describe('plugin-meetings', () => {
1029
1161
 
1030
1162
  assert(
1031
1163
  loggerSpy.calledWith(
1032
- 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent'
1033
- )
1164
+ 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent',
1165
+ ),
1034
1166
  );
1035
1167
  });
1036
1168
 
1037
1169
  it('does not log a message when video send packets increase', async () => {
1038
1170
  await startStatsAnalyzer(
1039
- {expected: {sendVideo: true}},
1040
- {video: {local: EVENTS.LOCAL_MEDIA_STOPPED}}
1041
- );
1171
+ {
1172
+ statsAnalyzer, pc,
1173
+ mediaStatus: {
1174
+ expected: {
1175
+ sendVideo: true,
1176
+ },
1177
+ },
1178
+ lastEmittedEvents: {
1179
+ video: {
1180
+ local: EVENTS.LOCAL_MEDIA_STOPPED,
1181
+ },
1182
+ },
1183
+ });
1042
1184
 
1043
1185
  fakeStats.video.senders[0].report[0].packetsSent += 5;
1044
1186
  await progressTime();
1045
1187
 
1046
1188
  assert(
1047
1189
  loggerSpy.neverCalledWith(
1048
- 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent'
1049
- )
1190
+ 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent',
1191
+ ),
1050
1192
  );
1051
1193
  });
1052
1194
 
1053
1195
  it('logs a message when share send packets do not increase', async () => {
1054
- await startStatsAnalyzer(
1055
- {expected: {sendShare: true}},
1056
- {share: {local: EVENTS.LOCAL_MEDIA_STARTED}}
1196
+ await startStatsAnalyzer({
1197
+ pc, mediaStatus: {expected: {sendShare: true}},
1198
+ lastEmittedEvents: {share: {local: EVENTS.LOCAL_MEDIA_STARTED}}, statsAnalyzer,
1199
+ },
1057
1200
  );
1058
1201
 
1059
1202
  // don't increase the packets when time progresses.
@@ -1061,15 +1204,16 @@ describe('plugin-meetings', () => {
1061
1204
 
1062
1205
  assert(
1063
1206
  loggerSpy.calledWith(
1064
- 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent'
1065
- )
1207
+ 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent',
1208
+ ),
1066
1209
  );
1067
1210
  });
1068
1211
 
1069
1212
  it('does not log a message when share send packets increase', async () => {
1070
- await startStatsAnalyzer(
1071
- {expected: {sendShare: true}},
1072
- {share: {local: EVENTS.LOCAL_MEDIA_STOPPED}}
1213
+ await startStatsAnalyzer({
1214
+ pc, statsAnalyzer, mediaStatus: {expected: {sendShare: true}},
1215
+ lastEmittedEvents: {share: {local: EVENTS.LOCAL_MEDIA_STOPPED}},
1216
+ },
1073
1217
  );
1074
1218
 
1075
1219
  fakeStats.share.senders[0].report[0].packetsSent += 5;
@@ -1077,8 +1221,8 @@ describe('plugin-meetings', () => {
1077
1221
 
1078
1222
  assert(
1079
1223
  loggerSpy.neverCalledWith(
1080
- 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent'
1081
- )
1224
+ 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent',
1225
+ ),
1082
1226
  );
1083
1227
  });
1084
1228
 
@@ -1091,7 +1235,7 @@ describe('plugin-meetings', () => {
1091
1235
  id: '4',
1092
1236
  };
1093
1237
 
1094
- await startStatsAnalyzer();
1238
+ await startStatsAnalyzer({pc, statsAnalyzer});
1095
1239
 
1096
1240
  // don't increase the packets when time progresses.
1097
1241
  await progressTime();
@@ -1099,10 +1243,10 @@ describe('plugin-meetings', () => {
1099
1243
  assert.neverCalledWith(
1100
1244
  loggerSpy,
1101
1245
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot id: "4" and csi: 2. Total packets received on slot: ',
1102
- 0
1246
+ 0,
1103
1247
  );
1104
1248
  });
1105
- }
1249
+ },
1106
1250
  );
1107
1251
 
1108
1252
  it(`logs a message if no packets are sent`, async () => {
@@ -1111,7 +1255,7 @@ describe('plugin-meetings', () => {
1111
1255
  csi: 2,
1112
1256
  id: '4',
1113
1257
  };
1114
- await startStatsAnalyzer();
1258
+ await startStatsAnalyzer({pc, statsAnalyzer});
1115
1259
 
1116
1260
  // don't increase the packets when time progresses.
1117
1261
  await progressTime();
@@ -1119,52 +1263,52 @@ describe('plugin-meetings', () => {
1119
1263
  assert.calledWith(
1120
1264
  loggerSpy,
1121
1265
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1122
- 0
1266
+ 0,
1123
1267
  );
1124
1268
 
1125
1269
  assert.calledWith(
1126
1270
  loggerSpy,
1127
1271
  'StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total frames received on slot: ',
1128
- 0
1272
+ 0,
1129
1273
  );
1130
1274
 
1131
1275
  assert.calledWith(
1132
1276
  loggerSpy,
1133
1277
  'StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total frames decoded on slot: ',
1134
- 0
1278
+ 0,
1135
1279
  );
1136
1280
 
1137
1281
  assert.calledWith(
1138
1282
  loggerSpy,
1139
1283
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: audio-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1140
- 0
1284
+ 0,
1141
1285
  );
1142
1286
 
1143
1287
  assert.calledWith(
1144
1288
  loggerSpy,
1145
1289
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1146
- 0
1290
+ 0,
1147
1291
  );
1148
1292
 
1149
1293
  assert.calledWith(
1150
1294
  loggerSpy,
1151
1295
  'StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total frames received on slot: ',
1152
- 0
1296
+ 0,
1153
1297
  );
1154
1298
  assert.calledWith(
1155
1299
  loggerSpy,
1156
1300
  'StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total frames decoded on slot: ',
1157
- 0
1301
+ 0,
1158
1302
  );
1159
1303
  assert.calledWith(
1160
1304
  loggerSpy,
1161
1305
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: audio-share-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1162
- 0
1306
+ 0,
1163
1307
  );
1164
1308
  });
1165
1309
 
1166
1310
  it(`does not log a message if receiveSlot is undefined`, async () => {
1167
- await startStatsAnalyzer();
1311
+ await startStatsAnalyzer({pc, statsAnalyzer});
1168
1312
 
1169
1313
  // don't increase the packets when time progresses.
1170
1314
  await progressTime();
@@ -1172,12 +1316,12 @@ describe('plugin-meetings', () => {
1172
1316
  assert.neverCalledWith(
1173
1317
  loggerSpy,
1174
1318
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot "". Total packets received on slot: ',
1175
- 0
1319
+ 0,
1176
1320
  );
1177
1321
  });
1178
1322
 
1179
1323
  it('has the correct number of senders and receivers (2)', async () => {
1180
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1324
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1181
1325
 
1182
1326
  await progressTime();
1183
1327
 
@@ -1188,7 +1332,7 @@ describe('plugin-meetings', () => {
1188
1332
  });
1189
1333
 
1190
1334
  it('has one stream per sender/reciever', async () => {
1191
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1335
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1192
1336
 
1193
1337
  await progressTime();
1194
1338
 
@@ -1375,7 +1519,7 @@ describe('plugin-meetings', () => {
1375
1519
  framesDropped: 0,
1376
1520
  },
1377
1521
  h264CodecProfile: 'BP',
1378
- isActiveSpeaker: true,
1522
+ isActiveSpeaker: false,
1379
1523
  optimalFrameSize: 0,
1380
1524
  receivedFrameSize: 3600,
1381
1525
  receivedHeight: 720,
@@ -1409,7 +1553,7 @@ describe('plugin-meetings', () => {
1409
1553
  framesDropped: 0,
1410
1554
  },
1411
1555
  h264CodecProfile: 'BP',
1412
- isActiveSpeaker: true,
1556
+ isActiveSpeaker: false,
1413
1557
  optimalFrameSize: 0,
1414
1558
  receivedFrameSize: 3600,
1415
1559
  receivedHeight: 720,
@@ -1448,7 +1592,7 @@ describe('plugin-meetings', () => {
1448
1592
  },
1449
1593
  });
1450
1594
 
1451
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1595
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1452
1596
 
1453
1597
  await progressTime();
1454
1598
 
@@ -1473,7 +1617,7 @@ describe('plugin-meetings', () => {
1473
1617
  framesDropped: 0,
1474
1618
  },
1475
1619
  h264CodecProfile: 'BP',
1476
- isActiveSpeaker: true,
1620
+ isActiveSpeaker: false,
1477
1621
  optimalFrameSize: 0,
1478
1622
  receivedFrameSize: 3600,
1479
1623
  receivedHeight: 720,
@@ -1505,7 +1649,7 @@ describe('plugin-meetings', () => {
1505
1649
  framesDropped: 0,
1506
1650
  },
1507
1651
  h264CodecProfile: 'BP',
1508
- isActiveSpeaker: true,
1652
+ isActiveSpeaker: false,
1509
1653
  optimalFrameSize: 0,
1510
1654
  receivedFrameSize: 3600,
1511
1655
  receivedHeight: 720,
@@ -1537,7 +1681,7 @@ describe('plugin-meetings', () => {
1537
1681
  framesDropped: 0,
1538
1682
  },
1539
1683
  h264CodecProfile: 'BP',
1540
- isActiveSpeaker: true,
1684
+ isActiveSpeaker: false,
1541
1685
  optimalFrameSize: 0,
1542
1686
  receivedFrameSize: 3600,
1543
1687
  receivedHeight: 720,
@@ -1552,186 +1696,358 @@ describe('plugin-meetings', () => {
1552
1696
  ]);
1553
1697
  });
1554
1698
 
1555
- it('has three streams for video senders for simulcast', async () => {
1556
- pc.getTransceiverStats = sinon.stub().resolves({
1557
- audio: {
1558
- senders: [fakeStats.audio.senders[0]],
1559
- receivers: [fakeStats.audio.receivers[0]],
1560
- },
1561
- video: {
1562
- senders: [
1563
- {
1564
- localTrackLabel: 'fake-camera',
1565
- report: [
1566
- {
1567
- type: 'outbound-rtp',
1568
- bytesSent: 1,
1569
- framesSent: 0,
1570
- packetsSent: 0,
1571
- },
1572
- {
1573
- type: 'outbound-rtp',
1574
- bytesSent: 0,
1575
- framesSent: 0,
1576
- packetsSent: 0,
1577
- },
1578
- {
1579
- type: 'outbound-rtp',
1580
- bytesSent: 1000,
1581
- framesSent: 1,
1582
- packetsSent: 1,
1583
- },
1584
- {
1585
- type: 'remote-inbound-rtp',
1586
- packetsLost: 0,
1587
- },
1588
- {
1589
- type: 'candidate-pair',
1590
- state: 'succeeded',
1591
- localCandidateId: 'fake-candidate-id',
1592
- },
1593
- {
1594
- type: 'candidate-pair',
1595
- state: 'failed',
1596
- localCandidateId: 'bad-candidate-id',
1597
- },
1598
- {
1599
- type: 'local-candidate',
1600
- id: 'fake-candidate-id',
1601
- protocol: 'tcp',
1602
- },
1603
- ],
1604
- },
1605
- ],
1606
- receivers: [fakeStats.video.receivers[0]],
1607
- },
1608
- screenShareAudio: {
1609
- senders: [fakeStats.audio.senders[0]],
1610
- receivers: [fakeStats.audio.receivers[0]],
1611
- },
1612
- screenShareVideo: {
1613
- senders: [fakeStats.video.senders[0]],
1614
- receivers: [fakeStats.video.receivers[0]],
1615
- },
1616
- });
1617
-
1618
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1699
+ describe('stream count for simulcast', async () => {
1700
+ it('has three streams for video senders for simulcast', async () => {
1701
+ pc.getTransceiverStats = sinon.stub().resolves({
1702
+ audio: {
1703
+ senders: [fakeStats.audio.senders[0]],
1704
+ receivers: [fakeStats.audio.receivers[0]],
1705
+ },
1706
+ video: {
1707
+ senders: [
1708
+ {
1709
+ localTrackLabel: 'fake-camera',
1710
+ report: [
1711
+ {
1712
+ type: 'outbound-rtp',
1713
+ bytesSent: 1,
1714
+ framesSent: 0,
1715
+ packetsSent: 0,
1716
+ isRequested: true,
1717
+ },
1718
+ {
1719
+ type: 'outbound-rtp',
1720
+ bytesSent: 1,
1721
+ framesSent: 0,
1722
+ packetsSent: 1,
1723
+ isRequested: true,
1724
+ },
1725
+ {
1726
+ type: 'outbound-rtp',
1727
+ bytesSent: 1000,
1728
+ framesSent: 1,
1729
+ packetsSent: 0,
1730
+ isRequested: true,
1731
+ },
1732
+ {
1733
+ type: 'remote-inbound-rtp',
1734
+ packetsLost: 0,
1735
+ },
1736
+ {
1737
+ type: 'candidate-pair',
1738
+ state: 'succeeded',
1739
+ localCandidateId: 'fake-candidate-id',
1740
+ },
1741
+ {
1742
+ type: 'candidate-pair',
1743
+ state: 'failed',
1744
+ localCandidateId: 'bad-candidate-id',
1745
+ },
1746
+ {
1747
+ type: 'local-candidate',
1748
+ id: 'fake-candidate-id',
1749
+ protocol: 'tcp',
1750
+ },
1751
+ ],
1752
+ },
1753
+ ],
1754
+ receivers: [fakeStats.video.receivers[0]],
1755
+ },
1756
+ screenShareAudio: {
1757
+ senders: [fakeStats.audio.senders[0]],
1758
+ receivers: [fakeStats.audio.receivers[0]],
1759
+ },
1760
+ screenShareVideo: {
1761
+ senders: [fakeStats.video.senders[0]],
1762
+ receivers: [fakeStats.video.receivers[0]],
1763
+ },
1764
+ });
1619
1765
 
1620
- await progressTime();
1766
+ await startStatsAnalyzer({
1767
+ pc,
1768
+ statsAnalyzer,
1769
+ mediaStatus: {
1770
+ expected: {
1771
+ receiveVideo: true,
1772
+ },
1773
+ },
1774
+ });
1621
1775
 
1622
- assert.deepEqual(mqeData.videoTransmit[0].streams, [
1623
- {
1624
- common: {
1625
- codec: 'H264',
1626
- csi: [],
1627
- duplicateSsci: 0,
1776
+ await progressTime();
1777
+
1778
+ assert.deepEqual(mqeData.videoTransmit[0].streams, [
1779
+ {
1780
+ common: {
1781
+ codec: 'H264',
1782
+ csi: [],
1783
+ duplicateSsci: 0,
1784
+ requestedBitrate: 0,
1785
+ requestedFrames: 0,
1786
+ rtpPackets: 0,
1787
+ ssci: 0,
1788
+ transmittedBitrate: 0.13333333333333333,
1789
+ transmittedFrameRate: 0,
1790
+ },
1791
+ h264CodecProfile: 'BP',
1792
+ isAvatar: false,
1793
+ isHardwareEncoded: false,
1794
+ localConfigurationChanges: 2,
1795
+ maxFrameQp: 0,
1796
+ maxNoiseLevel: 0,
1797
+ minRegionQp: 0,
1798
+ remoteConfigurationChanges: 0,
1799
+ requestedFrameSize: 0,
1800
+ requestedKeyFrames: 0,
1801
+ transmittedFrameSize: 0,
1802
+ transmittedHeight: 0,
1803
+ transmittedKeyFrames: 0,
1804
+ transmittedKeyFramesClient: 0,
1805
+ transmittedKeyFramesConfigurationChange: 0,
1806
+ transmittedKeyFramesFeedback: 0,
1807
+ transmittedKeyFramesLocalDrop: 0,
1808
+ transmittedKeyFramesOtherLayer: 0,
1809
+ transmittedKeyFramesPeriodic: 0,
1810
+ transmittedKeyFramesSceneChange: 0,
1811
+ transmittedKeyFramesStartup: 0,
1812
+ transmittedKeyFramesUnknown: 0,
1813
+ transmittedWidth: 0,
1628
1814
  requestedBitrate: 0,
1629
- requestedFrames: 0,
1630
- rtpPackets: 0,
1631
- ssci: 0,
1632
- transmittedBitrate: 0.13333333333333333,
1633
- transmittedFrameRate: 0
1634
1815
  },
1635
- h264CodecProfile: 'BP',
1636
- isAvatar: false,
1637
- isHardwareEncoded: false,
1638
- localConfigurationChanges: 2,
1639
- maxFrameQp: 0,
1640
- maxNoiseLevel: 0,
1641
- minRegionQp: 0,
1642
- remoteConfigurationChanges: 0,
1643
- requestedFrameSize: 0,
1644
- requestedKeyFrames: 0,
1645
- transmittedFrameSize: 0,
1646
- transmittedHeight: 0,
1647
- transmittedKeyFrames: 0,
1648
- transmittedKeyFramesClient: 0,
1649
- transmittedKeyFramesConfigurationChange: 0,
1650
- transmittedKeyFramesFeedback: 0,
1651
- transmittedKeyFramesLocalDrop: 0,
1652
- transmittedKeyFramesOtherLayer: 0,
1653
- transmittedKeyFramesPeriodic: 0,
1654
- transmittedKeyFramesSceneChange: 0,
1655
- transmittedKeyFramesStartup: 0,
1656
- transmittedKeyFramesUnknown: 0,
1657
- transmittedWidth: 0,
1658
- requestedBitrate: 0,
1659
- },
1660
- {
1661
- common: {
1662
- codec: 'H264',
1663
- csi: [],
1664
- duplicateSsci: 0,
1816
+ {
1817
+ common: {
1818
+ codec: 'H264',
1819
+ csi: [],
1820
+ duplicateSsci: 0,
1821
+ requestedBitrate: 0,
1822
+ requestedFrames: 0,
1823
+ rtpPackets: 1,
1824
+ ssci: 0,
1825
+ transmittedBitrate: 0.13333333333333333,
1826
+ transmittedFrameRate: 0,
1827
+ },
1828
+ h264CodecProfile: 'BP',
1829
+ isAvatar: false,
1830
+ isHardwareEncoded: false,
1831
+ localConfigurationChanges: 2,
1832
+ maxFrameQp: 0,
1833
+ maxNoiseLevel: 0,
1834
+ minRegionQp: 0,
1835
+ remoteConfigurationChanges: 0,
1836
+ requestedFrameSize: 0,
1837
+ requestedKeyFrames: 0,
1838
+ transmittedFrameSize: 0,
1839
+ transmittedHeight: 0,
1840
+ transmittedKeyFrames: 0,
1841
+ transmittedKeyFramesClient: 0,
1842
+ transmittedKeyFramesConfigurationChange: 0,
1843
+ transmittedKeyFramesFeedback: 0,
1844
+ transmittedKeyFramesLocalDrop: 0,
1845
+ transmittedKeyFramesOtherLayer: 0,
1846
+ transmittedKeyFramesPeriodic: 0,
1847
+ transmittedKeyFramesSceneChange: 0,
1848
+ transmittedKeyFramesStartup: 0,
1849
+ transmittedKeyFramesUnknown: 0,
1850
+ transmittedWidth: 0,
1665
1851
  requestedBitrate: 0,
1666
- requestedFrames: 0,
1667
- rtpPackets: 0,
1668
- ssci: 0,
1669
- transmittedBitrate: 0,
1670
- transmittedFrameRate: 0,
1671
1852
  },
1672
- h264CodecProfile: 'BP',
1673
- isAvatar: false,
1674
- isHardwareEncoded: false,
1675
- localConfigurationChanges: 2,
1676
- maxFrameQp: 0,
1677
- maxNoiseLevel: 0,
1678
- minRegionQp: 0,
1679
- remoteConfigurationChanges: 0,
1680
- requestedFrameSize: 0,
1681
- requestedKeyFrames: 0,
1682
- transmittedFrameSize: 0,
1683
- transmittedHeight: 0,
1684
- transmittedKeyFrames: 0,
1685
- transmittedKeyFramesClient: 0,
1686
- transmittedKeyFramesConfigurationChange: 0,
1687
- transmittedKeyFramesFeedback: 0,
1688
- transmittedKeyFramesLocalDrop: 0,
1689
- transmittedKeyFramesOtherLayer: 0,
1690
- transmittedKeyFramesPeriodic: 0,
1691
- transmittedKeyFramesSceneChange: 0,
1692
- transmittedKeyFramesStartup: 0,
1693
- transmittedKeyFramesUnknown: 0,
1694
- transmittedWidth: 0,
1695
- requestedBitrate: 0,
1696
- },
1697
- {
1698
- common: {
1699
- codec: 'H264',
1700
- csi: [],
1701
- duplicateSsci: 0,
1853
+ {
1854
+ common: {
1855
+ codec: 'H264',
1856
+ csi: [],
1857
+ duplicateSsci: 0,
1858
+ requestedBitrate: 0,
1859
+ requestedFrames: 0,
1860
+ rtpPackets: 0,
1861
+ ssci: 0,
1862
+ transmittedBitrate: 133.33333333333334,
1863
+ transmittedFrameRate: 0,
1864
+ },
1865
+ h264CodecProfile: 'BP',
1866
+ isAvatar: false,
1867
+ isHardwareEncoded: false,
1868
+ localConfigurationChanges: 2,
1869
+ maxFrameQp: 0,
1870
+ maxNoiseLevel: 0,
1871
+ minRegionQp: 0,
1872
+ remoteConfigurationChanges: 0,
1873
+ requestedFrameSize: 0,
1874
+ requestedKeyFrames: 0,
1875
+ transmittedFrameSize: 0,
1876
+ transmittedHeight: 0,
1877
+ transmittedKeyFrames: 0,
1878
+ transmittedKeyFramesClient: 0,
1879
+ transmittedKeyFramesConfigurationChange: 0,
1880
+ transmittedKeyFramesFeedback: 0,
1881
+ transmittedKeyFramesLocalDrop: 0,
1882
+ transmittedKeyFramesOtherLayer: 0,
1883
+ transmittedKeyFramesPeriodic: 0,
1884
+ transmittedKeyFramesSceneChange: 0,
1885
+ transmittedKeyFramesStartup: 0,
1886
+ transmittedKeyFramesUnknown: 0,
1887
+ transmittedWidth: 0,
1702
1888
  requestedBitrate: 0,
1703
- requestedFrames: 0,
1704
- rtpPackets: 1,
1705
- ssci: 0,
1706
- transmittedBitrate: 133.33333333333334,
1707
- transmittedFrameRate: 0,
1708
1889
  },
1709
- h264CodecProfile: 'BP',
1710
- isAvatar: false,
1711
- isHardwareEncoded: false,
1712
- localConfigurationChanges: 2,
1713
- maxFrameQp: 0,
1714
- maxNoiseLevel: 0,
1715
- minRegionQp: 0,
1716
- remoteConfigurationChanges: 0,
1717
- requestedFrameSize: 0,
1718
- requestedKeyFrames: 0,
1719
- transmittedFrameSize: 0,
1720
- transmittedHeight: 0,
1721
- transmittedKeyFrames: 0,
1722
- transmittedKeyFramesClient: 0,
1723
- transmittedKeyFramesConfigurationChange: 0,
1724
- transmittedKeyFramesFeedback: 0,
1725
- transmittedKeyFramesLocalDrop: 0,
1726
- transmittedKeyFramesOtherLayer: 0,
1727
- transmittedKeyFramesPeriodic: 0,
1728
- transmittedKeyFramesSceneChange: 0,
1729
- transmittedKeyFramesStartup: 0,
1730
- transmittedKeyFramesUnknown: 0,
1731
- transmittedWidth: 0,
1732
- requestedBitrate: 0,
1890
+ ]);
1891
+ });
1892
+ });
1893
+ describe('active speaker status emission', async () => {
1894
+ beforeEach(async () => {
1895
+ await startStatsAnalyzer({pc, statsAnalyzer});
1896
+ performance.timeOrigin = 1;
1897
+ });
1898
+
1899
+ it('reports active speaker as true when the participant has been speaking', async () => {
1900
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = true;
1901
+ await progressTime(5 * MQA_INTERVAL);
1902
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, true);
1903
+ });
1904
+
1905
+ it('reports active speaker as false when the participant has not spoken', async () => {
1906
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = false;
1907
+ await progressTime(5 * MQA_INTERVAL);
1908
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false);
1909
+ });
1910
+
1911
+ it('defaults to false when active speaker status is indeterminate', async () => {
1912
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = undefined;
1913
+ await progressTime(MQA_INTERVAL);
1914
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false);
1915
+ });
1916
+
1917
+ it('updates active speaker to true following a recent status change to speaking', async () => {
1918
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = false;
1919
+ fakeStats.video.receivers[0].report[0].lastActiveSpeakerUpdateTimestamp = performance.timeOrigin + performance.now() + (30 * 1000);
1920
+ await progressTime(MQA_INTERVAL);
1921
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, true);
1922
+ await progressTime(MQA_INTERVAL);
1923
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false);
1924
+ });
1925
+ });
1926
+ describe('sends streams according to their is requested flag', async () => {
1927
+
1928
+ beforeEach(async () => {
1929
+ performance.timeOrigin = 0;
1930
+ await startStatsAnalyzer({pc, statsAnalyzer});
1931
+ });
1932
+
1933
+ it('should send a stream if it is requested', async () => {
1934
+ fakeStats.audio.senders[0].report[0].isRequested = true;
1935
+ await progressTime(MQA_INTERVAL);
1936
+ assert.strictEqual(mqeData.audioTransmit[0].streams.length, 1);
1937
+ });
1938
+
1939
+ it('should not sent a stream if its is requested flag is undefined', async () => {
1940
+ fakeStats.audio.senders[0].report[0].isRequested = undefined;
1941
+ await progressTime(MQA_INTERVAL);
1942
+ assert.strictEqual(mqeData.audioTransmit[0].streams.length, 0);
1943
+ });
1944
+
1945
+ it('should not send a stream if it is not requested', async () => {
1946
+ fakeStats.audio.receivers[0].report[0].isRequested = false;
1947
+ await progressTime(MQA_INTERVAL);
1948
+ assert.strictEqual(mqeData.audioReceive[0].streams.length, 0);
1949
+ });
1950
+
1951
+ it('should send the stream if it was recently requested', async () => {
1952
+ fakeStats.audio.receivers[0].report[0].lastRequestedUpdateTimestamp = performance.timeOrigin + performance.now() + (30 * 1000);
1953
+ fakeStats.audio.receivers[0].report[0].isRequested = false;
1954
+ await progressTime(MQA_INTERVAL);
1955
+ assert.strictEqual(mqeData.audioReceive[0].streams.length, 1);
1956
+ await progressTime(MQA_INTERVAL);
1957
+ assert.strictEqual(mqeData.audioReceive[0].streams.length, 0);
1958
+ });
1959
+ });
1960
+
1961
+ describe('window and screen size emission', async () => {
1962
+ beforeEach(async () => {
1963
+ await startStatsAnalyzer({pc, statsAnalyzer});
1964
+ });
1965
+
1966
+ it('should record the screen size from window.screen properties', async () => {
1967
+ sinon.stub(window.screen, 'width').get(() => 1280);
1968
+ sinon.stub(window.screen, 'height').get(() => 720);
1969
+ await progressTime(MQA_INTERVAL);
1970
+ assert.strictEqual(mqeData.intervalMetadata.screenWidth, 1280);
1971
+ assert.strictEqual(mqeData.intervalMetadata.screenHeight, 720);
1972
+ assert.strictEqual(mqeData.intervalMetadata.screenResolution, 3600);
1973
+ });
1974
+
1975
+ it('should record the initial app window size from window properties', async () => {
1976
+ sinon.stub(window, 'innerWidth').get(() => 720);
1977
+ sinon.stub(window, 'innerHeight').get(() => 360);
1978
+ await progressTime(MQA_INTERVAL);
1979
+ assert.strictEqual(mqeData.intervalMetadata.appWindowWidth, 720);
1980
+ assert.strictEqual(mqeData.intervalMetadata.appWindowHeight, 360);
1981
+ assert.strictEqual(mqeData.intervalMetadata.appWindowSize, 1013);
1982
+
1983
+ sinon.stub(window, 'innerWidth').get(() => 1080);
1984
+ sinon.stub(window, 'innerHeight').get(() => 720);
1985
+ await progressTime(MQA_INTERVAL);
1986
+ assert.strictEqual(mqeData.intervalMetadata.appWindowWidth, 1080);
1987
+ assert.strictEqual(mqeData.intervalMetadata.appWindowHeight, 720);
1988
+ assert.strictEqual(mqeData.intervalMetadata.appWindowSize, 3038);
1989
+ });
1990
+ });
1991
+
1992
+ describe('sends multistreamEnabled', async () => {
1993
+ it('false if StatsAnalyzer initialized with default value for isMultistream', async () => {
1994
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1995
+
1996
+ await progressTime();
1997
+
1998
+ for (const data of [
1999
+ mqeData.audioTransmit,
2000
+ mqeData.audioReceive,
2001
+ mqeData.videoTransmit,
2002
+ mqeData.videoReceive,
2003
+ ]) {
2004
+ assert.strictEqual(data[0].common.common.multistreamEnabled, false);
1733
2005
  }
1734
- ]);
2006
+ });
2007
+
2008
+ it('false if StatsAnalyzer initialized with false', async () => {
2009
+ statsAnalyzer = new StatsAnalyzer({
2010
+ config: initialConfig,
2011
+ receiveSlotCallback: () => receiveSlot,
2012
+ networkQualityMonitor,
2013
+ isMultistream: false,
2014
+ });
2015
+ registerStatsAnalyzerEvents(statsAnalyzer);
2016
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: false}}});
2017
+
2018
+ await progressTime();
2019
+
2020
+ for (const data of [
2021
+ mqeData.audioTransmit,
2022
+ mqeData.audioReceive,
2023
+ mqeData.videoTransmit,
2024
+ mqeData.videoReceive,
2025
+ ]) {
2026
+ assert.strictEqual(data[0].common.common.multistreamEnabled, false);
2027
+ }
2028
+ });
2029
+
2030
+ it('true if StatsAnalyzer initialized with multistream', async () => {
2031
+ statsAnalyzer = new StatsAnalyzer({
2032
+ config: initialConfig,
2033
+ receiveSlotCallback: () => receiveSlot,
2034
+ networkQualityMonitor,
2035
+ isMultistream: true,
2036
+ });
2037
+ registerStatsAnalyzerEvents(statsAnalyzer);
2038
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
2039
+
2040
+ await progressTime();
2041
+
2042
+ for (const data of [
2043
+ mqeData.audioTransmit,
2044
+ mqeData.audioReceive,
2045
+ mqeData.videoTransmit,
2046
+ mqeData.videoReceive,
2047
+ ]) {
2048
+ assert.strictEqual(data[0].common.common.multistreamEnabled, true);
2049
+ }
2050
+ });
1735
2051
  });
1736
2052
  });
1737
2053
  });