@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +7 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/media/MediaConnectionAwaiter.js +50 -13
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/mediaQualityMetrics/config.js +16 -6
- package/dist/mediaQualityMetrics/config.js.map +1 -1
- package/dist/meeting/connectionStateHandler.js +67 -0
- package/dist/meeting/connectionStateHandler.js.map +1 -0
- package/dist/meeting/index.js +98 -46
- package/dist/meeting/index.js.map +1 -1
- package/dist/metrics/constants.js +2 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/metrics/index.js +57 -0
- package/dist/metrics/index.js.map +1 -1
- package/dist/reachability/clusterReachability.js +108 -53
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/reachability/index.js +415 -56
- package/dist/reachability/index.js.map +1 -1
- package/dist/statsAnalyzer/index.js +81 -27
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/dist/statsAnalyzer/mqaUtil.js +36 -10
- package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
- package/dist/types/media/MediaConnectionAwaiter.d.ts +18 -4
- package/dist/types/mediaQualityMetrics/config.d.ts +11 -0
- package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
- package/dist/types/meeting/index.d.ts +2 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/metrics/index.d.ts +15 -0
- package/dist/types/reachability/clusterReachability.d.ts +31 -3
- package/dist/types/reachability/index.d.ts +93 -2
- package/dist/types/statsAnalyzer/index.d.ts +15 -6
- package/dist/types/statsAnalyzer/mqaUtil.d.ts +17 -4
- package/dist/webinar/index.js +1 -1
- package/package.json +23 -22
- package/src/breakouts/index.ts +7 -1
- package/src/media/MediaConnectionAwaiter.ts +66 -11
- package/src/mediaQualityMetrics/config.ts +14 -3
- package/src/meeting/connectionStateHandler.ts +65 -0
- package/src/meeting/index.ts +72 -14
- package/src/metrics/constants.ts +1 -0
- package/src/metrics/index.ts +44 -0
- package/src/reachability/clusterReachability.ts +86 -25
- package/src/reachability/index.ts +316 -27
- package/src/statsAnalyzer/index.ts +85 -24
- package/src/statsAnalyzer/mqaUtil.ts +55 -7
- package/test/unit/spec/breakouts/index.ts +51 -32
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +90 -32
- package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
- package/test/unit/spec/meeting/index.js +158 -36
- package/test/unit/spec/metrics/index.js +126 -0
- package/test/unit/spec/reachability/clusterReachability.ts +116 -22
- package/test/unit/spec/reachability/index.ts +1153 -84
- 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
|
|
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
|
|
106
|
-
receiveSlotCallback
|
|
107
|
-
networkQualityMonitor
|
|
108
|
-
statsResults
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = ({
|
|
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.
|
|
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 = ({
|
|
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.
|
|
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
|
|