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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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/constants.js +11 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/locus-info/selfUtils.js +0 -5
  9. package/dist/locus-info/selfUtils.js.map +1 -1
  10. package/dist/media/MediaConnectionAwaiter.js +70 -15
  11. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  12. package/dist/media/index.js +18 -9
  13. package/dist/media/index.js.map +1 -1
  14. package/dist/meeting/connectionStateHandler.js +67 -0
  15. package/dist/meeting/connectionStateHandler.js.map +1 -0
  16. package/dist/meeting/index.js +576 -374
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/locusMediaRequest.js +7 -0
  19. package/dist/meeting/locusMediaRequest.js.map +1 -1
  20. package/dist/meeting/muteState.js +6 -1
  21. package/dist/meeting/muteState.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meeting-info/index.js +4 -4
  25. package/dist/meeting-info/index.js.map +1 -1
  26. package/dist/meeting-info/meeting-info-v2.js +2 -2
  27. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  28. package/dist/meeting-info/util.js +17 -17
  29. package/dist/meeting-info/util.js.map +1 -1
  30. package/dist/meeting-info/utilv2.js +16 -16
  31. package/dist/meeting-info/utilv2.js.map +1 -1
  32. package/dist/meetings/collection.js +1 -1
  33. package/dist/meetings/collection.js.map +1 -1
  34. package/dist/meetings/index.js +41 -35
  35. package/dist/meetings/index.js.map +1 -1
  36. package/dist/meetings/meetings.types.js +8 -0
  37. package/dist/meetings/meetings.types.js.map +1 -1
  38. package/dist/meetings/util.js +3 -2
  39. package/dist/meetings/util.js.map +1 -1
  40. package/dist/metrics/constants.js +2 -1
  41. package/dist/metrics/constants.js.map +1 -1
  42. package/dist/metrics/index.js +57 -0
  43. package/dist/metrics/index.js.map +1 -1
  44. package/dist/personal-meeting-room/index.js +1 -1
  45. package/dist/personal-meeting-room/index.js.map +1 -1
  46. package/dist/reachability/clusterReachability.js +108 -53
  47. package/dist/reachability/clusterReachability.js.map +1 -1
  48. package/dist/reachability/index.js +546 -115
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/reconnection-manager/index.js +1 -1
  51. package/dist/reconnection-manager/index.js.map +1 -1
  52. package/dist/rtcMetrics/index.js +26 -6
  53. package/dist/rtcMetrics/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +11 -3
  55. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  56. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  57. package/dist/types/meeting/index.d.ts +28 -8
  58. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  59. package/dist/types/meeting-info/index.d.ts +3 -2
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  61. package/dist/types/meeting-info/util.d.ts +5 -4
  62. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  63. package/dist/types/meetings/collection.d.ts +3 -2
  64. package/dist/types/meetings/index.d.ts +6 -4
  65. package/dist/types/meetings/meetings.types.d.ts +9 -0
  66. package/dist/types/metrics/constants.d.ts +1 -0
  67. package/dist/types/metrics/index.d.ts +15 -0
  68. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  69. package/dist/types/reachability/index.d.ts +107 -4
  70. package/dist/types/rtcMetrics/index.d.ts +11 -1
  71. package/dist/webinar/index.js +1 -1
  72. package/package.json +23 -23
  73. package/src/breakouts/index.ts +7 -1
  74. package/src/constants.ts +13 -17
  75. package/src/locus-info/selfUtils.ts +0 -5
  76. package/src/media/MediaConnectionAwaiter.ts +89 -14
  77. package/src/media/index.ts +18 -9
  78. package/src/meeting/connectionStateHandler.ts +65 -0
  79. package/src/meeting/index.ts +541 -298
  80. package/src/meeting/locusMediaRequest.ts +5 -0
  81. package/src/meeting/muteState.ts +6 -1
  82. package/src/meeting/util.ts +1 -0
  83. package/src/meeting-info/index.ts +9 -6
  84. package/src/meeting-info/meeting-info-v2.ts +4 -4
  85. package/src/meeting-info/util.ts +23 -28
  86. package/src/meeting-info/utilv2.ts +18 -24
  87. package/src/meetings/collection.ts +3 -3
  88. package/src/meetings/index.ts +43 -43
  89. package/src/meetings/meetings.types.ts +11 -0
  90. package/src/meetings/util.ts +5 -4
  91. package/src/metrics/constants.ts +1 -0
  92. package/src/metrics/index.ts +44 -0
  93. package/src/personal-meeting-room/index.ts +2 -2
  94. package/src/reachability/clusterReachability.ts +86 -25
  95. package/src/reachability/index.ts +364 -30
  96. package/src/reconnection-manager/index.ts +1 -1
  97. package/src/rtcMetrics/index.ts +25 -5
  98. package/test/unit/spec/breakouts/index.ts +51 -32
  99. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  100. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  101. package/test/unit/spec/media/index.ts +75 -34
  102. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  103. package/test/unit/spec/meeting/index.js +807 -185
  104. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  105. package/test/unit/spec/meeting/muteState.js +24 -0
  106. package/test/unit/spec/meeting-info/index.js +4 -4
  107. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  108. package/test/unit/spec/meeting-info/request.js +2 -2
  109. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  110. package/test/unit/spec/meetings/index.js +44 -3
  111. package/test/unit/spec/metrics/index.js +126 -0
  112. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  113. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  114. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  115. package/test/unit/spec/reachability/index.ts +1398 -131
  116. package/test/unit/spec/rtcMetrics/index.ts +32 -0
  117. package/dist/mediaQualityMetrics/config.js +0 -321
  118. package/dist/mediaQualityMetrics/config.js.map +0 -1
  119. package/dist/networkQualityMonitor/index.js +0 -227
  120. package/dist/networkQualityMonitor/index.js.map +0 -1
  121. package/dist/statsAnalyzer/global.js +0 -44
  122. package/dist/statsAnalyzer/global.js.map +0 -1
  123. package/dist/statsAnalyzer/index.js +0 -1072
  124. package/dist/statsAnalyzer/index.js.map +0 -1
  125. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  126. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  127. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  128. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  129. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  130. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  131. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  132. package/src/mediaQualityMetrics/config.ts +0 -255
  133. package/src/networkQualityMonitor/index.ts +0 -211
  134. package/src/statsAnalyzer/global.ts +0 -37
  135. package/src/statsAnalyzer/index.ts +0 -1318
  136. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  137. package/test/unit/spec/networkQualityMonitor/index.js +0 -99
  138. package/test/unit/spec/stats-analyzer/index.js +0 -1819
@@ -3,8 +3,9 @@
3
3
  */
4
4
 
5
5
  /* eslint-disable class-methods-use-this */
6
- import {mapValues} from 'lodash';
6
+ import {isEqual, mapValues, mean} from 'lodash';
7
7
 
8
+ import {Defer} from '@webex/common';
8
9
  import LoggerProxy from '../common/logs/logger-proxy';
9
10
  import MeetingUtil from '../meeting/util';
10
11
 
@@ -12,10 +13,16 @@ import {REACHABILITY} from '../constants';
12
13
 
13
14
  import ReachabilityRequest, {ClusterList} from './request';
14
15
  import {
16
+ ClientMediaIpsUpdatedEventData,
15
17
  ClusterReachability,
16
18
  ClusterReachabilityResult,
19
+ Events,
20
+ ResultEventData,
17
21
  TransportResult,
18
22
  } from './clusterReachability';
23
+ import EventsScope from '../common/events/events-scope';
24
+ import BEHAVIORAL_METRICS from '../metrics/constants';
25
+ import Metrics from '../metrics';
19
26
 
20
27
  export type ReachabilityMetrics = {
21
28
  reachability_public_udp_success: number;
@@ -60,11 +67,16 @@ export type ReachabilityResults = Record<
60
67
  }
61
68
  >;
62
69
 
70
+ // timeouts in seconds
71
+ const DEFAULT_TIMEOUT = 3;
72
+ const VIDEO_MESH_TIMEOUT = 1;
73
+ const OVERALL_TIMEOUT = 15;
74
+
63
75
  /**
64
76
  * @class Reachability
65
77
  * @export
66
78
  */
67
- export default class Reachability {
79
+ export default class Reachability extends EventsScope {
68
80
  namespace = REACHABILITY.namespace;
69
81
  webex: object;
70
82
  reachabilityRequest: ReachabilityRequest;
@@ -72,12 +84,24 @@ export default class Reachability {
72
84
  [key: string]: ClusterReachability;
73
85
  };
74
86
 
87
+ reachabilityDefer?: Defer;
88
+
89
+ vmnTimer?: ReturnType<typeof setTimeout>;
90
+ publicCloudTimer?: ReturnType<typeof setTimeout>;
91
+ overallTimer?: ReturnType<typeof setTimeout>;
92
+
93
+ expectedResultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
94
+ resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
95
+
96
+ protected lastTrigger?: string;
97
+
75
98
  /**
76
99
  * Creates an instance of Reachability.
77
100
  * @param {object} webex
78
101
  * @memberof Reachability
79
102
  */
80
103
  constructor(webex: object) {
104
+ super();
81
105
  this.webex = webex;
82
106
 
83
107
  /**
@@ -92,28 +116,51 @@ export default class Reachability {
92
116
  this.clusterReachability = {};
93
117
  }
94
118
 
119
+ /**
120
+ * Fetches the list of media clusters from the backend
121
+ * @param {boolean} isRetry
122
+ * @private
123
+ * @returns {Promise<{clusters: ClusterList, joinCookie: any}>}
124
+ */
125
+ async getClusters(isRetry = false): Promise<{clusters: ClusterList; joinCookie: any}> {
126
+ try {
127
+ const {clusters, joinCookie} = await this.reachabilityRequest.getClusters(
128
+ MeetingUtil.getIpVersion(this.webex)
129
+ );
130
+
131
+ return {clusters, joinCookie};
132
+ } catch (error) {
133
+ if (isRetry) {
134
+ throw error;
135
+ }
136
+
137
+ LoggerProxy.logger.error(
138
+ `Reachability:index#getClusters --> Failed with error: ${error}, retrying...`
139
+ );
140
+
141
+ return this.getClusters(true);
142
+ }
143
+ }
144
+
95
145
  /**
96
146
  * Gets a list of media clusters from the backend and performs reachability checks on all the clusters
147
+ * @param {string} trigger - explains the reason for starting reachability
97
148
  * @returns {Promise<ReachabilityResults>} reachability results
98
149
  * @public
99
150
  * @memberof Reachability
100
151
  */
101
- public async gatherReachability(): Promise<ReachabilityResults> {
152
+ public async gatherReachability(trigger: string): Promise<ReachabilityResults> {
102
153
  // Fetch clusters and measure latency
103
154
  try {
104
- const {clusters, joinCookie} = await this.reachabilityRequest.getClusters(
105
- MeetingUtil.getIpVersion(this.webex)
106
- );
107
-
108
- // Perform Reachability Check
109
- const results = await this.performReachabilityChecks(clusters);
155
+ this.lastTrigger = trigger;
110
156
 
157
+ // kick off ip version detection. For now we don't await it, as we're doing it
158
+ // to gather the timings and send them with our reachability metrics
111
159
  // @ts-ignore
112
- await this.webex.boundedStorage.put(
113
- this.namespace,
114
- REACHABILITY.localStorageResult,
115
- JSON.stringify(results)
116
- );
160
+ this.webex.internal.device.ipNetworkDetector.detect();
161
+
162
+ const {clusters, joinCookie} = await this.getClusters();
163
+
117
164
  // @ts-ignore
118
165
  await this.webex.boundedStorage.put(
119
166
  this.namespace,
@@ -121,11 +168,12 @@ export default class Reachability {
121
168
  JSON.stringify(joinCookie)
122
169
  );
123
170
 
124
- LoggerProxy.logger.log(
125
- 'Reachability:index#gatherReachability --> Reachability checks completed'
126
- );
171
+ this.reachabilityDefer = new Defer();
127
172
 
128
- return results;
173
+ // Perform Reachability Check
174
+ await this.performReachabilityChecks(clusters);
175
+
176
+ return this.reachabilityDefer.promise;
129
177
  } catch (error) {
130
178
  LoggerProxy.logger.error(`Reachability:index#gatherReachability --> Error:`, error);
131
179
 
@@ -395,16 +443,221 @@ export default class Reachability {
395
443
  });
396
444
  }
397
445
 
446
+ /**
447
+ * Returns true if we've obtained all the reachability results for all the public clusters
448
+ * In other words, it means that all public clusters are reachable over each protocol,
449
+ * because we only get a "result" if we managed to reach a cluster
450
+ *
451
+ * @returns {boolean}
452
+ */
453
+ private areAllPublicClusterResultsReady() {
454
+ return isEqual(this.expectedResultsCount.public, this.resultsCount.public);
455
+ }
456
+
457
+ /**
458
+ * Returns true if we've obtained all the reachability results for all the clusters
459
+ *
460
+ * @returns {boolean}
461
+ */
462
+ private areAllResultsReady() {
463
+ return isEqual(this.expectedResultsCount, this.resultsCount);
464
+ }
465
+
466
+ /**
467
+ * Resolves the promise returned by gatherReachability() method
468
+ * @returns {void}
469
+ */
470
+ private resolveReachabilityPromise() {
471
+ if (this.vmnTimer) {
472
+ clearTimeout(this.vmnTimer);
473
+ }
474
+ if (this.publicCloudTimer) {
475
+ clearTimeout(this.publicCloudTimer);
476
+ }
477
+
478
+ this.logUnreachableClusters();
479
+ this.reachabilityDefer?.resolve();
480
+ }
481
+
482
+ /**
483
+ * Aborts all cluster reachability checks that are in progress
484
+ *
485
+ * @returns {void}
486
+ */
487
+ private abortClusterReachability() {
488
+ Object.values(this.clusterReachability).forEach((clusterReachability) => {
489
+ clusterReachability.abort();
490
+ });
491
+ }
492
+
493
+ /**
494
+ * Helper function for calculating min/max/average values of latency
495
+ *
496
+ * @param {Array<any>} results
497
+ * @param {string} protocol
498
+ * @param {boolean} isVideoMesh
499
+ * @returns {{min:number, max: number, average: number}}
500
+ */
501
+ protected getStatistics(
502
+ results: Array<ClusterReachabilityResult & {isVideoMesh: boolean}>,
503
+ protocol: 'udp' | 'tcp' | 'xtls',
504
+ isVideoMesh: boolean
505
+ ) {
506
+ const values = results
507
+ .filter((result) => result.isVideoMesh === isVideoMesh)
508
+ .filter((result) => result[protocol].result === 'reachable')
509
+ .map((result) => result[protocol].latencyInMilliseconds);
510
+
511
+ if (values.length === 0) {
512
+ return {
513
+ min: -1,
514
+ max: -1,
515
+ average: -1,
516
+ };
517
+ }
518
+
519
+ return {
520
+ min: Math.min(...values),
521
+ max: Math.max(...values),
522
+ average: mean(values),
523
+ };
524
+ }
525
+
526
+ /**
527
+ * Sends a metric with all the statistics about how long reachability took
528
+ *
529
+ * @returns {void}
530
+ */
531
+ protected async sendMetric() {
532
+ const results = [];
533
+
534
+ Object.values(this.clusterReachability).forEach((clusterReachability) => {
535
+ results.push({
536
+ ...clusterReachability.getResult(),
537
+ isVideoMesh: clusterReachability.isVideoMesh,
538
+ });
539
+ });
540
+
541
+ const stats = {
542
+ vmn: {
543
+ udp: this.getStatistics(results, 'udp', true),
544
+ },
545
+ public: {
546
+ udp: this.getStatistics(results, 'udp', false),
547
+ tcp: this.getStatistics(results, 'tcp', false),
548
+ xtls: this.getStatistics(results, 'xtls', false),
549
+ },
550
+ ipver: {
551
+ // @ts-ignore
552
+ firstIpV4: this.webex.internal.device.ipNetworkDetector.firstIpV4,
553
+ // @ts-ignore
554
+ firstIpV6: this.webex.internal.device.ipNetworkDetector.firstIpV6,
555
+ // @ts-ignore
556
+ firstMdns: this.webex.internal.device.ipNetworkDetector.firstMdns,
557
+ // @ts-ignore
558
+ totalTime: this.webex.internal.device.ipNetworkDetector.totalTime,
559
+ },
560
+ trigger: this.lastTrigger,
561
+ };
562
+ Metrics.sendBehavioralMetric(
563
+ BEHAVIORAL_METRICS.REACHABILITY_COMPLETED,
564
+ Metrics.prepareMetricFields(stats)
565
+ );
566
+ }
567
+
568
+ /**
569
+ * Starts all the timers used for various timeouts
570
+ *
571
+ * @returns {void}
572
+ */
573
+ private startTimers() {
574
+ this.vmnTimer = setTimeout(() => {
575
+ this.vmnTimer = undefined;
576
+ // if we are only missing VMN results, then we don't want to wait for them any longer
577
+ // as they are likely to fail if users are not on corporate network
578
+ if (this.areAllPublicClusterResultsReady()) {
579
+ LoggerProxy.logger.log(
580
+ 'Reachability:index#startTimers --> Reachability checks timed out (VMN timeout)'
581
+ );
582
+
583
+ this.resolveReachabilityPromise();
584
+ }
585
+ }, VIDEO_MESH_TIMEOUT * 1000);
586
+
587
+ this.publicCloudTimer = setTimeout(() => {
588
+ this.publicCloudTimer = undefined;
589
+
590
+ LoggerProxy.logger.log(
591
+ `Reachability:index#startTimers --> Reachability checks timed out (${DEFAULT_TIMEOUT}s)`
592
+ );
593
+
594
+ // resolve the promise, so that the client won't be blocked waiting on meetings.register() for too long
595
+ this.resolveReachabilityPromise();
596
+ }, DEFAULT_TIMEOUT * 1000);
597
+
598
+ this.overallTimer = setTimeout(() => {
599
+ this.overallTimer = undefined;
600
+ this.abortClusterReachability();
601
+ this.emit(
602
+ {
603
+ file: 'reachability',
604
+ function: 'overallTimer timeout',
605
+ },
606
+ 'reachability:done',
607
+ {}
608
+ );
609
+ this.sendMetric();
610
+
611
+ LoggerProxy.logger.log(
612
+ `Reachability:index#startTimers --> Reachability checks fully timed out (${OVERALL_TIMEOUT}s)`
613
+ );
614
+ }, OVERALL_TIMEOUT * 1000);
615
+ }
616
+
617
+ /**
618
+ * Stores given reachability results in local storage
619
+ *
620
+ * @param {ReachabilityResults} results
621
+ * @returns {Promise<void>}
622
+ */
623
+ private async storeResults(results: ReachabilityResults) {
624
+ // @ts-ignore
625
+ await this.webex.boundedStorage.put(
626
+ this.namespace,
627
+ REACHABILITY.localStorageResult,
628
+ JSON.stringify(results)
629
+ );
630
+ }
631
+
632
+ /**
633
+ * Resets all the internal counters that keep track of the results
634
+ *
635
+ * @returns {void}
636
+ */
637
+ private resetResultCounters() {
638
+ this.expectedResultsCount.videoMesh.udp = 0;
639
+ this.expectedResultsCount.public.udp = 0;
640
+ this.expectedResultsCount.public.tcp = 0;
641
+ this.expectedResultsCount.public.xtls = 0;
642
+
643
+ this.resultsCount.videoMesh.udp = 0;
644
+ this.resultsCount.public.udp = 0;
645
+ this.resultsCount.public.tcp = 0;
646
+ this.resultsCount.public.xtls = 0;
647
+ }
648
+
398
649
  /**
399
650
  * Performs reachability checks for all clusters
400
651
  * @param {ClusterList} clusterList
401
- * @returns {Promise<ReachabilityResults>} reachability check results
652
+ * @returns {Promise<void>} promise that's resolved as soon as the checks are started
402
653
  */
403
- private async performReachabilityChecks(clusterList: ClusterList): Promise<ReachabilityResults> {
654
+ private async performReachabilityChecks(clusterList: ClusterList) {
404
655
  const results: ReachabilityResults = {};
405
656
 
657
+ this.clusterReachability = {};
658
+
406
659
  if (!clusterList || !Object.keys(clusterList).length) {
407
- return Promise.resolve(results);
660
+ return;
408
661
  }
409
662
 
410
663
  LoggerProxy.logger.log(
@@ -417,7 +670,11 @@ export default class Reachability {
417
670
  } reachability checks`
418
671
  );
419
672
 
420
- const clusterReachabilityChecks = Object.keys(clusterList).map((key) => {
673
+ this.resetResultCounters();
674
+ this.startTimers();
675
+
676
+ // sanitize the urls in the clusterList
677
+ Object.keys(clusterList).forEach((key) => {
421
678
  const cluster = clusterList[key];
422
679
 
423
680
  // Linus doesn't support TCP reachability checks on video mesh nodes
@@ -429,6 +686,7 @@ export default class Reachability {
429
686
  cluster.tcp = [];
430
687
  }
431
688
 
689
+ // Linus doesn't support xTLS reachability checks on video mesh nodes
432
690
  const includeTlsReachability =
433
691
  // @ts-ignore
434
692
  this.webex.config.meetings.experimental.enableTlsReachability && !cluster.isVideoMesh;
@@ -437,18 +695,94 @@ export default class Reachability {
437
695
  cluster.xtls = [];
438
696
  }
439
697
 
440
- this.clusterReachability[key] = new ClusterReachability(key, cluster);
698
+ // initialize the result for this cluster
699
+ results[key] = {
700
+ udp: {result: cluster.udp.length > 0 ? 'unreachable' : 'untested'},
701
+ tcp: {result: cluster.tcp.length > 0 ? 'unreachable' : 'untested'},
702
+ xtls: {result: cluster.xtls.length > 0 ? 'unreachable' : 'untested'},
703
+ isVideoMesh: cluster.isVideoMesh,
704
+ };
705
+
706
+ // update expected results counters to include this cluster
707
+ this.expectedResultsCount[cluster.isVideoMesh ? 'videoMesh' : 'public'].udp +=
708
+ cluster.udp.length;
709
+ if (!cluster.isVideoMesh) {
710
+ this.expectedResultsCount.public.tcp += cluster.tcp.length;
711
+ this.expectedResultsCount.public.xtls += cluster.xtls.length;
712
+ }
713
+ });
714
+
715
+ const isFirstResult = {
716
+ udp: true,
717
+ tcp: true,
718
+ xtls: true,
719
+ };
720
+
721
+ // save the initialized results (in case we don't get any "resultReady" events at all)
722
+ await this.storeResults(results);
723
+
724
+ // now start the reachability on all the clusters
725
+ Object.keys(clusterList).forEach((key) => {
726
+ const cluster = clusterList[key];
441
727
 
442
- return this.clusterReachability[key].start().then((result) => {
443
- results[key] = result;
444
- results[key].isVideoMesh = cluster.isVideoMesh;
728
+ this.clusterReachability[key] = new ClusterReachability(key, cluster);
729
+ this.clusterReachability[key].on(Events.resultReady, async (data: ResultEventData) => {
730
+ const {protocol, result, clientMediaIPs, latencyInMilliseconds} = data;
731
+
732
+ if (isFirstResult[protocol]) {
733
+ this.emit(
734
+ {
735
+ file: 'reachability',
736
+ function: 'resultReady event handler',
737
+ },
738
+ 'reachability:firstResultAvailable',
739
+ {
740
+ protocol,
741
+ }
742
+ );
743
+ isFirstResult[protocol] = false;
744
+ }
745
+ this.resultsCount[cluster.isVideoMesh ? 'videoMesh' : 'public'][protocol] += 1;
746
+
747
+ const areAllResultsReady = this.areAllResultsReady();
748
+
749
+ results[key][protocol].result = result;
750
+ results[key][protocol].clientMediaIPs = clientMediaIPs;
751
+ results[key][protocol].latencyInMilliseconds = latencyInMilliseconds;
752
+
753
+ await this.storeResults(results);
754
+
755
+ if (areAllResultsReady) {
756
+ clearTimeout(this.overallTimer);
757
+ this.overallTimer = undefined;
758
+ this.emit(
759
+ {
760
+ file: 'reachability',
761
+ function: 'performReachabilityChecks',
762
+ },
763
+ 'reachability:done',
764
+ {}
765
+ );
766
+ this.sendMetric();
767
+
768
+ LoggerProxy.logger.log(
769
+ `Reachability:index#gatherReachability --> Reachability checks fully completed`
770
+ );
771
+ this.resolveReachabilityPromise();
772
+ }
445
773
  });
446
- });
447
774
 
448
- await Promise.all(clusterReachabilityChecks);
775
+ // clientMediaIps can be updated independently from the results, so we need to listen for them too
776
+ this.clusterReachability[key].on(
777
+ Events.clientMediaIpsUpdated,
778
+ async (data: ClientMediaIpsUpdatedEventData) => {
779
+ results[key][data.protocol].clientMediaIPs = data.clientMediaIPs;
449
780
 
450
- this.logUnreachableClusters();
781
+ await this.storeResults(results);
782
+ }
783
+ );
451
784
 
452
- return results;
785
+ this.clusterReachability[key].start(); // not awaiting on purpose
786
+ });
453
787
  }
454
788
  }
@@ -342,7 +342,7 @@ export default class ReconnectionManager {
342
342
  }
343
343
 
344
344
  try {
345
- await this.webex.meetings.startReachability();
345
+ await this.webex.meetings.startReachability('reconnection');
346
346
  } catch (err) {
347
347
  LoggerProxy.logger.info(
348
348
  'ReconnectionManager:index#reconnect --> Reachability failed, continuing with reconnection attempt, err: ',
@@ -34,6 +34,8 @@ export default class RtcMetrics {
34
34
 
35
35
  connectionId: string;
36
36
 
37
+ shouldSendMetricsOnNextStatsReport: boolean;
38
+
37
39
  /**
38
40
  * Initialize the interval.
39
41
  *
@@ -47,9 +49,7 @@ export default class RtcMetrics {
47
49
  this.meetingId = meetingId;
48
50
  this.webex = webex;
49
51
  this.correlationId = correlationId;
50
- this.setNewConnectionId();
51
- // Send the first set of metrics at 5 seconds in the case of a user leaving the call shortly after joining.
52
- setTimeout(this.sendMetricsInQueue.bind(this), 5 * 1000);
52
+ this.resetConnection();
53
53
  }
54
54
 
55
55
  /**
@@ -64,6 +64,18 @@ export default class RtcMetrics {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Forces sending metrics when we get the next stats-report
69
+ *
70
+ * This is useful for cases when something important happens that affects the media connection,
71
+ * for example when we move from lobby into the meeting.
72
+ *
73
+ * @returns {void}
74
+ */
75
+ public sendNextMetrics() {
76
+ this.shouldSendMetricsOnNextStatsReport = true;
77
+ }
78
+
67
79
  /**
68
80
  * Add metrics items to the metrics queue.
69
81
  *
@@ -79,6 +91,13 @@ export default class RtcMetrics {
79
91
 
80
92
  this.metricsQueue.push(data);
81
93
 
94
+ if (this.shouldSendMetricsOnNextStatsReport && data.name === 'stats-report') {
95
+ // this is the first useful set of data (WCME gives it to us after 5s), send it out immediately
96
+ // in case the user is unhappy and closes the browser early
97
+ this.sendMetricsInQueue();
98
+ this.shouldSendMetricsOnNextStatsReport = false;
99
+ }
100
+
82
101
  try {
83
102
  // If a connection fails, send the rest of the metrics in queue and get a new connection id.
84
103
  const parsedPayload = parseJsonPayload(data.payload);
@@ -88,7 +107,7 @@ export default class RtcMetrics {
88
107
  parsedPayload.value === 'failed'
89
108
  ) {
90
109
  this.sendMetricsInQueue();
91
- this.setNewConnectionId();
110
+ this.resetConnection();
92
111
  }
93
112
  } catch (e) {
94
113
  console.error(e);
@@ -130,8 +149,9 @@ export default class RtcMetrics {
130
149
  *
131
150
  * @returns {void}
132
151
  */
133
- private setNewConnectionId() {
152
+ private resetConnection() {
134
153
  this.connectionId = uuid.v4();
154
+ this.shouldSendMetricsOnNextStatsReport = true;
135
155
  }
136
156
 
137
157
  /**
@@ -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