@webex/plugin-meetings 3.3.1-next.1 → 3.3.1-next.11

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.
@@ -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
 
@@ -89,7 +99,7 @@ describe('plugin-meetings', () => {
89
99
  requestedBitrate: 10000,
90
100
  },
91
101
  'audio-send',
92
- true
102
+ true,
93
103
  );
94
104
 
95
105
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.headerBytesSent, 25000);
@@ -126,7 +136,7 @@ describe('plugin-meetings', () => {
126
136
  requestedBitrate: 50000,
127
137
  },
128
138
  'video-send',
129
- true
139
+ true,
130
140
  );
131
141
 
132
142
  assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.headerBytesSent, 50000);
@@ -136,11 +146,11 @@ describe('plugin-meetings', () => {
136
146
  assert.strictEqual(statsAnalyzer.statsResults['video-send'].send.requestedBitrate, 50000);
137
147
  assert.strictEqual(
138
148
  statsAnalyzer.statsResults['video-send'].send.totalRtxPacketsSent,
139
- 10
149
+ 10,
140
150
  );
141
151
  assert.strictEqual(
142
152
  statsAnalyzer.statsResults['video-send'].send.totalRtxBytesSent,
143
- 500
153
+ 500,
144
154
  );
145
155
  });
146
156
 
@@ -183,12 +193,12 @@ describe('plugin-meetings', () => {
183
193
  requestedBitrate: 10000,
184
194
  },
185
195
  'audio-recv-1',
186
- false
196
+ false,
187
197
  );
188
198
 
189
199
  assert.strictEqual(
190
200
  statsAnalyzer.statsResults['audio-recv-1'].recv.totalPacketsReceived,
191
- 12
201
+ 12,
192
202
  );
193
203
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.fecPacketsDiscarded, 1);
194
204
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.fecPacketsReceived, 1);
@@ -196,18 +206,18 @@ describe('plugin-meetings', () => {
196
206
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.requestedBitrate, 10000);
197
207
  assert.strictEqual(
198
208
  statsAnalyzer.statsResults['audio-recv-1'].recv.headerBytesReceived,
199
- 250
209
+ 250,
200
210
  );
201
211
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.audioLevel, 0);
202
212
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalAudioEnergy, 133);
203
213
  assert.strictEqual(
204
214
  statsAnalyzer.statsResults['audio-recv-1'].recv.totalSamplesReceived,
205
- 300000
215
+ 300000,
206
216
  );
207
217
  assert.strictEqual(statsAnalyzer.statsResults['audio-recv-1'].recv.totalSamplesDecoded, 0);
208
218
  assert.strictEqual(
209
219
  statsAnalyzer.statsResults['audio-recv-1'].recv.concealedSamples,
210
- 200000
220
+ 200000,
211
221
  );
212
222
  });
213
223
 
@@ -243,7 +253,7 @@ describe('plugin-meetings', () => {
243
253
  retransmittedPacketsReceived: 10,
244
254
  },
245
255
  'video-recv',
246
- false
256
+ false,
247
257
  );
248
258
 
249
259
  assert.strictEqual(statsAnalyzer.statsResults['video-recv'].recv.totalPacketsReceived, 1500);
@@ -275,7 +285,7 @@ describe('plugin-meetings', () => {
275
285
  type: 'media-source',
276
286
  },
277
287
  'audio-send',
278
- true
288
+ true,
279
289
  );
280
290
 
281
291
  assert.strictEqual(statsAnalyzer.statsResults['audio-send'].send.audioLevel, 0.03);
@@ -322,15 +332,17 @@ describe('plugin-meetings', () => {
322
332
  const networkQualityMonitor = new NetworkQualityMonitor(initialConfig);
323
333
 
324
334
  statsAnalyzer = new StatsAnalyzer(
325
- initialConfig,
326
- () => ({}),
327
- networkQualityMonitor,
328
- defaultStats
335
+ {
336
+ config: initialConfig,
337
+ receiveSlotCallback: () => ({}),
338
+ networkQualityMonitor,
339
+ statsResults: defaultStats,
340
+ },
329
341
  );
330
342
 
331
343
  sandBoxSpy = sandbox.spy(
332
344
  statsAnalyzer.networkQualityMonitor,
333
- 'determineUplinkNetworkQuality'
345
+ 'determineUplinkNetworkQuality',
334
346
  );
335
347
  });
336
348
 
@@ -347,7 +359,7 @@ describe('plugin-meetings', () => {
347
359
  mediaType: 'video-send-1',
348
360
  remoteRtpResults: statusResult,
349
361
  statsAnalyzerCurrentStats: statsAnalyzer.statsResults,
350
- })
362
+ }),
351
363
  );
352
364
  });
353
365
  });
@@ -381,6 +393,24 @@ describe('plugin-meetings', () => {
381
393
  };
382
394
  };
383
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
+
384
414
  before(() => {
385
415
  LoggerConfig.set({enable: false});
386
416
  LoggerProxy.set();
@@ -404,6 +434,7 @@ describe('plugin-meetings', () => {
404
434
  type: 'outbound-rtp',
405
435
  bytesSent: 1,
406
436
  packetsSent: 0,
437
+ isRequested: true,
407
438
  },
408
439
  {
409
440
  type: 'remote-inbound-rtp',
@@ -437,6 +468,8 @@ describe('plugin-meetings', () => {
437
468
  fecPacketsReceived: 0,
438
469
  packetsLost: 0,
439
470
  packetsReceived: 0,
471
+ isRequested: true,
472
+ lastRequestedUpdateTimestamp: 0,
440
473
  },
441
474
  {
442
475
  type: 'remote-outbound-rtp',
@@ -470,6 +503,8 @@ describe('plugin-meetings', () => {
470
503
  bytesSent: 1,
471
504
  framesSent: 0,
472
505
  packetsSent: 0,
506
+ isRequested: true,
507
+ lastRequestedUpdateTimestamp: 0,
473
508
  },
474
509
  {
475
510
  type: 'remote-inbound-rtp',
@@ -505,6 +540,10 @@ describe('plugin-meetings', () => {
505
540
  framesReceived: 0,
506
541
  packetsLost: 0,
507
542
  packetsReceived: 0,
543
+ isRequested: true,
544
+ lastRequestedUpdateTimestamp: 0,
545
+ isActiveSpeaker: false,
546
+ lastActiveSpeakerUpdateTimestamp: 0,
508
547
  },
509
548
  {
510
549
  type: 'remote-outbound-rtp',
@@ -538,6 +577,8 @@ describe('plugin-meetings', () => {
538
577
  bytesSent: 1,
539
578
  framesSent: 0,
540
579
  packetsSent: 0,
580
+ isRequested: true,
581
+ lastRequestedUpdateTimestamp: 0,
541
582
  },
542
583
  {
543
584
  type: 'remote-inbound-rtp',
@@ -573,6 +614,8 @@ describe('plugin-meetings', () => {
573
614
  framesReceived: 0,
574
615
  packetsLost: 0,
575
616
  packetsReceived: 0,
617
+ isRequested: true,
618
+ lastRequestedUpdateTimestamp: 0,
576
619
  },
577
620
  {
578
621
  type: 'remote-outbound-rtp',
@@ -622,23 +665,11 @@ describe('plugin-meetings', () => {
622
665
 
623
666
  networkQualityMonitor = new NetworkQualityMonitor(initialConfig);
624
667
 
625
- statsAnalyzer = new StatsAnalyzer(initialConfig, () => receiveSlot, networkQualityMonitor);
626
-
627
- statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STARTED, (data) => {
628
- receivedEventsData.local.started = data;
629
- });
630
- statsAnalyzer.on(EVENTS.LOCAL_MEDIA_STOPPED, (data) => {
631
- receivedEventsData.local.stopped = data;
632
- });
633
- statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STARTED, (data) => {
634
- receivedEventsData.remote.started = data;
635
- });
636
- statsAnalyzer.on(EVENTS.REMOTE_MEDIA_STOPPED, (data) => {
637
- receivedEventsData.remote.stopped = data;
638
- });
639
- statsAnalyzer.on(EVENTS.MEDIA_QUALITY, ({data}) => {
640
- mqeData = data;
668
+ statsAnalyzer = new StatsAnalyzer({
669
+ config: initialConfig, receiveSlotCallback: () => receiveSlot, networkQualityMonitor,
641
670
  });
671
+
672
+ registerStatsAnalyzerEvents(statsAnalyzer);
642
673
  });
643
674
 
644
675
  afterEach(() => {
@@ -646,20 +677,12 @@ describe('plugin-meetings', () => {
646
677
  clock.restore();
647
678
  });
648
679
 
649
- const startStatsAnalyzer = async (mediaStatus, lastEmittedEvents) => {
650
- statsAnalyzer.updateMediaStatus(mediaStatus);
651
- statsAnalyzer.startAnalyzer(pc);
652
- statsAnalyzer.lastEmittedStartStopEvent = lastEmittedEvents || {};
653
-
654
- await testUtils.flushPromises();
655
- };
656
-
657
680
  const mergeProperties = (
658
681
  target,
659
682
  properties,
660
683
  keyValue = 'fake-candidate-id',
661
684
  matchKey = 'type',
662
- matchValue = 'local-candidate'
685
+ matchValue = 'local-candidate',
663
686
  ) => {
664
687
  for (let key in target) {
665
688
  if (target.hasOwnProperty(key)) {
@@ -704,7 +727,15 @@ describe('plugin-meetings', () => {
704
727
  };
705
728
 
706
729
  it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for audio', async () => {
707
- await startStatsAnalyzer({expected: {sendAudio: true}});
730
+ await startStatsAnalyzer({
731
+ statsAnalyzer,
732
+ pc,
733
+ mediaStatus: {
734
+ expected: {
735
+ sendAudio: true,
736
+ },
737
+ },
738
+ });
708
739
 
709
740
  // check that we haven't received any events yet
710
741
  checkReceivedEvent({expected: {}});
@@ -724,7 +755,7 @@ describe('plugin-meetings', () => {
724
755
  });
725
756
 
726
757
  it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for video', async () => {
727
- await startStatsAnalyzer({expected: {sendVideo: true}});
758
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {sendVideo: true}}});
728
759
 
729
760
  // check that we haven't received any events yet
730
761
  checkReceivedEvent({expected: {}});
@@ -744,7 +775,7 @@ describe('plugin-meetings', () => {
744
775
  });
745
776
 
746
777
  it('emits LOCAL_MEDIA_STARTED and LOCAL_MEDIA_STOPPED events for share', async () => {
747
- await startStatsAnalyzer({expected: {sendShare: true}});
778
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {sendShare: true}}});
748
779
 
749
780
  // check that we haven't received any events yet
750
781
  checkReceivedEvent({expected: {}});
@@ -764,7 +795,7 @@ describe('plugin-meetings', () => {
764
795
  });
765
796
 
766
797
  it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for audio', async () => {
767
- await startStatsAnalyzer({expected: {receiveAudio: true}});
798
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveAudio: true}}});
768
799
 
769
800
  // check that we haven't received any events yet
770
801
  checkReceivedEvent({expected: {}});
@@ -784,7 +815,7 @@ describe('plugin-meetings', () => {
784
815
  });
785
816
 
786
817
  it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for video', async () => {
787
- await startStatsAnalyzer({expected: {receiveVideo: true}});
818
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
788
819
 
789
820
  // check that we haven't received any events yet
790
821
  checkReceivedEvent({expected: {}});
@@ -804,7 +835,7 @@ describe('plugin-meetings', () => {
804
835
  });
805
836
 
806
837
  it('emits REMOTE_MEDIA_STARTED and REMOTE_MEDIA_STOPPED events for share', async () => {
807
- await startStatsAnalyzer({expected: {receiveShare: true}});
838
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveShare: true}}});
808
839
 
809
840
  // check that we haven't received any events yet
810
841
  checkReceivedEvent({expected: {}});
@@ -824,7 +855,7 @@ describe('plugin-meetings', () => {
824
855
  });
825
856
 
826
857
  it('emits the correct MEDIA_QUALITY events', async () => {
827
- await startStatsAnalyzer({expected: {receiveVideo: true}});
858
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
828
859
 
829
860
  await progressTime();
830
861
 
@@ -833,7 +864,7 @@ describe('plugin-meetings', () => {
833
864
  });
834
865
 
835
866
  it('emits the correct transportType in MEDIA_QUALITY events', async () => {
836
- await startStatsAnalyzer({expected: {receiveVideo: true}});
867
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
837
868
 
838
869
  await progressTime();
839
870
 
@@ -847,7 +878,7 @@ describe('plugin-meetings', () => {
847
878
  fakeStats.audio.receivers[0].report[4].relayProtocol = 'tls';
848
879
  fakeStats.video.receivers[0].report[4].relayProtocol = 'tls';
849
880
 
850
- await startStatsAnalyzer({expected: {receiveVideo: true}});
881
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
851
882
 
852
883
  await progressTime();
853
884
 
@@ -856,19 +887,19 @@ describe('plugin-meetings', () => {
856
887
  });
857
888
 
858
889
  it('emits the correct peripherals in MEDIA_QUALITY events', async () => {
859
- await startStatsAnalyzer({expected: {receiveVideo: true}});
890
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
860
891
 
861
892
  await progressTime();
862
893
 
863
894
  assert.strictEqual(
864
895
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.MICROPHONE)
865
896
  .information,
866
- 'fake-microphone'
897
+ 'fake-microphone',
867
898
  );
868
899
  assert.strictEqual(
869
900
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.CAMERA)
870
901
  .information,
871
- 'fake-camera'
902
+ 'fake-camera',
872
903
  );
873
904
  });
874
905
 
@@ -876,30 +907,33 @@ describe('plugin-meetings', () => {
876
907
  fakeStats.audio.senders[0].localTrackLabel = undefined;
877
908
  fakeStats.video.senders[0].localTrackLabel = undefined;
878
909
 
879
- await startStatsAnalyzer({expected: {receiveVideo: true}});
910
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
880
911
 
881
912
  await progressTime();
882
913
 
883
914
  assert.strictEqual(
884
915
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.MICROPHONE)
885
916
  .information,
886
- _UNKNOWN_
917
+ _UNKNOWN_,
887
918
  );
888
919
  assert.strictEqual(
889
920
  mqeData.intervalMetadata.peripherals.find((val) => val.name === MEDIA_DEVICES.CAMERA)
890
921
  .information,
891
- _UNKNOWN_
922
+ _UNKNOWN_,
892
923
  );
893
924
  });
894
925
 
895
- it('emits the correct transmittedFrameRate/receivedFrameRate', async () => {
896
- it('at the start of the stats analyzer', async () => {
897
- 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 () => {
898
932
  assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.transmittedFrameRate, 0);
899
933
  assert.strictEqual(mqeData.videoReceive[0].streams[0].common.receivedFrameRate, 0);
900
934
  });
901
935
 
902
- 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 () => {
903
937
  fakeStats.video.senders[0].report[0].framesSent += 300;
904
938
  fakeStats.video.receivers[0].report[0].framesReceived += 300;
905
939
  await progressTime(MQA_INTERVAL);
@@ -910,9 +944,12 @@ describe('plugin-meetings', () => {
910
944
  });
911
945
  });
912
946
 
913
- it('emits the correct rtpPackets', async () => {
914
- it('at the start of the stats analyzer', async () => {
915
- 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 () => {
916
953
  assert.strictEqual(mqeData.audioTransmit[0].common.rtpPackets, 0);
917
954
  assert.strictEqual(mqeData.audioTransmit[0].streams[0].common.rtpPackets, 0);
918
955
  assert.strictEqual(mqeData.audioReceive[0].common.rtpPackets, 0);
@@ -923,7 +960,7 @@ describe('plugin-meetings', () => {
923
960
  assert.strictEqual(mqeData.videoReceive[0].streams[0].common.rtpPackets, 0);
924
961
  });
925
962
 
926
- it('after packets are sent', async () => {
963
+ it('should update the RTP packets count correctly after audio and video packets are sent', async () => {
927
964
  fakeStats.audio.senders[0].report[0].packetsSent += 5;
928
965
  fakeStats.video.senders[0].report[0].packetsSent += 5;
929
966
  await progressTime(MQA_INTERVAL);
@@ -934,7 +971,7 @@ describe('plugin-meetings', () => {
934
971
  assert.strictEqual(mqeData.videoTransmit[0].streams[0].common.rtpPackets, 5);
935
972
  });
936
973
 
937
- it('after packets are received', async () => {
974
+ it('should update the RTP packets count correctly after audio and video packets are received', async () => {
938
975
  fakeStats.audio.senders[0].report[0].packetsSent += 10;
939
976
  fakeStats.video.senders[0].report[0].packetsSent += 10;
940
977
  fakeStats.audio.receivers[0].report[0].packetsReceived += 10;
@@ -948,38 +985,83 @@ describe('plugin-meetings', () => {
948
985
  });
949
986
  });
950
987
 
951
- it('emits the correct fecPackets', async () => {
952
- it('at the start of the stats analyzer', async () => {
953
- 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 () => {
954
994
  assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 0);
955
995
  });
956
996
 
957
- it('after FEC packets are received', async () => {
997
+ it('should accurately report the count of FEC packets received', async () => {
958
998
  fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 5;
959
999
  await progressTime(MQA_INTERVAL);
960
1000
 
961
1001
  assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 5);
962
1002
  });
963
1003
 
964
- it('after FEC packets are received and some FEC packets are discarded', async () => {
1004
+ it('should accurately update and reset the FEC packet count based on received packets over MQA intervals', async () => {
965
1005
  fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 15;
966
- fakeStats.audio.receivers[0].report[0].fecPacketsDiscarded += 5;
967
1006
  await progressTime(MQA_INTERVAL);
1007
+ assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 15);
968
1008
 
969
- assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 10);
1009
+ fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 45;
1010
+ await progressTime(MQA_INTERVAL);
1011
+ assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 45);
1012
+
1013
+ await progressTime(MQA_INTERVAL);
1014
+ assert.strictEqual(mqeData.audioReceive[0].common.fecPackets, 0);
970
1015
  });
971
1016
  });
972
1017
 
973
- it('emits the correct mediaHopByHopLost/rtpHopByHopLost', async () => {
974
- it('at the start of the stats analyzer', async () => {
975
- await startStatsAnalyzer();
1018
+ describe('RTP recovered packets emission', async() => {
1019
+ beforeEach(async() => {
1020
+ await startStatsAnalyzer({pc, statsAnalyzer});
1021
+ });
1022
+
1023
+ it('should initially report zero RTP recovered packets', async() => {
1024
+ assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 0);
1025
+ })
1026
+
1027
+ it('should report RTP recovered packets equal to FEC packets received', async() => {
1028
+ fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 10;
1029
+
1030
+ await progressTime(MQA_INTERVAL);
1031
+ assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 10);
1032
+ })
1033
+
1034
+ it('should reset RTP recovered packets count after each interval', async () => {
1035
+ fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 100;
1036
+ await progressTime(MQA_INTERVAL);
1037
+ assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 100);
1038
+
1039
+ await progressTime(MQA_INTERVAL);
1040
+ assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 0);
1041
+ })
1042
+
1043
+ it('should correctly calculate RTP recovered packets after discarding FEC packets', async () => {
1044
+ fakeStats.audio.receivers[0].report[0].fecPacketsReceived += 100;
1045
+ fakeStats.audio.receivers[0].report[0].fecPacketsDiscarded += 20;
1046
+
1047
+ await progressTime(MQA_INTERVAL);
1048
+ assert.strictEqual(mqeData.audioReceive[0].common.rtpRecovered, 80);
1049
+ })
1050
+ })
1051
+
1052
+ describe('packet loss metrics reporting in stats analyzer', () => {
1053
+ beforeEach(async () => {
1054
+ await startStatsAnalyzer({pc, statsAnalyzer});
1055
+ });
1056
+
1057
+ it('should report zero packet loss for both audio and video at the start of the stats analyzer', async () => {
976
1058
  assert.strictEqual(mqeData.audioReceive[0].common.mediaHopByHopLost, 0);
977
1059
  assert.strictEqual(mqeData.audioReceive[0].common.rtpHopByHopLost, 0);
978
1060
  assert.strictEqual(mqeData.videoReceive[0].common.mediaHopByHopLost, 0);
979
1061
  assert.strictEqual(mqeData.videoReceive[0].common.rtpHopByHopLost, 0);
980
1062
  });
981
1063
 
982
- it('after packets are lost', async () => {
1064
+ it('should update packet loss metrics correctly for both audio and video after packet loss is detected', async () => {
983
1065
  fakeStats.audio.receivers[0].report[0].packetsLost += 5;
984
1066
  fakeStats.video.receivers[0].report[0].packetsLost += 5;
985
1067
  await progressTime(MQA_INTERVAL);
@@ -991,14 +1073,17 @@ describe('plugin-meetings', () => {
991
1073
  });
992
1074
  });
993
1075
 
994
- it('emits the correct remoteLossRate', async () => {
995
- it('at the start of the stats analyzer', async () => {
996
- await startStatsAnalyzer();
1076
+ describe('remote loss rate reporting in stats analyzer', () => {
1077
+ beforeEach(async () => {
1078
+ await startStatsAnalyzer({pc, statsAnalyzer});
1079
+ });
1080
+
1081
+ it('should report a zero remote loss rate for both audio and video at the start', async () => {
997
1082
  assert.strictEqual(mqeData.audioTransmit[0].common.remoteLossRate, 0);
998
1083
  assert.strictEqual(mqeData.videoTransmit[0].common.remoteLossRate, 0);
999
1084
  });
1000
1085
 
1001
- it('after packets are sent', async () => {
1086
+ it('should maintain a zero remote loss rate for both audio and video after packets are sent without loss', async () => {
1002
1087
  fakeStats.audio.senders[0].report[0].packetsSent += 100;
1003
1088
  fakeStats.video.senders[0].report[0].packetsSent += 100;
1004
1089
  await progressTime(MQA_INTERVAL);
@@ -1007,7 +1092,7 @@ describe('plugin-meetings', () => {
1007
1092
  assert.strictEqual(mqeData.videoTransmit[0].common.remoteLossRate, 0);
1008
1093
  });
1009
1094
 
1010
- it('after packets are sent and some packets are lost', async () => {
1095
+ it('should accurately calculate the remote loss rate for both audio and video after packet loss is detected', async () => {
1011
1096
  fakeStats.audio.senders[0].report[0].packetsSent += 200;
1012
1097
  fakeStats.audio.senders[0].report[1].packetsLost += 10;
1013
1098
  fakeStats.video.senders[0].report[0].packetsSent += 200;
@@ -1020,7 +1105,7 @@ describe('plugin-meetings', () => {
1020
1105
  });
1021
1106
 
1022
1107
  it('has the correct localIpAddress set when the candidateType is host', async () => {
1023
- await startStatsAnalyzer();
1108
+ await startStatsAnalyzer({pc, statsAnalyzer});
1024
1109
 
1025
1110
  await progressTime();
1026
1111
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -1030,7 +1115,7 @@ describe('plugin-meetings', () => {
1030
1115
  });
1031
1116
 
1032
1117
  it('has the correct localIpAddress set when the candidateType is prflx and relayProtocol is set', async () => {
1033
- await startStatsAnalyzer();
1118
+ await startStatsAnalyzer({pc, statsAnalyzer});
1034
1119
 
1035
1120
  await progressTime();
1036
1121
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -1044,7 +1129,7 @@ describe('plugin-meetings', () => {
1044
1129
  });
1045
1130
 
1046
1131
  it('has the correct localIpAddress set when the candidateType is prflx and relayProtocol is not set', async () => {
1047
- await startStatsAnalyzer();
1132
+ await startStatsAnalyzer({pc, statsAnalyzer});
1048
1133
 
1049
1134
  await progressTime();
1050
1135
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -1058,7 +1143,7 @@ describe('plugin-meetings', () => {
1058
1143
  });
1059
1144
 
1060
1145
  it('has no localIpAddress set when the candidateType is invalid', async () => {
1061
- await startStatsAnalyzer();
1146
+ await startStatsAnalyzer({pc, statsAnalyzer});
1062
1147
 
1063
1148
  await progressTime();
1064
1149
  assert.strictEqual(statsAnalyzer.getLocalIpAddress(), '');
@@ -1069,8 +1154,10 @@ describe('plugin-meetings', () => {
1069
1154
 
1070
1155
  it('logs a message when audio send packets do not increase', async () => {
1071
1156
  await startStatsAnalyzer(
1072
- {expected: {sendAudio: true}},
1073
- {audio: {local: EVENTS.LOCAL_MEDIA_STARTED}}
1157
+ {
1158
+ statsAnalyzer, pc, mediaStatus: {expected: {sendAudio: true}},
1159
+ lastEmittedEvents: {audio: {local: EVENTS.LOCAL_MEDIA_STARTED}},
1160
+ },
1074
1161
  );
1075
1162
 
1076
1163
  // don't increase the packets when time progresses.
@@ -1078,15 +1165,17 @@ describe('plugin-meetings', () => {
1078
1165
 
1079
1166
  assert(
1080
1167
  loggerSpy.calledWith(
1081
- 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent'
1082
- )
1168
+ 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent',
1169
+ ),
1083
1170
  );
1084
1171
  });
1085
1172
 
1086
1173
  it('does not log a message when audio send packets increase', async () => {
1087
- await startStatsAnalyzer(
1088
- {expected: {sendAudio: true}},
1089
- {audio: {local: EVENTS.LOCAL_MEDIA_STOPPED}}
1174
+ await startStatsAnalyzer({
1175
+ statsAnalyzer, pc,
1176
+ mediaStatus: {expected: {sendAudio: true}},
1177
+ lastEmittedEvents: {audio: {local: EVENTS.LOCAL_MEDIA_STOPPED}},
1178
+ },
1090
1179
  );
1091
1180
 
1092
1181
  fakeStats.audio.senders[0].report[0].packetsSent += 5;
@@ -1094,15 +1183,16 @@ describe('plugin-meetings', () => {
1094
1183
 
1095
1184
  assert(
1096
1185
  loggerSpy.neverCalledWith(
1097
- 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent'
1098
- )
1186
+ 'StatsAnalyzer:index#compareLastStatsResult --> No audio RTP packets sent',
1187
+ ),
1099
1188
  );
1100
1189
  });
1101
1190
 
1102
1191
  it('logs a message when video send packets do not increase', async () => {
1103
- await startStatsAnalyzer(
1104
- {expected: {sendVideo: true}},
1105
- {video: {local: EVENTS.LOCAL_MEDIA_STARTED}}
1192
+ await startStatsAnalyzer({
1193
+ statsAnalyzer, pc, mediaStatus: {expected: {sendVideo: true}},
1194
+ lastEmittedEvents: {video: {local: EVENTS.LOCAL_MEDIA_STARTED}},
1195
+ },
1106
1196
  );
1107
1197
 
1108
1198
  // don't increase the packets when time progresses.
@@ -1110,31 +1200,42 @@ describe('plugin-meetings', () => {
1110
1200
 
1111
1201
  assert(
1112
1202
  loggerSpy.calledWith(
1113
- 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent'
1114
- )
1203
+ 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent',
1204
+ ),
1115
1205
  );
1116
1206
  });
1117
1207
 
1118
1208
  it('does not log a message when video send packets increase', async () => {
1119
1209
  await startStatsAnalyzer(
1120
- {expected: {sendVideo: true}},
1121
- {video: {local: EVENTS.LOCAL_MEDIA_STOPPED}}
1122
- );
1210
+ {
1211
+ statsAnalyzer, pc,
1212
+ mediaStatus: {
1213
+ expected: {
1214
+ sendVideo: true,
1215
+ },
1216
+ },
1217
+ lastEmittedEvents: {
1218
+ video: {
1219
+ local: EVENTS.LOCAL_MEDIA_STOPPED,
1220
+ },
1221
+ },
1222
+ });
1123
1223
 
1124
1224
  fakeStats.video.senders[0].report[0].packetsSent += 5;
1125
1225
  await progressTime();
1126
1226
 
1127
1227
  assert(
1128
1228
  loggerSpy.neverCalledWith(
1129
- 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent'
1130
- )
1229
+ 'StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent',
1230
+ ),
1131
1231
  );
1132
1232
  });
1133
1233
 
1134
1234
  it('logs a message when share send packets do not increase', async () => {
1135
- await startStatsAnalyzer(
1136
- {expected: {sendShare: true}},
1137
- {share: {local: EVENTS.LOCAL_MEDIA_STARTED}}
1235
+ await startStatsAnalyzer({
1236
+ pc, mediaStatus: {expected: {sendShare: true}},
1237
+ lastEmittedEvents: {share: {local: EVENTS.LOCAL_MEDIA_STARTED}}, statsAnalyzer,
1238
+ },
1138
1239
  );
1139
1240
 
1140
1241
  // don't increase the packets when time progresses.
@@ -1142,15 +1243,16 @@ describe('plugin-meetings', () => {
1142
1243
 
1143
1244
  assert(
1144
1245
  loggerSpy.calledWith(
1145
- 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent'
1146
- )
1246
+ 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent',
1247
+ ),
1147
1248
  );
1148
1249
  });
1149
1250
 
1150
1251
  it('does not log a message when share send packets increase', async () => {
1151
- await startStatsAnalyzer(
1152
- {expected: {sendShare: true}},
1153
- {share: {local: EVENTS.LOCAL_MEDIA_STOPPED}}
1252
+ await startStatsAnalyzer({
1253
+ pc, statsAnalyzer, mediaStatus: {expected: {sendShare: true}},
1254
+ lastEmittedEvents: {share: {local: EVENTS.LOCAL_MEDIA_STOPPED}},
1255
+ },
1154
1256
  );
1155
1257
 
1156
1258
  fakeStats.share.senders[0].report[0].packetsSent += 5;
@@ -1158,8 +1260,8 @@ describe('plugin-meetings', () => {
1158
1260
 
1159
1261
  assert(
1160
1262
  loggerSpy.neverCalledWith(
1161
- 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent'
1162
- )
1263
+ 'StatsAnalyzer:index#compareLastStatsResult --> No share RTP packets sent',
1264
+ ),
1163
1265
  );
1164
1266
  });
1165
1267
 
@@ -1172,7 +1274,7 @@ describe('plugin-meetings', () => {
1172
1274
  id: '4',
1173
1275
  };
1174
1276
 
1175
- await startStatsAnalyzer();
1277
+ await startStatsAnalyzer({pc, statsAnalyzer});
1176
1278
 
1177
1279
  // don't increase the packets when time progresses.
1178
1280
  await progressTime();
@@ -1180,10 +1282,10 @@ describe('plugin-meetings', () => {
1180
1282
  assert.neverCalledWith(
1181
1283
  loggerSpy,
1182
1284
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot id: "4" and csi: 2. Total packets received on slot: ',
1183
- 0
1285
+ 0,
1184
1286
  );
1185
1287
  });
1186
- }
1288
+ },
1187
1289
  );
1188
1290
 
1189
1291
  it(`logs a message if no packets are sent`, async () => {
@@ -1192,7 +1294,7 @@ describe('plugin-meetings', () => {
1192
1294
  csi: 2,
1193
1295
  id: '4',
1194
1296
  };
1195
- await startStatsAnalyzer();
1297
+ await startStatsAnalyzer({pc, statsAnalyzer});
1196
1298
 
1197
1299
  // don't increase the packets when time progresses.
1198
1300
  await progressTime();
@@ -1200,52 +1302,52 @@ describe('plugin-meetings', () => {
1200
1302
  assert.calledWith(
1201
1303
  loggerSpy,
1202
1304
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1203
- 0
1305
+ 0,
1204
1306
  );
1205
1307
 
1206
1308
  assert.calledWith(
1207
1309
  loggerSpy,
1208
1310
  'StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total frames received on slot: ',
1209
- 0
1311
+ 0,
1210
1312
  );
1211
1313
 
1212
1314
  assert.calledWith(
1213
1315
  loggerSpy,
1214
1316
  'StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: video-recv-0, receive slot id: "4" and csi: 2. Total frames decoded on slot: ',
1215
- 0
1317
+ 0,
1216
1318
  );
1217
1319
 
1218
1320
  assert.calledWith(
1219
1321
  loggerSpy,
1220
1322
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: audio-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1221
- 0
1323
+ 0,
1222
1324
  );
1223
1325
 
1224
1326
  assert.calledWith(
1225
1327
  loggerSpy,
1226
1328
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1227
- 0
1329
+ 0,
1228
1330
  );
1229
1331
 
1230
1332
  assert.calledWith(
1231
1333
  loggerSpy,
1232
1334
  'StatsAnalyzer:index#processInboundRTPResult --> No frames received for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total frames received on slot: ',
1233
- 0
1335
+ 0,
1234
1336
  );
1235
1337
  assert.calledWith(
1236
1338
  loggerSpy,
1237
1339
  'StatsAnalyzer:index#processInboundRTPResult --> No frames decoded for mediaType: video-share-recv-0, receive slot id: "4" and csi: 2. Total frames decoded on slot: ',
1238
- 0
1340
+ 0,
1239
1341
  );
1240
1342
  assert.calledWith(
1241
1343
  loggerSpy,
1242
1344
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for mediaType: audio-share-recv-0, receive slot id: "4" and csi: 2. Total packets received on slot: ',
1243
- 0
1345
+ 0,
1244
1346
  );
1245
1347
  });
1246
1348
 
1247
1349
  it(`does not log a message if receiveSlot is undefined`, async () => {
1248
- await startStatsAnalyzer();
1350
+ await startStatsAnalyzer({pc, statsAnalyzer});
1249
1351
 
1250
1352
  // don't increase the packets when time progresses.
1251
1353
  await progressTime();
@@ -1253,12 +1355,12 @@ describe('plugin-meetings', () => {
1253
1355
  assert.neverCalledWith(
1254
1356
  loggerSpy,
1255
1357
  'StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot "". Total packets received on slot: ',
1256
- 0
1358
+ 0,
1257
1359
  );
1258
1360
  });
1259
1361
 
1260
1362
  it('has the correct number of senders and receivers (2)', async () => {
1261
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1363
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1262
1364
 
1263
1365
  await progressTime();
1264
1366
 
@@ -1269,7 +1371,7 @@ describe('plugin-meetings', () => {
1269
1371
  });
1270
1372
 
1271
1373
  it('has one stream per sender/reciever', async () => {
1272
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1374
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1273
1375
 
1274
1376
  await progressTime();
1275
1377
 
@@ -1456,7 +1558,7 @@ describe('plugin-meetings', () => {
1456
1558
  framesDropped: 0,
1457
1559
  },
1458
1560
  h264CodecProfile: 'BP',
1459
- isActiveSpeaker: true,
1561
+ isActiveSpeaker: false,
1460
1562
  optimalFrameSize: 0,
1461
1563
  receivedFrameSize: 3600,
1462
1564
  receivedHeight: 720,
@@ -1490,7 +1592,7 @@ describe('plugin-meetings', () => {
1490
1592
  framesDropped: 0,
1491
1593
  },
1492
1594
  h264CodecProfile: 'BP',
1493
- isActiveSpeaker: true,
1595
+ isActiveSpeaker: false,
1494
1596
  optimalFrameSize: 0,
1495
1597
  receivedFrameSize: 3600,
1496
1598
  receivedHeight: 720,
@@ -1529,7 +1631,7 @@ describe('plugin-meetings', () => {
1529
1631
  },
1530
1632
  });
1531
1633
 
1532
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1634
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
1533
1635
 
1534
1636
  await progressTime();
1535
1637
 
@@ -1554,7 +1656,7 @@ describe('plugin-meetings', () => {
1554
1656
  framesDropped: 0,
1555
1657
  },
1556
1658
  h264CodecProfile: 'BP',
1557
- isActiveSpeaker: true,
1659
+ isActiveSpeaker: false,
1558
1660
  optimalFrameSize: 0,
1559
1661
  receivedFrameSize: 3600,
1560
1662
  receivedHeight: 720,
@@ -1586,7 +1688,7 @@ describe('plugin-meetings', () => {
1586
1688
  framesDropped: 0,
1587
1689
  },
1588
1690
  h264CodecProfile: 'BP',
1589
- isActiveSpeaker: true,
1691
+ isActiveSpeaker: false,
1590
1692
  optimalFrameSize: 0,
1591
1693
  receivedFrameSize: 3600,
1592
1694
  receivedHeight: 720,
@@ -1618,7 +1720,7 @@ describe('plugin-meetings', () => {
1618
1720
  framesDropped: 0,
1619
1721
  },
1620
1722
  h264CodecProfile: 'BP',
1621
- isActiveSpeaker: true,
1723
+ isActiveSpeaker: false,
1622
1724
  optimalFrameSize: 0,
1623
1725
  receivedFrameSize: 3600,
1624
1726
  receivedHeight: 720,
@@ -1633,186 +1735,358 @@ describe('plugin-meetings', () => {
1633
1735
  ]);
1634
1736
  });
1635
1737
 
1636
- it('has three streams for video senders for simulcast', async () => {
1637
- pc.getTransceiverStats = sinon.stub().resolves({
1638
- audio: {
1639
- senders: [fakeStats.audio.senders[0]],
1640
- receivers: [fakeStats.audio.receivers[0]],
1641
- },
1642
- video: {
1643
- senders: [
1644
- {
1645
- localTrackLabel: 'fake-camera',
1646
- report: [
1647
- {
1648
- type: 'outbound-rtp',
1649
- bytesSent: 1,
1650
- framesSent: 0,
1651
- packetsSent: 0,
1652
- },
1653
- {
1654
- type: 'outbound-rtp',
1655
- bytesSent: 0,
1656
- framesSent: 0,
1657
- packetsSent: 0,
1658
- },
1659
- {
1660
- type: 'outbound-rtp',
1661
- bytesSent: 1000,
1662
- framesSent: 1,
1663
- packetsSent: 1,
1664
- },
1665
- {
1666
- type: 'remote-inbound-rtp',
1667
- packetsLost: 0,
1668
- },
1669
- {
1670
- type: 'candidate-pair',
1671
- state: 'succeeded',
1672
- localCandidateId: 'fake-candidate-id',
1673
- },
1674
- {
1675
- type: 'candidate-pair',
1676
- state: 'failed',
1677
- localCandidateId: 'bad-candidate-id',
1678
- },
1679
- {
1680
- type: 'local-candidate',
1681
- id: 'fake-candidate-id',
1682
- protocol: 'tcp',
1683
- },
1684
- ],
1685
- },
1686
- ],
1687
- receivers: [fakeStats.video.receivers[0]],
1688
- },
1689
- screenShareAudio: {
1690
- senders: [fakeStats.audio.senders[0]],
1691
- receivers: [fakeStats.audio.receivers[0]],
1692
- },
1693
- screenShareVideo: {
1694
- senders: [fakeStats.video.senders[0]],
1695
- receivers: [fakeStats.video.receivers[0]],
1696
- },
1697
- });
1698
-
1699
- await startStatsAnalyzer({expected: {receiveVideo: true}});
1738
+ describe('stream count for simulcast', async () => {
1739
+ it('has three streams for video senders for simulcast', async () => {
1740
+ pc.getTransceiverStats = sinon.stub().resolves({
1741
+ audio: {
1742
+ senders: [fakeStats.audio.senders[0]],
1743
+ receivers: [fakeStats.audio.receivers[0]],
1744
+ },
1745
+ video: {
1746
+ senders: [
1747
+ {
1748
+ localTrackLabel: 'fake-camera',
1749
+ report: [
1750
+ {
1751
+ type: 'outbound-rtp',
1752
+ bytesSent: 1,
1753
+ framesSent: 0,
1754
+ packetsSent: 0,
1755
+ isRequested: true,
1756
+ },
1757
+ {
1758
+ type: 'outbound-rtp',
1759
+ bytesSent: 1,
1760
+ framesSent: 0,
1761
+ packetsSent: 1,
1762
+ isRequested: true,
1763
+ },
1764
+ {
1765
+ type: 'outbound-rtp',
1766
+ bytesSent: 1000,
1767
+ framesSent: 1,
1768
+ packetsSent: 0,
1769
+ isRequested: true,
1770
+ },
1771
+ {
1772
+ type: 'remote-inbound-rtp',
1773
+ packetsLost: 0,
1774
+ },
1775
+ {
1776
+ type: 'candidate-pair',
1777
+ state: 'succeeded',
1778
+ localCandidateId: 'fake-candidate-id',
1779
+ },
1780
+ {
1781
+ type: 'candidate-pair',
1782
+ state: 'failed',
1783
+ localCandidateId: 'bad-candidate-id',
1784
+ },
1785
+ {
1786
+ type: 'local-candidate',
1787
+ id: 'fake-candidate-id',
1788
+ protocol: 'tcp',
1789
+ },
1790
+ ],
1791
+ },
1792
+ ],
1793
+ receivers: [fakeStats.video.receivers[0]],
1794
+ },
1795
+ screenShareAudio: {
1796
+ senders: [fakeStats.audio.senders[0]],
1797
+ receivers: [fakeStats.audio.receivers[0]],
1798
+ },
1799
+ screenShareVideo: {
1800
+ senders: [fakeStats.video.senders[0]],
1801
+ receivers: [fakeStats.video.receivers[0]],
1802
+ },
1803
+ });
1700
1804
 
1701
- await progressTime();
1805
+ await startStatsAnalyzer({
1806
+ pc,
1807
+ statsAnalyzer,
1808
+ mediaStatus: {
1809
+ expected: {
1810
+ receiveVideo: true,
1811
+ },
1812
+ },
1813
+ });
1702
1814
 
1703
- assert.deepEqual(mqeData.videoTransmit[0].streams, [
1704
- {
1705
- common: {
1706
- codec: 'H264',
1707
- csi: [],
1708
- duplicateSsci: 0,
1815
+ await progressTime();
1816
+
1817
+ assert.deepEqual(mqeData.videoTransmit[0].streams, [
1818
+ {
1819
+ common: {
1820
+ codec: 'H264',
1821
+ csi: [],
1822
+ duplicateSsci: 0,
1823
+ requestedBitrate: 0,
1824
+ requestedFrames: 0,
1825
+ rtpPackets: 0,
1826
+ ssci: 0,
1827
+ transmittedBitrate: 0.13333333333333333,
1828
+ transmittedFrameRate: 0,
1829
+ },
1830
+ h264CodecProfile: 'BP',
1831
+ isAvatar: false,
1832
+ isHardwareEncoded: false,
1833
+ localConfigurationChanges: 2,
1834
+ maxFrameQp: 0,
1835
+ maxNoiseLevel: 0,
1836
+ minRegionQp: 0,
1837
+ remoteConfigurationChanges: 0,
1838
+ requestedFrameSize: 0,
1839
+ requestedKeyFrames: 0,
1840
+ transmittedFrameSize: 0,
1841
+ transmittedHeight: 0,
1842
+ transmittedKeyFrames: 0,
1843
+ transmittedKeyFramesClient: 0,
1844
+ transmittedKeyFramesConfigurationChange: 0,
1845
+ transmittedKeyFramesFeedback: 0,
1846
+ transmittedKeyFramesLocalDrop: 0,
1847
+ transmittedKeyFramesOtherLayer: 0,
1848
+ transmittedKeyFramesPeriodic: 0,
1849
+ transmittedKeyFramesSceneChange: 0,
1850
+ transmittedKeyFramesStartup: 0,
1851
+ transmittedKeyFramesUnknown: 0,
1852
+ transmittedWidth: 0,
1709
1853
  requestedBitrate: 0,
1710
- requestedFrames: 0,
1711
- rtpPackets: 0,
1712
- ssci: 0,
1713
- transmittedBitrate: 0.13333333333333333,
1714
- transmittedFrameRate: 0
1715
1854
  },
1716
- h264CodecProfile: 'BP',
1717
- isAvatar: false,
1718
- isHardwareEncoded: false,
1719
- localConfigurationChanges: 2,
1720
- maxFrameQp: 0,
1721
- maxNoiseLevel: 0,
1722
- minRegionQp: 0,
1723
- remoteConfigurationChanges: 0,
1724
- requestedFrameSize: 0,
1725
- requestedKeyFrames: 0,
1726
- transmittedFrameSize: 0,
1727
- transmittedHeight: 0,
1728
- transmittedKeyFrames: 0,
1729
- transmittedKeyFramesClient: 0,
1730
- transmittedKeyFramesConfigurationChange: 0,
1731
- transmittedKeyFramesFeedback: 0,
1732
- transmittedKeyFramesLocalDrop: 0,
1733
- transmittedKeyFramesOtherLayer: 0,
1734
- transmittedKeyFramesPeriodic: 0,
1735
- transmittedKeyFramesSceneChange: 0,
1736
- transmittedKeyFramesStartup: 0,
1737
- transmittedKeyFramesUnknown: 0,
1738
- transmittedWidth: 0,
1739
- requestedBitrate: 0,
1740
- },
1741
- {
1742
- common: {
1743
- codec: 'H264',
1744
- csi: [],
1745
- duplicateSsci: 0,
1855
+ {
1856
+ common: {
1857
+ codec: 'H264',
1858
+ csi: [],
1859
+ duplicateSsci: 0,
1860
+ requestedBitrate: 0,
1861
+ requestedFrames: 0,
1862
+ rtpPackets: 1,
1863
+ ssci: 0,
1864
+ transmittedBitrate: 0.13333333333333333,
1865
+ transmittedFrameRate: 0,
1866
+ },
1867
+ h264CodecProfile: 'BP',
1868
+ isAvatar: false,
1869
+ isHardwareEncoded: false,
1870
+ localConfigurationChanges: 2,
1871
+ maxFrameQp: 0,
1872
+ maxNoiseLevel: 0,
1873
+ minRegionQp: 0,
1874
+ remoteConfigurationChanges: 0,
1875
+ requestedFrameSize: 0,
1876
+ requestedKeyFrames: 0,
1877
+ transmittedFrameSize: 0,
1878
+ transmittedHeight: 0,
1879
+ transmittedKeyFrames: 0,
1880
+ transmittedKeyFramesClient: 0,
1881
+ transmittedKeyFramesConfigurationChange: 0,
1882
+ transmittedKeyFramesFeedback: 0,
1883
+ transmittedKeyFramesLocalDrop: 0,
1884
+ transmittedKeyFramesOtherLayer: 0,
1885
+ transmittedKeyFramesPeriodic: 0,
1886
+ transmittedKeyFramesSceneChange: 0,
1887
+ transmittedKeyFramesStartup: 0,
1888
+ transmittedKeyFramesUnknown: 0,
1889
+ transmittedWidth: 0,
1746
1890
  requestedBitrate: 0,
1747
- requestedFrames: 0,
1748
- rtpPackets: 0,
1749
- ssci: 0,
1750
- transmittedBitrate: 0,
1751
- transmittedFrameRate: 0,
1752
1891
  },
1753
- h264CodecProfile: 'BP',
1754
- isAvatar: false,
1755
- isHardwareEncoded: false,
1756
- localConfigurationChanges: 2,
1757
- maxFrameQp: 0,
1758
- maxNoiseLevel: 0,
1759
- minRegionQp: 0,
1760
- remoteConfigurationChanges: 0,
1761
- requestedFrameSize: 0,
1762
- requestedKeyFrames: 0,
1763
- transmittedFrameSize: 0,
1764
- transmittedHeight: 0,
1765
- transmittedKeyFrames: 0,
1766
- transmittedKeyFramesClient: 0,
1767
- transmittedKeyFramesConfigurationChange: 0,
1768
- transmittedKeyFramesFeedback: 0,
1769
- transmittedKeyFramesLocalDrop: 0,
1770
- transmittedKeyFramesOtherLayer: 0,
1771
- transmittedKeyFramesPeriodic: 0,
1772
- transmittedKeyFramesSceneChange: 0,
1773
- transmittedKeyFramesStartup: 0,
1774
- transmittedKeyFramesUnknown: 0,
1775
- transmittedWidth: 0,
1776
- requestedBitrate: 0,
1777
- },
1778
- {
1779
- common: {
1780
- codec: 'H264',
1781
- csi: [],
1782
- duplicateSsci: 0,
1892
+ {
1893
+ common: {
1894
+ codec: 'H264',
1895
+ csi: [],
1896
+ duplicateSsci: 0,
1897
+ requestedBitrate: 0,
1898
+ requestedFrames: 0,
1899
+ rtpPackets: 0,
1900
+ ssci: 0,
1901
+ transmittedBitrate: 133.33333333333334,
1902
+ transmittedFrameRate: 0,
1903
+ },
1904
+ h264CodecProfile: 'BP',
1905
+ isAvatar: false,
1906
+ isHardwareEncoded: false,
1907
+ localConfigurationChanges: 2,
1908
+ maxFrameQp: 0,
1909
+ maxNoiseLevel: 0,
1910
+ minRegionQp: 0,
1911
+ remoteConfigurationChanges: 0,
1912
+ requestedFrameSize: 0,
1913
+ requestedKeyFrames: 0,
1914
+ transmittedFrameSize: 0,
1915
+ transmittedHeight: 0,
1916
+ transmittedKeyFrames: 0,
1917
+ transmittedKeyFramesClient: 0,
1918
+ transmittedKeyFramesConfigurationChange: 0,
1919
+ transmittedKeyFramesFeedback: 0,
1920
+ transmittedKeyFramesLocalDrop: 0,
1921
+ transmittedKeyFramesOtherLayer: 0,
1922
+ transmittedKeyFramesPeriodic: 0,
1923
+ transmittedKeyFramesSceneChange: 0,
1924
+ transmittedKeyFramesStartup: 0,
1925
+ transmittedKeyFramesUnknown: 0,
1926
+ transmittedWidth: 0,
1783
1927
  requestedBitrate: 0,
1784
- requestedFrames: 0,
1785
- rtpPackets: 1,
1786
- ssci: 0,
1787
- transmittedBitrate: 133.33333333333334,
1788
- transmittedFrameRate: 0,
1789
1928
  },
1790
- h264CodecProfile: 'BP',
1791
- isAvatar: false,
1792
- isHardwareEncoded: false,
1793
- localConfigurationChanges: 2,
1794
- maxFrameQp: 0,
1795
- maxNoiseLevel: 0,
1796
- minRegionQp: 0,
1797
- remoteConfigurationChanges: 0,
1798
- requestedFrameSize: 0,
1799
- requestedKeyFrames: 0,
1800
- transmittedFrameSize: 0,
1801
- transmittedHeight: 0,
1802
- transmittedKeyFrames: 0,
1803
- transmittedKeyFramesClient: 0,
1804
- transmittedKeyFramesConfigurationChange: 0,
1805
- transmittedKeyFramesFeedback: 0,
1806
- transmittedKeyFramesLocalDrop: 0,
1807
- transmittedKeyFramesOtherLayer: 0,
1808
- transmittedKeyFramesPeriodic: 0,
1809
- transmittedKeyFramesSceneChange: 0,
1810
- transmittedKeyFramesStartup: 0,
1811
- transmittedKeyFramesUnknown: 0,
1812
- transmittedWidth: 0,
1813
- requestedBitrate: 0,
1929
+ ]);
1930
+ });
1931
+ });
1932
+ describe('active speaker status emission', async () => {
1933
+ beforeEach(async () => {
1934
+ await startStatsAnalyzer({pc, statsAnalyzer});
1935
+ performance.timeOrigin = 1;
1936
+ });
1937
+
1938
+ it('reports active speaker as true when the participant has been speaking', async () => {
1939
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = true;
1940
+ await progressTime(5 * MQA_INTERVAL);
1941
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, true);
1942
+ });
1943
+
1944
+ it('reports active speaker as false when the participant has not spoken', async () => {
1945
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = false;
1946
+ await progressTime(5 * MQA_INTERVAL);
1947
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false);
1948
+ });
1949
+
1950
+ it('defaults to false when active speaker status is indeterminate', async () => {
1951
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = undefined;
1952
+ await progressTime(MQA_INTERVAL);
1953
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false);
1954
+ });
1955
+
1956
+ it('updates active speaker to true following a recent status change to speaking', async () => {
1957
+ fakeStats.video.receivers[0].report[0].isActiveSpeaker = false;
1958
+ fakeStats.video.receivers[0].report[0].lastActiveSpeakerUpdateTimestamp = performance.timeOrigin + performance.now() + (30 * 1000);
1959
+ await progressTime(MQA_INTERVAL);
1960
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, true);
1961
+ await progressTime(MQA_INTERVAL);
1962
+ assert.strictEqual(mqeData.videoReceive[0].streams[0].isActiveSpeaker, false);
1963
+ });
1964
+ });
1965
+ describe('sends streams according to their is requested flag', async () => {
1966
+
1967
+ beforeEach(async () => {
1968
+ performance.timeOrigin = 0;
1969
+ await startStatsAnalyzer({pc, statsAnalyzer});
1970
+ });
1971
+
1972
+ it('should send a stream if it is requested', async () => {
1973
+ fakeStats.audio.senders[0].report[0].isRequested = true;
1974
+ await progressTime(MQA_INTERVAL);
1975
+ assert.strictEqual(mqeData.audioTransmit[0].streams.length, 1);
1976
+ });
1977
+
1978
+ it('should not sent a stream if its is requested flag is undefined', async () => {
1979
+ fakeStats.audio.senders[0].report[0].isRequested = undefined;
1980
+ await progressTime(MQA_INTERVAL);
1981
+ assert.strictEqual(mqeData.audioTransmit[0].streams.length, 0);
1982
+ });
1983
+
1984
+ it('should not send a stream if it is not requested', async () => {
1985
+ fakeStats.audio.receivers[0].report[0].isRequested = false;
1986
+ await progressTime(MQA_INTERVAL);
1987
+ assert.strictEqual(mqeData.audioReceive[0].streams.length, 0);
1988
+ });
1989
+
1990
+ it('should send the stream if it was recently requested', async () => {
1991
+ fakeStats.audio.receivers[0].report[0].lastRequestedUpdateTimestamp = performance.timeOrigin + performance.now() + (30 * 1000);
1992
+ fakeStats.audio.receivers[0].report[0].isRequested = false;
1993
+ await progressTime(MQA_INTERVAL);
1994
+ assert.strictEqual(mqeData.audioReceive[0].streams.length, 1);
1995
+ await progressTime(MQA_INTERVAL);
1996
+ assert.strictEqual(mqeData.audioReceive[0].streams.length, 0);
1997
+ });
1998
+ });
1999
+
2000
+ describe('window and screen size emission', async () => {
2001
+ beforeEach(async () => {
2002
+ await startStatsAnalyzer({pc, statsAnalyzer});
2003
+ });
2004
+
2005
+ it('should record the screen size from window.screen properties', async () => {
2006
+ sinon.stub(window.screen, 'width').get(() => 1280);
2007
+ sinon.stub(window.screen, 'height').get(() => 720);
2008
+ await progressTime(MQA_INTERVAL);
2009
+ assert.strictEqual(mqeData.intervalMetadata.screenWidth, 1280);
2010
+ assert.strictEqual(mqeData.intervalMetadata.screenHeight, 720);
2011
+ assert.strictEqual(mqeData.intervalMetadata.screenResolution, 3600);
2012
+ });
2013
+
2014
+ it('should record the initial app window size from window properties', async () => {
2015
+ sinon.stub(window, 'innerWidth').get(() => 720);
2016
+ sinon.stub(window, 'innerHeight').get(() => 360);
2017
+ await progressTime(MQA_INTERVAL);
2018
+ assert.strictEqual(mqeData.intervalMetadata.appWindowWidth, 720);
2019
+ assert.strictEqual(mqeData.intervalMetadata.appWindowHeight, 360);
2020
+ assert.strictEqual(mqeData.intervalMetadata.appWindowSize, 1013);
2021
+
2022
+ sinon.stub(window, 'innerWidth').get(() => 1080);
2023
+ sinon.stub(window, 'innerHeight').get(() => 720);
2024
+ await progressTime(MQA_INTERVAL);
2025
+ assert.strictEqual(mqeData.intervalMetadata.appWindowWidth, 1080);
2026
+ assert.strictEqual(mqeData.intervalMetadata.appWindowHeight, 720);
2027
+ assert.strictEqual(mqeData.intervalMetadata.appWindowSize, 3038);
2028
+ });
2029
+ });
2030
+
2031
+ describe('sends multistreamEnabled', async () => {
2032
+ it('false if StatsAnalyzer initialized with default value for isMultistream', async () => {
2033
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
2034
+
2035
+ await progressTime();
2036
+
2037
+ for (const data of [
2038
+ mqeData.audioTransmit,
2039
+ mqeData.audioReceive,
2040
+ mqeData.videoTransmit,
2041
+ mqeData.videoReceive,
2042
+ ]) {
2043
+ assert.strictEqual(data[0].common.common.multistreamEnabled, false);
1814
2044
  }
1815
- ]);
2045
+ });
2046
+
2047
+ it('false if StatsAnalyzer initialized with false', async () => {
2048
+ statsAnalyzer = new StatsAnalyzer({
2049
+ config: initialConfig,
2050
+ receiveSlotCallback: () => receiveSlot,
2051
+ networkQualityMonitor,
2052
+ isMultistream: false,
2053
+ });
2054
+ registerStatsAnalyzerEvents(statsAnalyzer);
2055
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: false}}});
2056
+
2057
+ await progressTime();
2058
+
2059
+ for (const data of [
2060
+ mqeData.audioTransmit,
2061
+ mqeData.audioReceive,
2062
+ mqeData.videoTransmit,
2063
+ mqeData.videoReceive,
2064
+ ]) {
2065
+ assert.strictEqual(data[0].common.common.multistreamEnabled, false);
2066
+ }
2067
+ });
2068
+
2069
+ it('true if StatsAnalyzer initialized with multistream', async () => {
2070
+ statsAnalyzer = new StatsAnalyzer({
2071
+ config: initialConfig,
2072
+ receiveSlotCallback: () => receiveSlot,
2073
+ networkQualityMonitor,
2074
+ isMultistream: true,
2075
+ });
2076
+ registerStatsAnalyzerEvents(statsAnalyzer);
2077
+ await startStatsAnalyzer({pc, statsAnalyzer, mediaStatus: {expected: {receiveVideo: true}}});
2078
+
2079
+ await progressTime();
2080
+
2081
+ for (const data of [
2082
+ mqeData.audioTransmit,
2083
+ mqeData.audioReceive,
2084
+ mqeData.videoTransmit,
2085
+ mqeData.videoReceive,
2086
+ ]) {
2087
+ assert.strictEqual(data[0].common.common.multistreamEnabled, true);
2088
+ }
2089
+ });
1816
2090
  });
1817
2091
  });
1818
2092
  });