@webex/plugin-meetings 3.3.1-next.2 → 3.3.1-next.21

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 (56) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +7 -2
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/interpretation/index.js +1 -1
  5. package/dist/interpretation/siLanguage.js +1 -1
  6. package/dist/media/MediaConnectionAwaiter.js +50 -13
  7. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  8. package/dist/mediaQualityMetrics/config.js +16 -6
  9. package/dist/mediaQualityMetrics/config.js.map +1 -1
  10. package/dist/meeting/connectionStateHandler.js +67 -0
  11. package/dist/meeting/connectionStateHandler.js.map +1 -0
  12. package/dist/meeting/index.js +98 -46
  13. package/dist/meeting/index.js.map +1 -1
  14. package/dist/metrics/constants.js +2 -1
  15. package/dist/metrics/constants.js.map +1 -1
  16. package/dist/metrics/index.js +57 -0
  17. package/dist/metrics/index.js.map +1 -1
  18. package/dist/reachability/clusterReachability.js +108 -53
  19. package/dist/reachability/clusterReachability.js.map +1 -1
  20. package/dist/reachability/index.js +415 -56
  21. package/dist/reachability/index.js.map +1 -1
  22. package/dist/statsAnalyzer/index.js +81 -27
  23. package/dist/statsAnalyzer/index.js.map +1 -1
  24. package/dist/statsAnalyzer/mqaUtil.js +36 -10
  25. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  26. package/dist/types/media/MediaConnectionAwaiter.d.ts +18 -4
  27. package/dist/types/mediaQualityMetrics/config.d.ts +11 -0
  28. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  29. package/dist/types/meeting/index.d.ts +2 -0
  30. package/dist/types/metrics/constants.d.ts +1 -0
  31. package/dist/types/metrics/index.d.ts +15 -0
  32. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  33. package/dist/types/reachability/index.d.ts +93 -2
  34. package/dist/types/statsAnalyzer/index.d.ts +15 -6
  35. package/dist/types/statsAnalyzer/mqaUtil.d.ts +17 -4
  36. package/dist/webinar/index.js +1 -1
  37. package/package.json +23 -22
  38. package/src/breakouts/index.ts +7 -1
  39. package/src/media/MediaConnectionAwaiter.ts +66 -11
  40. package/src/mediaQualityMetrics/config.ts +14 -3
  41. package/src/meeting/connectionStateHandler.ts +65 -0
  42. package/src/meeting/index.ts +72 -14
  43. package/src/metrics/constants.ts +1 -0
  44. package/src/metrics/index.ts +44 -0
  45. package/src/reachability/clusterReachability.ts +86 -25
  46. package/src/reachability/index.ts +316 -27
  47. package/src/statsAnalyzer/index.ts +85 -24
  48. package/src/statsAnalyzer/mqaUtil.ts +55 -7
  49. package/test/unit/spec/breakouts/index.ts +51 -32
  50. package/test/unit/spec/media/MediaConnectionAwaiter.ts +90 -32
  51. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  52. package/test/unit/spec/meeting/index.js +158 -36
  53. package/test/unit/spec/metrics/index.js +126 -0
  54. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  55. package/test/unit/spec/reachability/index.ts +1153 -84
  56. package/test/unit/spec/stats-analyzer/index.js +647 -319
@@ -1,8 +1,6 @@
1
- /* eslint-disable prefer-destructuring */
2
-
3
1
  import {cloneDeep, isEmpty} from 'lodash';
2
+ import {CpuInfo} from '@webex/web-capabilities';
4
3
  import {ConnectionState} from '@webex/internal-media-core';
5
-
6
4
  import EventsScope from '../common/events/events-scope';
7
5
  import {
8
6
  DEFAULT_GET_STATS_FILTER,
@@ -35,6 +33,7 @@ import {
35
33
  getAudioReceiverStreamMqa,
36
34
  getVideoSenderStreamMqa,
37
35
  getVideoReceiverStreamMqa,
36
+ isStreamRequested,
38
37
  } from './mqaUtil';
39
38
  import {ReceiveSlot} from '../multistream/receiveSlot';
40
39
 
@@ -90,23 +89,33 @@ export class StatsAnalyzer extends EventsScope {
90
89
  statsStarted: any;
91
90
  successfulCandidatePair: any;
92
91
  localIpAddress: string; // Returns the local IP address for diagnostics. this is the local IP of the interface used for the current media connection a host can have many local Ip Addresses
92
+ shareVideoEncoderImplementation?: string;
93
93
  receiveSlotCallback: ReceiveSlotCallback;
94
+ isMultistream: boolean;
94
95
 
95
96
  /**
96
97
  * Creates a new instance of StatsAnalyzer
97
98
  * @constructor
98
99
  * @public
99
- * @param {Object} config SDK Configuration Object
100
- * @param {Function} receiveSlotCallback Callback used to access receive slots.
101
- * @param {Object} networkQualityMonitor class for assessing network characteristics (jitter, packetLoss, latency)
102
- * @param {Object} statsResults Default properties for stats
100
+ * @param {Object} config - SDK Configuration Object
101
+ * @param {Function} receiveSlotCallback - Callback used to access receive slots.
102
+ * @param {Object} networkQualityMonitor - Class for assessing network characteristics (jitter, packetLoss, latency)
103
+ * @param {Object} statsResults - Default properties for stats
104
+ * @param {boolean | undefined} isMultistream - Param indicating if the media connection is multistream or not
103
105
  */
104
- constructor(
105
- config: any,
106
- receiveSlotCallback: ReceiveSlotCallback = () => undefined,
107
- networkQualityMonitor: object = {},
108
- statsResults: object = defaultStats
109
- ) {
106
+ constructor({
107
+ config,
108
+ receiveSlotCallback = () => undefined,
109
+ networkQualityMonitor = {},
110
+ statsResults = defaultStats,
111
+ isMultistream = false,
112
+ }: {
113
+ config: any;
114
+ receiveSlotCallback: ReceiveSlotCallback;
115
+ networkQualityMonitor: any;
116
+ statsResults?: any;
117
+ isMultistream?: boolean;
118
+ }) {
110
119
  super();
111
120
  this.statsStarted = false;
112
121
  this.statsResults = statsResults;
@@ -120,6 +129,7 @@ export class StatsAnalyzer extends EventsScope {
120
129
  this.receiveSlotCallback = receiveSlotCallback;
121
130
  this.successfulCandidatePair = {};
122
131
  this.localIpAddress = '';
132
+ this.isMultistream = isMultistream;
123
133
  }
124
134
 
125
135
  /**
@@ -203,6 +213,7 @@ export class StatsAnalyzer extends EventsScope {
203
213
  statsResults: this.statsResults,
204
214
  lastMqaDataSent: this.lastMqaDataSent,
205
215
  baseMediaType: 'audio-send',
216
+ isMultistream: this.isMultistream,
206
217
  });
207
218
  newMqa.audioTransmit.push(audioSender);
208
219
 
@@ -211,6 +222,7 @@ export class StatsAnalyzer extends EventsScope {
211
222
  statsResults: this.statsResults,
212
223
  lastMqaDataSent: this.lastMqaDataSent,
213
224
  baseMediaType: 'audio-share-send',
225
+ isMultistream: this.isMultistream,
214
226
  });
215
227
  newMqa.audioTransmit.push(audioShareSender);
216
228
 
@@ -219,6 +231,7 @@ export class StatsAnalyzer extends EventsScope {
219
231
  statsResults: this.statsResults,
220
232
  lastMqaDataSent: this.lastMqaDataSent,
221
233
  baseMediaType: 'audio-recv',
234
+ isMultistream: this.isMultistream,
222
235
  });
223
236
  newMqa.audioReceive.push(audioReceiver);
224
237
 
@@ -227,6 +240,7 @@ export class StatsAnalyzer extends EventsScope {
227
240
  statsResults: this.statsResults,
228
241
  lastMqaDataSent: this.lastMqaDataSent,
229
242
  baseMediaType: 'audio-share-recv',
243
+ isMultistream: this.isMultistream,
230
244
  });
231
245
  newMqa.audioReceive.push(audioShareReceiver);
232
246
 
@@ -235,6 +249,7 @@ export class StatsAnalyzer extends EventsScope {
235
249
  statsResults: this.statsResults,
236
250
  lastMqaDataSent: this.lastMqaDataSent,
237
251
  baseMediaType: 'video-send',
252
+ isMultistream: this.isMultistream,
238
253
  });
239
254
  newMqa.videoTransmit.push(videoSender);
240
255
 
@@ -243,6 +258,7 @@ export class StatsAnalyzer extends EventsScope {
243
258
  statsResults: this.statsResults,
244
259
  lastMqaDataSent: this.lastMqaDataSent,
245
260
  baseMediaType: 'video-share-send',
261
+ isMultistream: this.isMultistream,
246
262
  });
247
263
  newMqa.videoTransmit.push(videoShareSender);
248
264
 
@@ -251,6 +267,7 @@ export class StatsAnalyzer extends EventsScope {
251
267
  statsResults: this.statsResults,
252
268
  lastMqaDataSent: this.lastMqaDataSent,
253
269
  baseMediaType: 'video-recv',
270
+ isMultistream: this.isMultistream,
254
271
  });
255
272
  newMqa.videoReceive.push(videoReceiver);
256
273
 
@@ -259,6 +276,7 @@ export class StatsAnalyzer extends EventsScope {
259
276
  statsResults: this.statsResults,
260
277
  lastMqaDataSent: this.lastMqaDataSent,
261
278
  baseMediaType: 'video-share-recv',
279
+ isMultistream: this.isMultistream,
262
280
  });
263
281
  newMqa.videoReceive.push(videoShareReceiver);
264
282
 
@@ -273,7 +291,9 @@ export class StatsAnalyzer extends EventsScope {
273
291
  lastMqaDataSent: this.lastMqaDataSent,
274
292
  mediaType,
275
293
  });
276
- newMqa.audioTransmit[0].streams.push(audioSenderStream);
294
+ if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) {
295
+ newMqa.audioTransmit[0].streams.push(audioSenderStream);
296
+ }
277
297
 
278
298
  this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
279
299
  } else if (mediaType.startsWith('audio-share-send')) {
@@ -285,7 +305,9 @@ export class StatsAnalyzer extends EventsScope {
285
305
  lastMqaDataSent: this.lastMqaDataSent,
286
306
  mediaType,
287
307
  });
288
- newMqa.audioTransmit[1].streams.push(audioSenderStream);
308
+ if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) {
309
+ newMqa.audioTransmit[1].streams.push(audioSenderStream);
310
+ }
289
311
 
290
312
  this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
291
313
  } else if (mediaType.startsWith('audio-recv')) {
@@ -297,7 +319,9 @@ export class StatsAnalyzer extends EventsScope {
297
319
  lastMqaDataSent: this.lastMqaDataSent,
298
320
  mediaType,
299
321
  });
300
- newMqa.audioReceive[0].streams.push(audioReceiverStream);
322
+ if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) {
323
+ newMqa.audioReceive[0].streams.push(audioReceiverStream);
324
+ }
301
325
 
302
326
  this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
303
327
  } else if (mediaType.startsWith('audio-share-recv')) {
@@ -309,8 +333,9 @@ export class StatsAnalyzer extends EventsScope {
309
333
  lastMqaDataSent: this.lastMqaDataSent,
310
334
  mediaType,
311
335
  });
312
- newMqa.audioReceive[1].streams.push(audioReceiverStream);
313
-
336
+ if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) {
337
+ newMqa.audioReceive[1].streams.push(audioReceiverStream);
338
+ }
314
339
  this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
315
340
  } else if (mediaType.startsWith('video-send-layer')) {
316
341
  // We only want the stream-specific stats we get with video-send-layer-0, video-send-layer-1, etc.
@@ -322,8 +347,9 @@ export class StatsAnalyzer extends EventsScope {
322
347
  lastMqaDataSent: this.lastMqaDataSent,
323
348
  mediaType,
324
349
  });
325
- newMqa.videoTransmit[0].streams.push(videoSenderStream);
326
-
350
+ if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) {
351
+ newMqa.videoTransmit[0].streams.push(videoSenderStream);
352
+ }
327
353
  this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
328
354
  } else if (mediaType.startsWith('video-share-send')) {
329
355
  const videoSenderStream = cloneDeep(emptyVideoTransmitStream);
@@ -334,7 +360,9 @@ export class StatsAnalyzer extends EventsScope {
334
360
  lastMqaDataSent: this.lastMqaDataSent,
335
361
  mediaType,
336
362
  });
337
- newMqa.videoTransmit[1].streams.push(videoSenderStream);
363
+ if (isStreamRequested(this.statsResults, mediaType, STATS.SEND_DIRECTION)) {
364
+ newMqa.videoTransmit[1].streams.push(videoSenderStream);
365
+ }
338
366
 
339
367
  this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
340
368
  } else if (mediaType.startsWith('video-recv')) {
@@ -346,7 +374,9 @@ export class StatsAnalyzer extends EventsScope {
346
374
  lastMqaDataSent: this.lastMqaDataSent,
347
375
  mediaType,
348
376
  });
349
- newMqa.videoReceive[0].streams.push(videoReceiverStream);
377
+ if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) {
378
+ newMqa.videoReceive[0].streams.push(videoReceiverStream);
379
+ }
350
380
 
351
381
  this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
352
382
  } else if (mediaType.startsWith('video-share-recv')) {
@@ -358,14 +388,17 @@ export class StatsAnalyzer extends EventsScope {
358
388
  lastMqaDataSent: this.lastMqaDataSent,
359
389
  mediaType,
360
390
  });
361
- newMqa.videoReceive[1].streams.push(videoReceiverStream);
362
-
391
+ if (isStreamRequested(this.statsResults, mediaType, STATS.RECEIVE_DIRECTION)) {
392
+ newMqa.videoReceive[1].streams.push(videoReceiverStream);
393
+ }
363
394
  this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
364
395
  }
365
396
  });
366
397
 
367
398
  newMqa.intervalMetadata.peerReflexiveIP = this.statsResults.connectionType.local.ipAddress;
368
399
 
400
+ newMqa.intervalMetadata.cpuInfo.numberOfCores = CpuInfo.getNumLogicalCores() || 1;
401
+
369
402
  // Adding peripheral information
370
403
  newMqa.intervalMetadata.peripherals.push({information: _UNKNOWN_, name: MEDIA_DEVICES.SPEAKER});
371
404
  if (this.statsResults['audio-send']) {
@@ -388,6 +421,17 @@ export class StatsAnalyzer extends EventsScope {
388
421
 
389
422
  newMqa.networkType = this.statsResults.connectionType.local.networkType;
390
423
 
424
+ newMqa.intervalMetadata.screenWidth = window.screen.width;
425
+ newMqa.intervalMetadata.screenHeight = window.screen.height;
426
+ newMqa.intervalMetadata.screenResolution = Math.round(
427
+ (window.screen.width * window.screen.height) / 256
428
+ );
429
+ newMqa.intervalMetadata.appWindowWidth = window.innerWidth;
430
+ newMqa.intervalMetadata.appWindowHeight = window.innerHeight;
431
+ newMqa.intervalMetadata.appWindowSize = Math.round(
432
+ (window.innerWidth * window.innerHeight) / 256
433
+ );
434
+
391
435
  this.mqaSentCount += 1;
392
436
 
393
437
  newMqa.intervalNumber = this.mqaSentCount;
@@ -570,6 +614,9 @@ export class StatsAnalyzer extends EventsScope {
570
614
  this.statsResults[newType].direction = statsItem.currentDirection;
571
615
  this.statsResults[newType].trackLabel = statsItem.localTrackLabel;
572
616
  this.statsResults[newType].csi = statsItem.csi;
617
+ } else if (type === 'video-share-send' && result.type === 'outbound-rtp') {
618
+ this.shareVideoEncoderImplementation = result.encoderImplementation;
619
+ this.parseGetStatsResult(result, type, isSender);
573
620
  } else {
574
621
  this.parseGetStatsResult(result, type, isSender);
575
622
  }
@@ -1003,6 +1050,11 @@ export class StatsAnalyzer extends EventsScope {
1003
1050
  this.statsResults[mediaType][sendrecvType].totalRtxBytesSent = result.retransmittedBytesSent;
1004
1051
  this.statsResults[mediaType][sendrecvType].totalBytesSent = result.bytesSent;
1005
1052
  this.statsResults[mediaType][sendrecvType].headerBytesSent = result.headerBytesSent;
1053
+ this.statsResults[mediaType][sendrecvType].retransmittedBytesSent =
1054
+ result.retransmittedBytesSent;
1055
+ this.statsResults[mediaType][sendrecvType].isRequested = result.isRequested;
1056
+ this.statsResults[mediaType][sendrecvType].lastRequestedUpdateTimestamp =
1057
+ result.lastRequestedUpdateTimestamp;
1006
1058
  this.statsResults[mediaType][sendrecvType].requestedBitrate = result.requestedBitrate;
1007
1059
  this.statsResults[mediaType][sendrecvType].requestedFrameSize = result.requestedFrameSize;
1008
1060
  }
@@ -1088,6 +1140,12 @@ export class StatsAnalyzer extends EventsScope {
1088
1140
  }
1089
1141
  }
1090
1142
 
1143
+ if (mediaType.startsWith('video-recv')) {
1144
+ this.statsResults[mediaType][sendrecvType].isActiveSpeaker = result.isActiveSpeaker;
1145
+ this.statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp =
1146
+ result.lastActiveSpeakerUpdateTimestamp;
1147
+ }
1148
+
1091
1149
  // Check the over all packet Lost ratio
1092
1150
  this.statsResults[mediaType][sendrecvType].currentPacketLossRatio =
1093
1151
  currentPacketsLost > 0
@@ -1151,6 +1209,9 @@ export class StatsAnalyzer extends EventsScope {
1151
1209
  this.statsResults[mediaType][sendrecvType].totalSamplesDecoded =
1152
1210
  result.totalSamplesDecoded || 0;
1153
1211
  this.statsResults[mediaType][sendrecvType].concealedSamples = result.concealedSamples || 0;
1212
+ this.statsResults[mediaType][sendrecvType].isRequested = result.isRequested;
1213
+ this.statsResults[mediaType][sendrecvType].lastRequestedUpdateTimestamp =
1214
+ result.lastRequestedUpdateTimestamp;
1154
1215
  }
1155
1216
  }
1156
1217
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {mean, max} from 'lodash';
4
4
 
5
- import {STATS} from '../constants';
5
+ import {MQA_INTERVAL, STATS} from '../constants';
6
6
 
7
7
  /**
8
8
  * Get the totals of a certain value from a certain media type.
@@ -28,6 +28,7 @@ export const getAudioReceiverMqa = ({
28
28
  statsResults,
29
29
  lastMqaDataSent,
30
30
  baseMediaType,
31
+ isMultistream,
31
32
  }) => {
32
33
  const sendrecvType = STATS.RECEIVE_DIRECTION;
33
34
 
@@ -52,6 +53,7 @@ export const getAudioReceiverMqa = ({
52
53
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
53
54
  ?.direction || 'inactive';
54
55
  audioReceiver.common.common.isMain = !baseMediaType.includes('-share');
56
+ audioReceiver.common.common.multistreamEnabled = isMultistream;
55
57
  audioReceiver.common.transportType = statsResults.connectionType.local.transport;
56
58
 
57
59
  // add rtpPacket info inside common as also for call analyzer
@@ -67,7 +69,9 @@ export const getAudioReceiverMqa = ({
67
69
  totalFecPacketsReceived -
68
70
  lastFecPacketsReceived -
69
71
  (totalFecPacketsDiscarded - lastFecPacketsDiscarded);
70
- audioReceiver.common.fecPackets = fecRecovered;
72
+ audioReceiver.common.fecPackets = totalFecPacketsReceived - lastFecPacketsReceived;
73
+
74
+ audioReceiver.common.rtpRecovered = fecRecovered;
71
75
 
72
76
  audioReceiver.common.rtpBitrate = ((totalBytesReceived - lastBytesReceived) * 8) / 60 || 0;
73
77
  };
@@ -127,7 +131,13 @@ export const getAudioReceiverStreamMqa = ({
127
131
  ((statsResults[mediaType][sendrecvType].totalBytesReceived - lastBytesReceived) * 8) / 60 || 0;
128
132
  };
129
133
 
130
- export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, baseMediaType}) => {
134
+ export const getAudioSenderMqa = ({
135
+ audioSender,
136
+ statsResults,
137
+ lastMqaDataSent,
138
+ baseMediaType,
139
+ isMultistream,
140
+ }) => {
131
141
  const sendrecvType = STATS.SEND_DIRECTION;
132
142
 
133
143
  const getLastTotalValue = (value: string) =>
@@ -152,6 +162,7 @@ export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, b
152
162
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
153
163
  ?.direction || 'inactive';
154
164
  audioSender.common.common.isMain = !baseMediaType.includes('-share');
165
+ audioSender.common.common.multistreamEnabled = isMultistream;
155
166
  audioSender.common.transportType = statsResults.connectionType.local.transport;
156
167
 
157
168
  audioSender.common.maxRemoteJitter = max(meanRemoteJitter) * 1000 || 0;
@@ -166,10 +177,10 @@ export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, b
166
177
  baseMediaType,
167
178
  'availableOutgoingBitrate'
168
179
  );
169
- // Calculate based on how much packets lost of received compated to how to the client sent
170
180
 
181
+ // Calculate based on how much packets lost of received compated to how to the client sent
171
182
  const totalPacketsLostForaMin = totalPacketsLostOnReceiver - lastPacketsLostTotal;
172
- audioSender.common.remoteLossRate =
183
+ audioSender.common.maxRemoteLossRate =
173
184
  totalPacketsSent - lastPacketsSent > 0
174
185
  ? (totalPacketsLostForaMin * 100) / (totalPacketsSent - lastPacketsSent)
175
186
  : 0; // This is the packets sent with in last min
@@ -225,6 +236,7 @@ export const getVideoReceiverMqa = ({
225
236
  statsResults,
226
237
  lastMqaDataSent,
227
238
  baseMediaType,
239
+ isMultistream,
228
240
  }) => {
229
241
  const sendrecvType = STATS.RECEIVE_DIRECTION;
230
242
 
@@ -254,6 +266,7 @@ export const getVideoReceiverMqa = ({
254
266
  videoReceiver.common.common.direction =
255
267
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
256
268
  ?.direction || 'inactive';
269
+ videoReceiver.common.common.multistreamEnabled = isMultistream;
257
270
  videoReceiver.common.common.isMain = !baseMediaType.includes('-share');
258
271
  videoReceiver.common.transportType = statsResults.connectionType.local.transport;
259
272
 
@@ -347,9 +360,23 @@ export const getVideoReceiverStreamMqa = ({
347
360
  statsResults[mediaType][sendrecvType].keyFramesDecoded - lastKeyFramesDecoded || 0;
348
361
  videoReceiverStream.requestedKeyFrames =
349
362
  statsResults[mediaType][sendrecvType].totalPliCount - lastPliCount || 0;
363
+
364
+ videoReceiverStream.isActiveSpeaker =
365
+ statsResults[mediaType][sendrecvType].isActiveSpeaker ||
366
+ ((statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp ?? 0) > 0 &&
367
+ performance.now() +
368
+ performance.timeOrigin -
369
+ (statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp ?? 0) <
370
+ MQA_INTERVAL);
350
371
  };
351
372
 
352
- export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, baseMediaType}) => {
373
+ export const getVideoSenderMqa = ({
374
+ videoSender,
375
+ statsResults,
376
+ lastMqaDataSent,
377
+ baseMediaType,
378
+ isMultistream,
379
+ }) => {
353
380
  const sendrecvType = STATS.SEND_DIRECTION;
354
381
 
355
382
  const getLastTotalValue = (value: string) =>
@@ -373,6 +400,7 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, b
373
400
  videoSender.common.common.direction =
374
401
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
375
402
  ?.direction || 'inactive';
403
+ videoSender.common.common.multistreamEnabled = isMultistream;
376
404
  videoSender.common.common.isMain = !baseMediaType.includes('-share');
377
405
  videoSender.common.transportType = statsResults.connectionType.local.transport;
378
406
 
@@ -393,7 +421,7 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, b
393
421
  // Calculate based on how much packets lost of received compated to how to the client sent
394
422
  const totalPacketsLostForaMin = totalPacketsLostOnReceiver - lastPacketsLostTotal;
395
423
 
396
- videoSender.common.remoteLossRate =
424
+ videoSender.common.maxRemoteLossRate =
397
425
  totalPacketsSent - lastPacketsSent > 0
398
426
  ? (totalPacketsLostForaMin * 100) / (totalPacketsSent - lastPacketsSent)
399
427
  : 0; // This is the packets sent with in last min || 0;
@@ -461,3 +489,23 @@ export const getVideoSenderStreamMqa = ({
461
489
  videoSenderStream.requestedFrameSize =
462
490
  statsResults[mediaType][sendrecvType].requestedFrameSize || 0;
463
491
  };
492
+
493
+ /**
494
+ * Checks if stream stats should be updated based on request status and elapsed time.
495
+ *
496
+ * @param {Object} statsResults - Stats results object.
497
+ * @param {string} mediaType - Media type (e.g., 'audio', 'video').
498
+ * @param {string} direction - Stats direction (e.g., 'send', 'receive').
499
+ * @returns {boolean} Whether stats should be updated.
500
+ */
501
+ export const isStreamRequested = (
502
+ statsResults: any,
503
+ mediaType: string,
504
+ direction: string
505
+ ): boolean => {
506
+ const now = performance.timeOrigin + performance.now();
507
+ const lastUpdateTimestamp = statsResults[mediaType][direction]?.lastRequestedUpdateTimestamp;
508
+ const isRequested = statsResults[mediaType][direction]?.isRequested;
509
+
510
+ return isRequested || (lastUpdateTimestamp && now - lastUpdateTimestamp < MQA_INTERVAL);
511
+ };
@@ -87,6 +87,7 @@ describe('plugin-meetings', () => {
87
87
  // @ts-ignore
88
88
  webex = new MockWebex({});
89
89
  webex.internal.llm.on = sinon.stub();
90
+ webex.internal.llm.isConnected = sinon.stub();
90
91
  webex.internal.mercury.on = sinon.stub();
91
92
  breakouts = new Breakouts({}, {parent: webex});
92
93
  breakouts.groupId = 'groupId';
@@ -225,38 +226,6 @@ describe('plugin-meetings', () => {
225
226
  });
226
227
  });
227
228
 
228
- describe('#listenToBroadcastMessages', () => {
229
- it('triggers message event when a message received', () => {
230
- const call = webex.internal.llm.on.getCall(0);
231
- const callback = call.args[1];
232
-
233
- assert.equal(call.args[0], 'event:breakout.message');
234
-
235
- let message;
236
-
237
- breakouts.listenTo(breakouts, BREAKOUTS.EVENTS.MESSAGE, (event) => {
238
- message = event;
239
- });
240
-
241
- breakouts.currentBreakoutSession.sessionId = 'sessionId';
242
-
243
- callback({
244
- data: {
245
- senderUserId: 'senderUserId',
246
- sentTime: 'sentTime',
247
- message: 'message',
248
- },
249
- });
250
-
251
- assert.deepEqual(message, {
252
- senderUserId: 'senderUserId',
253
- sentTime: 'sentTime',
254
- message: 'message',
255
- sessionId: 'sessionId',
256
- });
257
- });
258
- });
259
-
260
229
  describe('#listenToBreakoutRosters', () => {
261
230
  it('triggers member update event when a roster received', () => {
262
231
  const call = webex.internal.mercury.on.getCall(0);
@@ -496,8 +465,58 @@ describe('plugin-meetings', () => {
496
465
  describe('#locusUrlUpdate', () => {
497
466
  it('sets the locus url', () => {
498
467
  breakouts.locusUrlUpdate('newUrl');
468
+ assert.equal(breakouts.locusUrl, 'newUrl');
469
+ });
470
+ });
471
+
472
+ describe('#listenToBroadcastMessages', () => {
473
+ it('do not subscribe message if llm not connected', () => {
474
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
475
+ breakouts.listenTo = sinon.stub();
476
+ breakouts.locusUrlUpdate('newUrl');
477
+ assert.equal(breakouts.locusUrl, 'newUrl');
478
+ assert.notCalled(breakouts.listenTo);
479
+ });
499
480
 
481
+ it('do not subscribe message if already done', () => {
482
+ webex.internal.llm.isConnected = sinon.stub().returns(true);
483
+ breakouts.hasSubscribedToMessage = true;
484
+ breakouts.listenTo = sinon.stub();
485
+ breakouts.locusUrlUpdate('newUrl');
500
486
  assert.equal(breakouts.locusUrl, 'newUrl');
487
+ assert.notCalled(breakouts.listenTo);
488
+ });
489
+
490
+ it('triggers message event when a message received', () => {
491
+ webex.internal.llm.isConnected = sinon.stub().returns(true);
492
+ breakouts.locusUrlUpdate('newUrl');
493
+ const call = webex.internal.llm.on.getCall(0);
494
+ const callback = call.args[1];
495
+
496
+ assert.equal(call.args[0], 'event:breakout.message');
497
+
498
+ let message;
499
+
500
+ breakouts.listenTo(breakouts, BREAKOUTS.EVENTS.MESSAGE, (event) => {
501
+ message = event;
502
+ });
503
+
504
+ breakouts.currentBreakoutSession.sessionId = 'sessionId';
505
+
506
+ callback({
507
+ data: {
508
+ senderUserId: 'senderUserId',
509
+ sentTime: 'sentTime',
510
+ message: 'message',
511
+ },
512
+ });
513
+
514
+ assert.deepEqual(message, {
515
+ senderUserId: 'senderUserId',
516
+ sentTime: 'sentTime',
517
+ message: 'message',
518
+ sessionId: 'sessionId',
519
+ });
501
520
  });
502
521
  });
503
522