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

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 (131) 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 +12 -0
  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 +554 -358
  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 +37 -33
  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 +415 -56
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/types/constants.d.ts +11 -3
  51. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  52. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  53. package/dist/types/meeting/index.d.ts +27 -8
  54. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  55. package/dist/types/meeting-info/index.d.ts +3 -2
  56. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  57. package/dist/types/meeting-info/util.d.ts +5 -4
  58. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  59. package/dist/types/meetings/collection.d.ts +3 -2
  60. package/dist/types/meetings/index.d.ts +4 -3
  61. package/dist/types/meetings/meetings.types.d.ts +9 -0
  62. package/dist/types/metrics/constants.d.ts +1 -0
  63. package/dist/types/metrics/index.d.ts +15 -0
  64. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  65. package/dist/types/reachability/index.d.ts +93 -2
  66. package/dist/webinar/index.js +1 -1
  67. package/package.json +23 -23
  68. package/src/breakouts/index.ts +7 -1
  69. package/src/constants.ts +13 -17
  70. package/src/locus-info/selfUtils.ts +0 -5
  71. package/src/media/MediaConnectionAwaiter.ts +89 -14
  72. package/src/media/index.ts +13 -0
  73. package/src/meeting/connectionStateHandler.ts +65 -0
  74. package/src/meeting/index.ts +532 -295
  75. package/src/meeting/locusMediaRequest.ts +5 -0
  76. package/src/meeting/muteState.ts +6 -1
  77. package/src/meeting/util.ts +1 -0
  78. package/src/meeting-info/index.ts +9 -6
  79. package/src/meeting-info/meeting-info-v2.ts +4 -4
  80. package/src/meeting-info/util.ts +23 -28
  81. package/src/meeting-info/utilv2.ts +18 -24
  82. package/src/meetings/collection.ts +3 -3
  83. package/src/meetings/index.ts +39 -40
  84. package/src/meetings/meetings.types.ts +11 -0
  85. package/src/meetings/util.ts +5 -4
  86. package/src/metrics/constants.ts +1 -0
  87. package/src/metrics/index.ts +44 -0
  88. package/src/personal-meeting-room/index.ts +2 -2
  89. package/src/reachability/clusterReachability.ts +86 -25
  90. package/src/reachability/index.ts +316 -27
  91. package/test/unit/spec/breakouts/index.ts +51 -32
  92. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  93. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  94. package/test/unit/spec/media/index.ts +42 -27
  95. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  96. package/test/unit/spec/meeting/index.js +762 -179
  97. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  98. package/test/unit/spec/meeting/muteState.js +24 -0
  99. package/test/unit/spec/meeting-info/index.js +4 -4
  100. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  101. package/test/unit/spec/meeting-info/request.js +2 -2
  102. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  103. package/test/unit/spec/meetings/index.js +14 -0
  104. package/test/unit/spec/metrics/index.js +126 -0
  105. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  106. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  107. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  108. package/test/unit/spec/reachability/index.ts +1153 -84
  109. package/test/unit/spec/rtcMetrics/index.ts +1 -0
  110. package/dist/mediaQualityMetrics/config.js +0 -321
  111. package/dist/mediaQualityMetrics/config.js.map +0 -1
  112. package/dist/networkQualityMonitor/index.js +0 -227
  113. package/dist/networkQualityMonitor/index.js.map +0 -1
  114. package/dist/statsAnalyzer/global.js +0 -44
  115. package/dist/statsAnalyzer/global.js.map +0 -1
  116. package/dist/statsAnalyzer/index.js +0 -1072
  117. package/dist/statsAnalyzer/index.js.map +0 -1
  118. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  119. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  120. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  121. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  122. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  123. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  124. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  125. package/src/mediaQualityMetrics/config.ts +0 -255
  126. package/src/networkQualityMonitor/index.ts +0 -211
  127. package/src/statsAnalyzer/global.ts +0 -37
  128. package/src/statsAnalyzer/index.ts +0 -1318
  129. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  130. package/test/unit/spec/networkQualityMonitor/index.js +0 -99
  131. package/test/unit/spec/stats-analyzer/index.js +0 -1819
@@ -9,6 +9,7 @@ import {
9
9
  ClientEvent,
10
10
  ClientEventLeaveReason,
11
11
  CallDiagnosticUtils,
12
+ CALL_DIAGNOSTIC_CONFIG,
12
13
  } from '@webex/internal-plugin-metrics';
13
14
  import {ClientEvent as RawClientEvent} from '@webex/event-dictionary-ts';
14
15
 
@@ -16,11 +17,15 @@ import {
16
17
  ConnectionState,
17
18
  Errors,
18
19
  ErrorType,
19
- Event,
20
+ MediaConnectionEventNames,
20
21
  MediaContent,
21
22
  MediaType,
22
23
  RemoteTrackType,
23
24
  RoapMessage,
25
+ StatsAnalyzer,
26
+ StatsAnalyzerEventNames,
27
+ NetworkQualityEventNames,
28
+ NetworkQualityMonitor,
24
29
  } from '@webex/internal-media-core';
25
30
 
26
31
  import {
@@ -40,6 +45,7 @@ import {
40
45
  TURN_ON_CAPTION_STATUS,
41
46
  type MeetingTranscriptPayload,
42
47
  } from '@webex/internal-plugin-voicea';
48
+
43
49
  import {processNewCaptions} from './voicea-meeting';
44
50
 
45
51
  import {
@@ -50,8 +56,6 @@ import {
50
56
  AddMediaFailed,
51
57
  } from '../common/errors/webex-errors';
52
58
 
53
- import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer';
54
- import NetworkQualityMonitor from '../networkQualityMonitor';
55
59
  import LoggerProxy from '../common/logs/logger-proxy';
56
60
  import EventsUtil from '../common/events/util';
57
61
  import Trigger from '../common/events/trigger-proxy';
@@ -79,10 +83,9 @@ import {Reactions, SkinTones} from '../reactions/reactions';
79
83
  import PasswordError from '../common/errors/password-error';
80
84
  import CaptchaError from '../common/errors/captcha-error';
81
85
  import {
82
- _CONVERSATION_URL_,
86
+ DESTINATION_TYPE,
83
87
  _INCOMING_,
84
88
  _JOIN_,
85
- _MEETING_LINK_,
86
89
  AUDIO,
87
90
  CONTENT,
88
91
  DISPLAY_HINTS,
@@ -116,9 +119,7 @@ import {
116
119
  MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
117
120
  MEETING_PERMISSION_TOKEN_REFRESH_REASON,
118
121
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
119
- RECONNECTION,
120
122
  NAMED_MEDIA_GROUP_TYPE_AUDIO,
121
- LANGUAGE_ENGLISH,
122
123
  } from '../constants';
123
124
  import BEHAVIORAL_METRICS from '../metrics/constants';
124
125
  import ParameterError from '../common/errors/parameter';
@@ -153,6 +154,10 @@ import RecordingController from '../recording-controller';
153
154
  import ControlsOptionsManager from '../controls-options-manager';
154
155
  import PermissionError from '../common/errors/permission';
155
156
  import {LocusMediaRequest} from './locusMediaRequest';
157
+ import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
158
+
159
+ // default callback so we don't call an undefined function, but in practice it should never be used
160
+ const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
156
161
 
157
162
  const logRequest = (request: any, {logText = ''}) => {
158
163
  LoggerProxy.logger.info(`${logText} - sending request`);
@@ -526,7 +531,7 @@ export default class Meeting extends StatelessWebexPlugin {
526
531
  conversationUrl: string;
527
532
  callStateForMetrics: CallStateForMetrics;
528
533
  destination: string;
529
- destinationType: string;
534
+ destinationType: DESTINATION_TYPE;
530
535
  deviceUrl: string;
531
536
  hostId: string;
532
537
  id: string;
@@ -644,6 +649,10 @@ export default class Meeting extends StatelessWebexPlugin {
644
649
  })}event#${EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION}`
645
650
  );
646
651
 
652
+ if (this.getCurUserType() !== 'host') {
653
+ delete payload.spokenLanguages;
654
+ }
655
+
647
656
  // @ts-ignore
648
657
  this.trigger(EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION, payload);
649
658
  },
@@ -674,12 +683,19 @@ export default class Meeting extends StatelessWebexPlugin {
674
683
  },
675
684
  };
676
685
 
677
- private retriedWithTurnServer: boolean;
686
+ private addMediaData: {
687
+ retriedWithTurnServer: boolean;
688
+ icePhaseCallback: () => string;
689
+ };
690
+
678
691
  private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
679
692
  private deferSDPAnswer?: Defer; // used for waiting for a response
680
693
  private sdpResponseTimer?: ReturnType<typeof setTimeout>;
681
694
  private hasMediaConnectionConnectedAtLeastOnce: boolean;
682
695
  private joinWithMediaRetryInfo?: {isRetry: boolean; prevJoinResponse?: any};
696
+ private connectionStateHandler?: ConnectionStateHandler;
697
+ private iceCandidateErrors: Map<string, number>;
698
+ private iceCandidatesCount: number;
683
699
 
684
700
  /**
685
701
  * @param {Object} attrs
@@ -1445,13 +1461,19 @@ export default class Meeting extends StatelessWebexPlugin {
1445
1461
  this.turnServerUsed = false;
1446
1462
 
1447
1463
  /**
1448
- * Whether retry was done using TURN Discovery.
1464
+ * Contains information used during the addMedia() operation:
1465
+ * retriedWithTurnServer - whether retry was done using TURN Discovery
1466
+ * icePhaseCallback - callback for determining the value for icePhase when sending failure event to CA
1467
+ *
1449
1468
  * @instance
1450
- * @type {boolean}
1469
+ * @type {Object}
1451
1470
  * @private
1452
1471
  * @memberof Meeting
1453
1472
  */
1454
- this.retriedWithTurnServer = false;
1473
+ this.addMediaData = {
1474
+ retriedWithTurnServer: false,
1475
+ icePhaseCallback: DEFAULT_ICE_PHASE_CALLBACK,
1476
+ };
1455
1477
 
1456
1478
  /**
1457
1479
  * Whether or not the media connection has ever successfully connected.
@@ -1470,6 +1492,33 @@ export default class Meeting extends StatelessWebexPlugin {
1470
1492
  * @memberof Meeting
1471
1493
  */
1472
1494
  this.joinWithMediaRetryInfo = {isRetry: false, prevJoinResponse: undefined};
1495
+
1496
+ /**
1497
+ * Connection state handler
1498
+ * @instance
1499
+ * @type {ConnectionStateHandler}
1500
+ * @private
1501
+ * @memberof Meeting
1502
+ */
1503
+ this.connectionStateHandler = undefined;
1504
+
1505
+ /**
1506
+ * ICE Candidates errors map
1507
+ * @instance
1508
+ * @type {Map<[number, string], number>}
1509
+ * @private
1510
+ * @memberof Meeting
1511
+ */
1512
+ this.iceCandidateErrors = new Map();
1513
+
1514
+ /**
1515
+ * Gathered ICE Candidates count
1516
+ * @instance
1517
+ * @type {number}
1518
+ * @private
1519
+ * @memberof Meeting
1520
+ */
1521
+ this.iceCandidatesCount = 0;
1473
1522
  }
1474
1523
 
1475
1524
  /**
@@ -1718,7 +1767,7 @@ export default class Meeting extends StatelessWebexPlugin {
1718
1767
  }
1719
1768
 
1720
1769
  const isStartingSpaceInstantV2Meeting =
1721
- this.destinationType === _CONVERSATION_URL_ &&
1770
+ this.destinationType === DESTINATION_TYPE.CONVERSATION_URL &&
1722
1771
  // @ts-ignore - config coming from registerPlugin
1723
1772
  this.config.experimental.enableAdhocMeetings &&
1724
1773
  // @ts-ignore
@@ -1727,7 +1776,9 @@ export default class Meeting extends StatelessWebexPlugin {
1727
1776
  const destination = isStartingSpaceInstantV2Meeting
1728
1777
  ? this.meetingInfo.meetingJoinUrl
1729
1778
  : this.destination;
1730
- const destinationType = isStartingSpaceInstantV2Meeting ? _MEETING_LINK_ : this.destinationType;
1779
+ const destinationType = isStartingSpaceInstantV2Meeting
1780
+ ? DESTINATION_TYPE.MEETING_LINK
1781
+ : this.destinationType;
1731
1782
 
1732
1783
  const permissionTokenExpiryInfo = this.getPermissionTokenExpiryInfo();
1733
1784
 
@@ -3114,6 +3165,9 @@ export default class Meeting extends StatelessWebexPlugin {
3114
3165
  correlation_id: this.correlationId,
3115
3166
  locus_id: this.locusId,
3116
3167
  });
3168
+ LoggerProxy.logger.info(
3169
+ 'Meeting:index#setUpLocusInfoSelfListener --> MEDIA_INACTIVITY received, reconnecting...'
3170
+ );
3117
3171
  this.reconnect();
3118
3172
  });
3119
3173
 
@@ -4242,8 +4296,6 @@ export default class Meeting extends StatelessWebexPlugin {
4242
4296
  * @memberof Meeting
4243
4297
  */
4244
4298
  public closePeerConnections() {
4245
- this.locusMediaRequest = undefined;
4246
-
4247
4299
  if (this.mediaProperties.webrtcMediaConnection) {
4248
4300
  if (this.remoteMediaManager) {
4249
4301
  this.remoteMediaManager.stop();
@@ -4556,38 +4608,57 @@ export default class Meeting extends StatelessWebexPlugin {
4556
4608
  try {
4557
4609
  let turnServerInfo;
4558
4610
  let turnDiscoverySkippedReason;
4611
+ let forceTurnDiscovery = false;
4559
4612
 
4560
- // @ts-ignore
4561
- joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4562
- const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(this, true);
4613
+ if (!joinResponse) {
4614
+ // This is the 1st attempt or a retry after join request failed -> we need to do a join with TURN discovery
4563
4615
 
4564
- ({turnDiscoverySkippedReason} = turnDiscoveryRequest);
4565
- joinOptions.roapMessage = turnDiscoveryRequest.roapMessage;
4616
+ // @ts-ignore
4617
+ joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4618
+ const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(
4619
+ this,
4620
+ true
4621
+ );
4622
+
4623
+ ({turnDiscoverySkippedReason} = turnDiscoveryRequest);
4624
+ joinOptions.roapMessage = turnDiscoveryRequest.roapMessage;
4566
4625
 
4567
- if (!joinResponse) {
4568
4626
  LoggerProxy.logger.info(
4569
4627
  'Meeting:index#joinWithMedia ---> calling join with joinOptions, ',
4570
4628
  joinOptions
4571
4629
  );
4572
4630
 
4573
4631
  joinResponse = await this.join(joinOptions);
4574
- }
4575
4632
 
4576
- joined = true;
4633
+ joined = true;
4577
4634
 
4578
- if (joinOptions.roapMessage) {
4579
- ({turnServerInfo, turnDiscoverySkippedReason} =
4580
- await this.roap.handleTurnDiscoveryHttpResponse(this, joinResponse));
4635
+ // if we sent out TURN discovery Roap message with join, process the TURN discovery response
4636
+ if (joinOptions.roapMessage) {
4637
+ ({turnServerInfo, turnDiscoverySkippedReason} =
4638
+ await this.roap.handleTurnDiscoveryHttpResponse(this, joinResponse));
4581
4639
 
4582
- this.turnDiscoverySkippedReason = turnDiscoverySkippedReason;
4583
- this.turnServerUsed = !!turnServerInfo;
4640
+ this.turnDiscoverySkippedReason = turnDiscoverySkippedReason;
4641
+ this.turnServerUsed = !!turnServerInfo;
4584
4642
 
4585
- if (turnServerInfo === undefined) {
4586
- this.roap.abortTurnDiscovery();
4643
+ if (turnServerInfo === undefined) {
4644
+ this.roap.abortTurnDiscovery();
4645
+ }
4587
4646
  }
4647
+ } else {
4648
+ // This is a retry, when join succeeded but addMedia failed, so we'll just call addMedia() again,
4649
+ // but we need to ensure that it also does a new TURN discovery
4650
+ forceTurnDiscovery = true;
4651
+ joined = true;
4588
4652
  }
4589
4653
 
4590
- const mediaResponse = await this.addMedia(mediaOptions, turnServerInfo);
4654
+ const mediaResponse = await this.addMediaInternal(
4655
+ () => {
4656
+ return this.joinWithMediaRetryInfo.isRetry ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY';
4657
+ },
4658
+ turnServerInfo,
4659
+ forceTurnDiscovery,
4660
+ mediaOptions
4661
+ );
4591
4662
 
4592
4663
  this.joinWithMediaRetryInfo = {isRetry: false, prevJoinResponse: undefined};
4593
4664
 
@@ -4626,7 +4697,16 @@ export default class Meeting extends StatelessWebexPlugin {
4626
4697
  }
4627
4698
  );
4628
4699
 
4629
- if (!isRetry) {
4700
+ // if this was the first attempt, let's do a retry
4701
+ let shouldRetry = !isRetry;
4702
+
4703
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
4704
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
4705
+ // so there is no point doing a retry
4706
+ shouldRetry = false;
4707
+ }
4708
+
4709
+ if (shouldRetry) {
4630
4710
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4631
4711
  this.joinWithMediaRetryInfo.isRetry = true;
4632
4712
  this.joinWithMediaRetryInfo.prevJoinResponse = joinResponse;
@@ -4786,6 +4866,14 @@ export default class Meeting extends StatelessWebexPlugin {
4786
4866
  reject(new Error('Webex Assistant is not enabled/supported'));
4787
4867
  }
4788
4868
 
4869
+ if (this.getCurUserType() !== 'host') {
4870
+ LoggerProxy.logger.error(
4871
+ 'Meeting:index#setSpokenLanguage --> Only host can set spoken language'
4872
+ );
4873
+
4874
+ reject(new Error('Only host can set spoken language'));
4875
+ }
4876
+
4789
4877
  try {
4790
4878
  const voiceaListenerLanguageUpdate = (payload) => {
4791
4879
  // @ts-ignore
@@ -4839,10 +4927,8 @@ export default class Meeting extends StatelessWebexPlugin {
4839
4927
  this.setUpVoiceaListeners();
4840
4928
  }
4841
4929
 
4842
- if (this.getCurUserType() === 'host') {
4843
- // @ts-ignore
4844
- await this.webex.internal.voicea.turnOnCaptions(options?.spokenLanguage);
4845
- }
4930
+ // @ts-ignore
4931
+ await this.webex.internal.voicea.turnOnCaptions(options?.spokenLanguage);
4846
4932
  } catch (error) {
4847
4933
  LoggerProxy.logger.error(`Meeting:index#startTranscription --> ${error}`);
4848
4934
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
@@ -5141,6 +5227,8 @@ export default class Meeting extends StatelessWebexPlugin {
5141
5227
  return MeetingUtil.joinMeetingOptions(this, options)
5142
5228
  .then((join) => {
5143
5229
  this.meetingFiniteStateMachine.join();
5230
+ this.setupLocusMediaRequest();
5231
+
5144
5232
  LoggerProxy.logger.log('Meeting:index#join --> Success');
5145
5233
 
5146
5234
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
@@ -5233,8 +5321,13 @@ export default class Meeting extends StatelessWebexPlugin {
5233
5321
 
5234
5322
  // @ts-ignore - Fix type
5235
5323
  if (this.webex.internal.llm.isConnected()) {
5236
- // @ts-ignore - Fix type
5237
- if (url === this.webex.internal.llm.getLocusUrl() && isJoined) {
5324
+ if (
5325
+ // @ts-ignore - Fix type
5326
+ url === this.webex.internal.llm.getLocusUrl() &&
5327
+ // @ts-ignore - Fix type
5328
+ datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5329
+ isJoined
5330
+ ) {
5238
5331
  return undefined;
5239
5332
  }
5240
5333
  // @ts-ignore - Fix type
@@ -5634,219 +5727,258 @@ export default class Meeting extends StatelessWebexPlugin {
5634
5727
  * @returns {undefined}
5635
5728
  */
5636
5729
  setupSdpListeners = () => {
5637
- this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_SDP_ANSWER_PROCESSED, () => {
5638
- // @ts-ignore
5639
- const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5730
+ this.mediaProperties.webrtcMediaConnection.on(
5731
+ MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED,
5732
+ () => {
5733
+ // @ts-ignore
5734
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5640
5735
 
5641
- // @ts-ignore
5642
- this.webex.internal.newMetrics.submitClientEvent({
5643
- name: 'client.media-engine.remote-sdp-received',
5644
- options: {meetingId: this.id},
5645
- });
5646
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, {
5647
- correlation_id: this.correlationId,
5648
- latency: cdl.getLocalSDPGenRemoteSDPRecv(),
5649
- meetingId: this.id,
5650
- });
5736
+ // @ts-ignore
5737
+ this.webex.internal.newMetrics.submitClientEvent({
5738
+ name: 'client.media-engine.remote-sdp-received',
5739
+ options: {meetingId: this.id},
5740
+ });
5741
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, {
5742
+ correlation_id: this.correlationId,
5743
+ latency: cdl.getLocalSDPGenRemoteSDPRecv(),
5744
+ meetingId: this.id,
5745
+ });
5651
5746
 
5652
- if (this.deferSDPAnswer) {
5653
- this.deferSDPAnswer.resolve();
5654
- clearTimeout(this.sdpResponseTimer);
5655
- this.sdpResponseTimer = undefined;
5747
+ if (this.deferSDPAnswer) {
5748
+ this.deferSDPAnswer.resolve();
5749
+ clearTimeout(this.sdpResponseTimer);
5750
+ this.sdpResponseTimer = undefined;
5751
+ }
5656
5752
  }
5657
- });
5753
+ );
5658
5754
 
5659
- this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_OFFER_GENERATED, () => {
5660
- // @ts-ignore
5661
- this.webex.internal.newMetrics.submitClientEvent({
5662
- name: 'client.media-engine.local-sdp-generated',
5663
- options: {meetingId: this.id},
5664
- });
5755
+ this.mediaProperties.webrtcMediaConnection.on(
5756
+ MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED,
5757
+ () => {
5758
+ // @ts-ignore
5759
+ this.webex.internal.newMetrics.submitClientEvent({
5760
+ name: 'client.media-engine.local-sdp-generated',
5761
+ options: {meetingId: this.id},
5762
+ });
5665
5763
 
5666
- // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5667
- this.deferSDPAnswer = new Defer();
5668
- });
5764
+ // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5765
+ this.deferSDPAnswer = new Defer();
5766
+ }
5767
+ );
5669
5768
 
5670
- this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_ANSWER_GENERATED, () => {
5671
- // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer
5672
- // @ts-ignore
5673
- this.webex.internal.newMetrics.submitClientEvent({
5674
- name: 'client.media-engine.remote-sdp-received',
5675
- options: {meetingId: this.id},
5676
- });
5677
- });
5769
+ this.mediaProperties.webrtcMediaConnection.on(
5770
+ MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED,
5771
+ () => {
5772
+ // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer
5773
+ // @ts-ignore
5774
+ this.webex.internal.newMetrics.submitClientEvent({
5775
+ name: 'client.media-engine.remote-sdp-received',
5776
+ options: {meetingId: this.id},
5777
+ });
5778
+ }
5779
+ );
5678
5780
  };
5679
5781
 
5680
5782
  setupMediaConnectionListeners = () => {
5681
5783
  this.setupSdpListeners();
5682
5784
 
5683
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
5785
+ this.mediaProperties.webrtcMediaConnection.on(MediaConnectionEventNames.ROAP_STARTED, () => {
5684
5786
  this.isRoapInProgress = true;
5685
5787
  });
5686
5788
 
5687
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
5789
+ this.mediaProperties.webrtcMediaConnection.on(MediaConnectionEventNames.ROAP_DONE, () => {
5688
5790
  this.mediaNegotiatedEvent();
5689
5791
  this.isRoapInProgress = false;
5690
5792
  this.processNextQueuedMediaUpdate();
5691
5793
  });
5692
5794
 
5693
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
5795
+ this.mediaProperties.webrtcMediaConnection.on(
5796
+ MediaConnectionEventNames.ROAP_FAILURE,
5797
+ this.handleRoapFailure
5798
+ );
5694
5799
 
5695
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_MESSAGE_TO_SEND, (event) => {
5696
- const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
5800
+ this.mediaProperties.webrtcMediaConnection.on(
5801
+ MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND,
5802
+ (event) => {
5803
+ const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
5804
+
5805
+ switch (event.roapMessage.messageType) {
5806
+ case 'OK':
5807
+ logRequest(
5808
+ this.roap.sendRoapOK({
5809
+ seq: event.roapMessage.seq,
5810
+ mediaId: this.mediaId,
5811
+ correlationId: this.correlationId,
5812
+ }),
5813
+ {
5814
+ logText: `${LOG_HEADER} Roap OK`,
5815
+ }
5816
+ );
5817
+ break;
5697
5818
 
5698
- switch (event.roapMessage.messageType) {
5699
- case 'OK':
5700
- logRequest(
5701
- this.roap.sendRoapOK({
5702
- seq: event.roapMessage.seq,
5703
- mediaId: this.mediaId,
5704
- correlationId: this.correlationId,
5705
- }),
5706
- {
5707
- logText: `${LOG_HEADER} Roap OK`,
5708
- }
5709
- );
5710
- break;
5819
+ case 'OFFER':
5820
+ logRequest(
5821
+ this.roap
5822
+ .sendRoapMediaRequest({
5823
+ sdp: event.roapMessage.sdp,
5824
+ seq: event.roapMessage.seq,
5825
+ tieBreaker: event.roapMessage.tieBreaker,
5826
+ meeting: this, // or can pass meeting ID
5827
+ })
5828
+ .then(({roapAnswer}) => {
5829
+ if (roapAnswer) {
5830
+ LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`);
5831
+
5832
+ this.roapMessageReceived(roapAnswer);
5833
+ }
5834
+ }),
5835
+ {
5836
+ logText: `${LOG_HEADER} Roap Offer`,
5837
+ }
5838
+ ).catch((error) => {
5839
+ // @ts-ignore
5840
+ this.webex.internal.newMetrics.submitClientEvent({
5841
+ name: 'client.media-engine.remote-sdp-received',
5842
+ payload: {
5843
+ canProceed: false,
5844
+ errors: [
5845
+ // @ts-ignore
5846
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5847
+ {
5848
+ clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
5849
+ }
5850
+ ),
5851
+ ],
5852
+ },
5853
+ options: {meetingId: this.id, rawError: error},
5854
+ });
5855
+
5856
+ this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
5857
+ clearTimeout(this.sdpResponseTimer);
5858
+ this.sdpResponseTimer = undefined;
5859
+ });
5860
+ break;
5711
5861
 
5712
- case 'OFFER':
5713
- logRequest(
5714
- this.roap
5715
- .sendRoapMediaRequest({
5862
+ case 'ANSWER':
5863
+ logRequest(
5864
+ this.roap.sendRoapAnswer({
5716
5865
  sdp: event.roapMessage.sdp,
5717
5866
  seq: event.roapMessage.seq,
5718
- tieBreaker: event.roapMessage.tieBreaker,
5719
- meeting: this, // or can pass meeting ID
5720
- })
5721
- .then(({roapAnswer}) => {
5722
- if (roapAnswer) {
5723
- LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`);
5724
-
5725
- this.roapMessageReceived(roapAnswer);
5726
- }
5867
+ mediaId: this.mediaId,
5868
+ correlationId: this.correlationId,
5727
5869
  }),
5728
- {
5729
- logText: `${LOG_HEADER} Roap Offer`,
5730
- }
5731
- ).catch(() => {
5732
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
5733
- clearTimeout(this.sdpResponseTimer);
5734
- this.sdpResponseTimer = undefined;
5735
- });
5736
- break;
5737
-
5738
- case 'ANSWER':
5739
- logRequest(
5740
- this.roap.sendRoapAnswer({
5741
- sdp: event.roapMessage.sdp,
5742
- seq: event.roapMessage.seq,
5743
- mediaId: this.mediaId,
5744
- correlationId: this.correlationId,
5745
- }),
5746
- {
5747
- logText: `${LOG_HEADER} Roap Answer`,
5748
- }
5749
- ).catch((error) => {
5750
- const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
5751
- const data = {
5752
- correlation_id: this.correlationId,
5753
- locus_id: this.locusUrl.split('/').pop(),
5754
- reason: error.message,
5755
- stack: error.stack,
5756
- };
5757
- const metadata = {
5758
- type: error.name,
5759
- };
5760
-
5761
- Metrics.sendBehavioralMetric(metricName, data, metadata);
5762
- });
5763
- break;
5870
+ {
5871
+ logText: `${LOG_HEADER} Roap Answer`,
5872
+ }
5873
+ ).catch((error) => {
5874
+ const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
5875
+ const data = {
5876
+ correlation_id: this.correlationId,
5877
+ locus_id: this.locusUrl.split('/').pop(),
5878
+ reason: error.message,
5879
+ stack: error.stack,
5880
+ };
5881
+ const metadata = {
5882
+ type: error.name,
5883
+ };
5764
5884
 
5765
- case 'ERROR':
5766
- if (
5767
- event.roapMessage.errorType === ErrorType.CONFLICT ||
5768
- event.roapMessage.errorType === ErrorType.DOUBLECONFLICT
5769
- ) {
5770
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
5771
- correlation_id: this.correlationId,
5772
- locus_id: this.locusUrl.split('/').pop(),
5773
- sequence: event.roapMessage.seq,
5885
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
5774
5886
  });
5775
- }
5776
- logRequest(
5777
- this.roap.sendRoapError({
5778
- seq: event.roapMessage.seq,
5779
- errorType: event.roapMessage.errorType,
5780
- mediaId: this.mediaId,
5781
- correlationId: this.correlationId,
5782
- }),
5783
- {
5784
- logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`,
5887
+ break;
5888
+
5889
+ case 'ERROR':
5890
+ if (
5891
+ event.roapMessage.errorType === ErrorType.CONFLICT ||
5892
+ event.roapMessage.errorType === ErrorType.DOUBLECONFLICT
5893
+ ) {
5894
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
5895
+ correlation_id: this.correlationId,
5896
+ locus_id: this.locusUrl.split('/').pop(),
5897
+ sequence: event.roapMessage.seq,
5898
+ });
5785
5899
  }
5786
- );
5787
- break;
5900
+ logRequest(
5901
+ this.roap.sendRoapError({
5902
+ seq: event.roapMessage.seq,
5903
+ errorType: event.roapMessage.errorType,
5904
+ mediaId: this.mediaId,
5905
+ correlationId: this.correlationId,
5906
+ }),
5907
+ {
5908
+ logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`,
5909
+ }
5910
+ );
5911
+ break;
5788
5912
 
5789
- default:
5790
- LoggerProxy.logger.error(
5791
- `${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}`
5792
- );
5793
- break;
5913
+ default:
5914
+ LoggerProxy.logger.error(
5915
+ `${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}`
5916
+ );
5917
+ break;
5918
+ }
5794
5919
  }
5795
- });
5920
+ );
5796
5921
 
5797
5922
  // eslint-disable-next-line no-param-reassign
5798
- this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_TRACK_ADDED, (event) => {
5799
- LoggerProxy.logger.log(
5800
- `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(
5801
- event
5802
- )}`
5803
- );
5804
-
5805
- if (event.track) {
5806
- const mediaTrack = event.track;
5807
- const remoteStream = new RemoteStream(MediaUtil.createMediaStream([mediaTrack]));
5923
+ this.mediaProperties.webrtcMediaConnection.on(
5924
+ MediaConnectionEventNames.REMOTE_TRACK_ADDED,
5925
+ (event) => {
5926
+ LoggerProxy.logger.log(
5927
+ `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(
5928
+ event
5929
+ )}`
5930
+ );
5808
5931
 
5809
- // eslint-disable-next-line @typescript-eslint/no-shadow
5810
- let eventType;
5932
+ if (event.track) {
5933
+ const mediaTrack = event.track;
5934
+ const remoteStream = new RemoteStream(MediaUtil.createMediaStream([mediaTrack]));
5935
+
5936
+ // eslint-disable-next-line @typescript-eslint/no-shadow
5937
+ let eventType;
5938
+
5939
+ switch (event.type) {
5940
+ case RemoteTrackType.AUDIO:
5941
+ eventType = EVENT_TYPES.REMOTE_AUDIO;
5942
+ this.mediaProperties.setRemoteAudioStream(remoteStream);
5943
+ break;
5944
+ case RemoteTrackType.VIDEO:
5945
+ eventType = EVENT_TYPES.REMOTE_VIDEO;
5946
+ this.mediaProperties.setRemoteVideoStream(remoteStream);
5947
+ break;
5948
+ case RemoteTrackType.SCREENSHARE_VIDEO:
5949
+ eventType = EVENT_TYPES.REMOTE_SHARE;
5950
+ this.mediaProperties.setRemoteShareStream(remoteStream);
5951
+ break;
5952
+ default: {
5953
+ LoggerProxy.logger.log(
5954
+ 'Meeting:index#setupMediaConnectionListeners --> unexpected track'
5955
+ );
5956
+ }
5957
+ }
5811
5958
 
5812
- switch (event.type) {
5813
- case RemoteTrackType.AUDIO:
5814
- eventType = EVENT_TYPES.REMOTE_AUDIO;
5815
- this.mediaProperties.setRemoteAudioStream(remoteStream);
5816
- break;
5817
- case RemoteTrackType.VIDEO:
5818
- eventType = EVENT_TYPES.REMOTE_VIDEO;
5819
- this.mediaProperties.setRemoteVideoStream(remoteStream);
5820
- break;
5821
- case RemoteTrackType.SCREENSHARE_VIDEO:
5822
- eventType = EVENT_TYPES.REMOTE_SHARE;
5823
- this.mediaProperties.setRemoteShareStream(remoteStream);
5824
- break;
5825
- default: {
5826
- LoggerProxy.logger.log(
5827
- 'Meeting:index#setupMediaConnectionListeners --> unexpected track'
5959
+ if (eventType && mediaTrack) {
5960
+ Trigger.trigger(
5961
+ this,
5962
+ {
5963
+ file: 'meeting/index',
5964
+ function: 'setupRemoteTrackListener:MediaConnectionEventNames.REMOTE_TRACK_ADDED',
5965
+ },
5966
+ EVENT_TRIGGERS.MEDIA_READY,
5967
+ {
5968
+ type: eventType,
5969
+ stream: remoteStream.outputStream,
5970
+ }
5828
5971
  );
5829
5972
  }
5830
5973
  }
5831
-
5832
- if (eventType && mediaTrack) {
5833
- Trigger.trigger(
5834
- this,
5835
- {
5836
- file: 'meeting/index',
5837
- function: 'setupRemoteTrackListener:Event.REMOTE_TRACK_ADDED',
5838
- },
5839
- EVENT_TRIGGERS.MEDIA_READY,
5840
- {
5841
- type: eventType,
5842
- stream: remoteStream.outputStream,
5843
- }
5844
- );
5845
- }
5846
5974
  }
5847
- });
5975
+ );
5976
+
5977
+ this.connectionStateHandler = new ConnectionStateHandler(
5978
+ this.mediaProperties.webrtcMediaConnection
5979
+ );
5848
5980
 
5849
- this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5981
+ this.connectionStateHandler.on(ConnectionStateEvent.stateChanged, (event) => {
5850
5982
  const connectionFailed = () => {
5851
5983
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5852
5984
  correlation_id: this.correlationId,
@@ -5875,7 +6007,6 @@ export default class Meeting extends StatelessWebexPlugin {
5875
6007
 
5876
6008
  // @ts-ignore
5877
6009
  const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5878
-
5879
6010
  switch (event.state) {
5880
6011
  case ConnectionState.Connecting:
5881
6012
  if (!this.hasMediaConnectionConnectedAtLeastOnce) {
@@ -5932,25 +6063,28 @@ export default class Meeting extends StatelessWebexPlugin {
5932
6063
  }
5933
6064
  });
5934
6065
 
5935
- this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (csis) => {
5936
- Trigger.trigger(
5937
- this,
5938
- {
5939
- file: 'meeting/index',
5940
- function: 'setupMediaConnectionListeners',
5941
- },
5942
- EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED,
5943
- {
5944
- memberIds: csis
5945
- // @ts-ignore
5946
- .map((csi) => this.members.findMemberByCsi(csi)?.id)
5947
- .filter((item) => item !== undefined),
5948
- }
5949
- );
5950
- });
6066
+ this.mediaProperties.webrtcMediaConnection.on(
6067
+ MediaConnectionEventNames.ACTIVE_SPEAKERS_CHANGED,
6068
+ (csis) => {
6069
+ Trigger.trigger(
6070
+ this,
6071
+ {
6072
+ file: 'meeting/index',
6073
+ function: 'setupMediaConnectionListeners',
6074
+ },
6075
+ EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED,
6076
+ {
6077
+ memberIds: csis
6078
+ // @ts-ignore
6079
+ .map((csi) => this.members.findMemberByCsi(csi)?.id)
6080
+ .filter((item) => item !== undefined),
6081
+ }
6082
+ );
6083
+ }
6084
+ );
5951
6085
 
5952
6086
  this.mediaProperties.webrtcMediaConnection.on(
5953
- Event.VIDEO_SOURCES_COUNT_CHANGED,
6087
+ MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED,
5954
6088
  (numTotalSources, numLiveSources, mediaContent) => {
5955
6089
  Trigger.trigger(
5956
6090
  this,
@@ -5973,7 +6107,7 @@ export default class Meeting extends StatelessWebexPlugin {
5973
6107
  );
5974
6108
 
5975
6109
  this.mediaProperties.webrtcMediaConnection.on(
5976
- Event.AUDIO_SOURCES_COUNT_CHANGED,
6110
+ MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED,
5977
6111
  (numTotalSources, numLiveSources, mediaContent) => {
5978
6112
  Trigger.trigger(
5979
6113
  this,
@@ -5990,6 +6124,45 @@ export default class Meeting extends StatelessWebexPlugin {
5990
6124
  );
5991
6125
  }
5992
6126
  );
6127
+
6128
+ this.iceCandidateErrors.clear();
6129
+ this.mediaProperties.webrtcMediaConnection.on(
6130
+ MediaConnectionEventNames.ICE_CANDIDATE_ERROR,
6131
+ (event) => {
6132
+ const {errorCode} = event.error;
6133
+ let {errorText} = event.error;
6134
+
6135
+ if (
6136
+ errorCode === 600 &&
6137
+ errorText === 'Address not associated with the desired network interface.'
6138
+ ) {
6139
+ return;
6140
+ }
6141
+
6142
+ if (errorText.endsWith('.')) {
6143
+ errorText = errorText.slice(0, -1);
6144
+ }
6145
+
6146
+ errorText = errorText.toLowerCase();
6147
+ errorText = errorText.replace(/ /g, '_');
6148
+
6149
+ const error = `${errorCode}_${errorText}`;
6150
+
6151
+ const count = this.iceCandidateErrors.get(error) || 0;
6152
+
6153
+ this.iceCandidateErrors.set(error, count + 1);
6154
+ }
6155
+ );
6156
+
6157
+ this.iceCandidatesCount = 0;
6158
+ this.mediaProperties.webrtcMediaConnection.on(
6159
+ MediaConnectionEventNames.ICE_CANDIDATE,
6160
+ (event) => {
6161
+ if (event.candidate) {
6162
+ this.iceCandidatesCount += 1;
6163
+ }
6164
+ }
6165
+ );
5993
6166
  };
5994
6167
 
5995
6168
  /**
@@ -5999,7 +6172,7 @@ export default class Meeting extends StatelessWebexPlugin {
5999
6172
  * @memberof Meetings
6000
6173
  */
6001
6174
  setupStatsAnalyzerEventHandlers = () => {
6002
- this.statsAnalyzer.on(StatsAnalyzerEvents.MEDIA_QUALITY, (options) => {
6175
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.MEDIA_QUALITY, (options) => {
6003
6176
  // TODO: might have to send the same event to the developer
6004
6177
  // Add ip address info if geo hint is present
6005
6178
  // @ts-ignore fix type
@@ -6013,14 +6186,15 @@ export default class Meeting extends StatelessWebexPlugin {
6013
6186
  name: 'client.mediaquality.event',
6014
6187
  options: {
6015
6188
  meetingId: this.id,
6016
- networkType: options.networkType,
6189
+ networkType: options.data.networkType,
6017
6190
  },
6018
6191
  payload: {
6019
6192
  intervals: [options.data],
6020
6193
  },
6021
6194
  });
6022
6195
  });
6023
- this.statsAnalyzer.on(StatsAnalyzerEvents.LOCAL_MEDIA_STARTED, (data) => {
6196
+
6197
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED, (data) => {
6024
6198
  Trigger.trigger(
6025
6199
  this,
6026
6200
  {
@@ -6034,28 +6208,28 @@ export default class Meeting extends StatelessWebexPlugin {
6034
6208
  this.webex.internal.newMetrics.submitClientEvent({
6035
6209
  name: 'client.media.tx.start',
6036
6210
  payload: {
6037
- mediaType: data.type,
6038
- shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
6211
+ mediaType: data.mediaType,
6212
+ shareInstanceId: data.mediaType === 'share' ? this.localShareInstanceId : undefined,
6039
6213
  },
6040
6214
  options: {
6041
6215
  meetingId: this.id,
6042
6216
  },
6043
6217
  });
6044
6218
  });
6045
- this.statsAnalyzer.on(StatsAnalyzerEvents.LOCAL_MEDIA_STOPPED, (data) => {
6219
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED, (data) => {
6046
6220
  // @ts-ignore
6047
6221
  this.webex.internal.newMetrics.submitClientEvent({
6048
6222
  name: 'client.media.tx.stop',
6049
6223
  payload: {
6050
- mediaType: data.type,
6051
- shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
6224
+ mediaType: data.mediaType,
6225
+ shareInstanceId: data.mediaType === 'share' ? this.localShareInstanceId : undefined,
6052
6226
  },
6053
6227
  options: {
6054
6228
  meetingId: this.id,
6055
6229
  },
6056
6230
  });
6057
6231
  });
6058
- this.statsAnalyzer.on(StatsAnalyzerEvents.REMOTE_MEDIA_STARTED, (data) => {
6232
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED, (data) => {
6059
6233
  Trigger.trigger(
6060
6234
  this,
6061
6235
  {
@@ -6069,15 +6243,15 @@ export default class Meeting extends StatelessWebexPlugin {
6069
6243
  this.webex.internal.newMetrics.submitClientEvent({
6070
6244
  name: 'client.media.rx.start',
6071
6245
  payload: {
6072
- mediaType: data.type,
6073
- shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
6246
+ mediaType: data.mediaType,
6247
+ shareInstanceId: data.mediaType === 'share' ? this.remoteShareInstanceId : undefined,
6074
6248
  },
6075
6249
  options: {
6076
6250
  meetingId: this.id,
6077
6251
  },
6078
6252
  });
6079
6253
 
6080
- if (data.type === 'share') {
6254
+ if (data.mediaType === 'share') {
6081
6255
  // @ts-ignore
6082
6256
  this.webex.internal.newMetrics.submitClientEvent({
6083
6257
  name: 'client.media.render.start',
@@ -6091,20 +6265,20 @@ export default class Meeting extends StatelessWebexPlugin {
6091
6265
  });
6092
6266
  }
6093
6267
  });
6094
- this.statsAnalyzer.on(StatsAnalyzerEvents.REMOTE_MEDIA_STOPPED, (data) => {
6268
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED, (data) => {
6095
6269
  // @ts-ignore
6096
6270
  this.webex.internal.newMetrics.submitClientEvent({
6097
6271
  name: 'client.media.rx.stop',
6098
6272
  payload: {
6099
- mediaType: data.type,
6100
- shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
6273
+ mediaType: data.mediaType,
6274
+ shareInstanceId: data.mediaType === 'share' ? this.remoteShareInstanceId : undefined,
6101
6275
  },
6102
6276
  options: {
6103
6277
  meetingId: this.id,
6104
6278
  },
6105
6279
  });
6106
6280
 
6107
- if (data.type === 'share') {
6281
+ if (data.mediaType === 'share') {
6108
6282
  // @ts-ignore
6109
6283
  this.webex.internal.newMetrics.submitClientEvent({
6110
6284
  name: 'client.media.render.stop',
@@ -6264,6 +6438,8 @@ export default class Meeting extends StatelessWebexPlugin {
6264
6438
  try {
6265
6439
  await this.mediaProperties.waitForMediaConnectionConnected();
6266
6440
  } catch (error) {
6441
+ const {iceConnected} = error;
6442
+
6267
6443
  if (!this.hasMediaConnectionConnectedAtLeastOnce) {
6268
6444
  // Only send CA event for join flow if we haven't successfully connected media yet
6269
6445
  // @ts-ignore
@@ -6271,7 +6447,7 @@ export default class Meeting extends StatelessWebexPlugin {
6271
6447
  name: 'client.ice.end',
6272
6448
  payload: {
6273
6449
  canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
6274
- icePhase: this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY',
6450
+ icePhase: this.addMediaData.icePhaseCallback(),
6275
6451
  errors: [
6276
6452
  // @ts-ignore
6277
6453
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
@@ -6283,13 +6459,13 @@ export default class Meeting extends StatelessWebexPlugin {
6283
6459
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6284
6460
  ?.signalingState ||
6285
6461
  'unknown',
6286
- iceConnectionState:
6287
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6288
- ?.iceConnectionState ||
6289
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6290
- ?.iceConnectionState ||
6291
- 'unknown',
6462
+ iceConnected,
6292
6463
  turnServerUsed: this.turnServerUsed,
6464
+ unreachable:
6465
+ // @ts-ignore
6466
+ await this.webex.meetings.reachability
6467
+ .isWebexMediaBackendUnreachable()
6468
+ .catch(() => false),
6293
6469
  }),
6294
6470
  }
6295
6471
  ),
@@ -6317,15 +6493,15 @@ export default class Meeting extends StatelessWebexPlugin {
6317
6493
  if (this.config.stats.enableStatsAnalyzer) {
6318
6494
  // @ts-ignore - config coming from registerPlugin
6319
6495
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
6320
- this.statsAnalyzer = new StatsAnalyzer(
6496
+ this.statsAnalyzer = new StatsAnalyzer({
6321
6497
  // @ts-ignore - config coming from registerPlugin
6322
- this.config.stats,
6323
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
6324
- this.networkQualityMonitor
6325
- );
6498
+ config: this.config.stats,
6499
+ networkQualityMonitor: this.networkQualityMonitor,
6500
+ isMultistream: this.isMultistream,
6501
+ });
6326
6502
  this.setupStatsAnalyzerEventHandlers();
6327
6503
  this.networkQualityMonitor.on(
6328
- EVENT_TRIGGERS.NETWORK_QUALITY,
6504
+ NetworkQualityEventNames.NETWORK_QUALITY,
6329
6505
  this.sendNetworkQualityEvent.bind(this)
6330
6506
  );
6331
6507
  }
@@ -6374,6 +6550,21 @@ export default class Meeting extends StatelessWebexPlugin {
6374
6550
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
6375
6551
  } seconds`
6376
6552
  );
6553
+ // @ts-ignore
6554
+ this.webex.internal.newMetrics.submitClientEvent({
6555
+ name: 'client.media-engine.remote-sdp-received',
6556
+ payload: {
6557
+ canProceed: false,
6558
+ errors: [
6559
+ // @ts-ignore
6560
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode({
6561
+ clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6562
+ }),
6563
+ ],
6564
+ },
6565
+ options: {meetingId: this.id, rawError: new Error('Timeout waiting for SDP answer')},
6566
+ });
6567
+
6377
6568
  deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
6378
6569
  }, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
6379
6570
 
@@ -6422,7 +6613,7 @@ export default class Meeting extends StatelessWebexPlugin {
6422
6613
  remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6423
6614
  bundlePolicy?: BundlePolicy
6424
6615
  ): Promise<void> {
6425
- this.retriedWithTurnServer = true;
6616
+ this.addMediaData.retriedWithTurnServer = true;
6426
6617
  const LOG_HEADER = 'Meeting:index#addMedia():retryWithForcedTurnDiscovery -->';
6427
6618
 
6428
6619
  await this.cleanUpBeforeRetryWithTurnServer();
@@ -6517,7 +6708,7 @@ export default class Meeting extends StatelessWebexPlugin {
6517
6708
  correlation_id: this.correlationId,
6518
6709
  latency: cdl.getTurnDiscoveryTime(),
6519
6710
  turnServerUsed: this.turnServerUsed,
6520
- retriedWithTurnServer: this.retriedWithTurnServer,
6711
+ retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
6521
6712
  });
6522
6713
  }
6523
6714
 
@@ -6541,7 +6732,8 @@ export default class Meeting extends StatelessWebexPlugin {
6541
6732
  turnServerInfo?: TurnServerInfo
6542
6733
  ): Promise<void> {
6543
6734
  const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
6544
- const isReconnecting = this.isMoveToInProgress || this.retriedWithTurnServer;
6735
+ const isReconnecting =
6736
+ this.isMoveToInProgress || !!this.locusMediaRequest?.isConfluenceCreated();
6545
6737
 
6546
6738
  // We are forcing turn discovery if the case is moveTo and a turn server was used already
6547
6739
  if (this.isMoveToInProgress && this.turnServerUsed) {
@@ -6663,24 +6855,80 @@ export default class Meeting extends StatelessWebexPlugin {
6663
6855
  }
6664
6856
  }
6665
6857
 
6858
+ /**
6859
+ * Creates an instance of LocusMediaRequest for this meeting - it is needed for doing any calls
6860
+ * to Locus /media API (these are used for sending Roap messages and updating audio/video mute status).
6861
+ *
6862
+ * @returns {void}
6863
+ */
6864
+ private setupLocusMediaRequest() {
6865
+ this.locusMediaRequest = new LocusMediaRequest(
6866
+ {
6867
+ correlationId: this.correlationId,
6868
+ meetingId: this.id,
6869
+ device: {
6870
+ url: this.deviceUrl,
6871
+ // @ts-ignore
6872
+ deviceType: this.config.deviceType,
6873
+ // @ts-ignore
6874
+ countryCode: this.webex.meetings.geoHintInfo?.countryCode,
6875
+ // @ts-ignore
6876
+ regionCode: this.webex.meetings.geoHintInfo?.regionCode,
6877
+ },
6878
+ preferTranscoding: !this.isMultistream,
6879
+ },
6880
+ {
6881
+ // @ts-ignore
6882
+ parent: this.webex,
6883
+ }
6884
+ );
6885
+ }
6886
+
6666
6887
  /**
6667
6888
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
6668
6889
  *
6669
6890
  * @param {AddMediaOptions} options
6670
- * @param {TurnServerInfo} turnServerInfo - TURN server information (used only internally by the SDK)
6671
6891
  * @returns {Promise<void>}
6672
6892
  * @public
6673
6893
  * @memberof Meeting
6674
6894
  */
6675
- async addMedia(
6676
- options: AddMediaOptions = {},
6677
- turnServerInfo: TurnServerInfo = undefined
6895
+ addMedia(options: AddMediaOptions = {}): Promise<void> {
6896
+ return this.addMediaInternal(
6897
+ () => (this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY'),
6898
+ undefined,
6899
+ false,
6900
+ options
6901
+ );
6902
+ }
6903
+
6904
+ /**
6905
+ * Internal version of addMedia() with some more arguments for finer control of its behavior
6906
+ *
6907
+ * @param {Function} icePhaseCallback - callback to determine the icePhase for CA "client.ice.end" failure events
6908
+ * @param {TurnServerInfo} turnServerInfo - TURN server information
6909
+ * @param {boolean} forceTurnDiscovery - if true, TURN discovery will be done
6910
+ * @param {AddMediaOptions} options - same as options of the public addMedia() method
6911
+ * @returns {Promise<void>}
6912
+ * @protected
6913
+ * @memberof Meeting
6914
+ */
6915
+ protected async addMediaInternal(
6916
+ icePhaseCallback: () => string,
6917
+ turnServerInfo: TurnServerInfo,
6918
+ forceTurnDiscovery,
6919
+ options: AddMediaOptions = {}
6678
6920
  ): Promise<void> {
6679
- this.retriedWithTurnServer = false;
6921
+ this.addMediaData.retriedWithTurnServer = false;
6922
+ this.addMediaData.icePhaseCallback = icePhaseCallback;
6923
+
6680
6924
  this.hasMediaConnectionConnectedAtLeastOnce = false;
6681
6925
  const LOG_HEADER = 'Meeting:index#addMedia -->';
6682
6926
  LoggerProxy.logger.info(
6683
- `${LOG_HEADER} called with: ${JSON.stringify(options)}, ${JSON.stringify(turnServerInfo)}`
6927
+ `${LOG_HEADER} called with: options=${JSON.stringify(
6928
+ options
6929
+ )}, turnServerInfo=${JSON.stringify(
6930
+ turnServerInfo
6931
+ )}, forceTurnDiscovery=${forceTurnDiscovery}`
6684
6932
  );
6685
6933
 
6686
6934
  if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
@@ -6744,27 +6992,6 @@ export default class Meeting extends StatelessWebexPlugin {
6744
6992
  receiveShare: shareAudioEnabled || shareVideoEnabled,
6745
6993
  });
6746
6994
 
6747
- this.locusMediaRequest = new LocusMediaRequest(
6748
- {
6749
- correlationId: this.correlationId,
6750
- meetingId: this.id,
6751
- device: {
6752
- url: this.deviceUrl,
6753
- // @ts-ignore
6754
- deviceType: this.config.deviceType,
6755
- // @ts-ignore
6756
- countryCode: this.webex.meetings.geoHintInfo?.countryCode,
6757
- // @ts-ignore
6758
- regionCode: this.webex.meetings.geoHintInfo?.regionCode,
6759
- },
6760
- preferTranscoding: !this.isMultistream,
6761
- },
6762
- {
6763
- // @ts-ignore
6764
- parent: this.webex,
6765
- }
6766
- );
6767
-
6768
6995
  this.audio = createMuteState(AUDIO, this, audioEnabled);
6769
6996
  this.video = createMuteState(VIDEO, this, videoEnabled);
6770
6997
 
@@ -6778,7 +7005,7 @@ export default class Meeting extends StatelessWebexPlugin {
6778
7005
  await this.establishMediaConnection(
6779
7006
  remoteMediaManagerConfig,
6780
7007
  bundlePolicy,
6781
- false,
7008
+ forceTurnDiscovery,
6782
7009
  turnServerInfo
6783
7010
  );
6784
7011
 
@@ -6796,6 +7023,7 @@ export default class Meeting extends StatelessWebexPlugin {
6796
7023
  await this.mediaProperties.getCurrentConnectionInfo();
6797
7024
  // @ts-ignore
6798
7025
  const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
7026
+ const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
6799
7027
 
6800
7028
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
6801
7029
  correlation_id: this.correlationId,
@@ -6804,9 +7032,11 @@ export default class Meeting extends StatelessWebexPlugin {
6804
7032
  selectedCandidatePairChanges,
6805
7033
  numTransports,
6806
7034
  isMultistream: this.isMultistream,
6807
- retriedWithTurnServer: this.retriedWithTurnServer,
7035
+ retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
6808
7036
  isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
6809
7037
  ...reachabilityStats,
7038
+ ...iceCandidateErrors,
7039
+ iceCandidatesCount: this.iceCandidatesCount,
6810
7040
  });
6811
7041
  // @ts-ignore
6812
7042
  this.webex.internal.newMetrics.submitClientEvent({
@@ -6830,6 +7060,8 @@ export default class Meeting extends StatelessWebexPlugin {
6830
7060
  const {selectedCandidatePairChanges, numTransports} =
6831
7061
  await this.mediaProperties.getCurrentConnectionInfo();
6832
7062
 
7063
+ const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
7064
+
6833
7065
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
6834
7066
  correlation_id: this.correlationId,
6835
7067
  locus_id: this.locusUrl.split('/').pop(),
@@ -6840,7 +7072,7 @@ export default class Meeting extends StatelessWebexPlugin {
6840
7072
  numTransports,
6841
7073
  turnDiscoverySkippedReason: this.turnDiscoverySkippedReason,
6842
7074
  turnServerUsed: this.turnServerUsed,
6843
- retriedWithTurnServer: this.retriedWithTurnServer,
7075
+ retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
6844
7076
  isMultistream: this.isMultistream,
6845
7077
  isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
6846
7078
  signalingState:
@@ -6859,6 +7091,8 @@ export default class Meeting extends StatelessWebexPlugin {
6859
7091
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
6860
7092
  'unknown',
6861
7093
  ...reachabilityMetrics,
7094
+ ...iceCandidateErrors,
7095
+ iceCandidatesCount: this.iceCandidatesCount,
6862
7096
  });
6863
7097
 
6864
7098
  await this.cleanUpOnAddMediaFailure();
@@ -6879,6 +7113,8 @@ export default class Meeting extends StatelessWebexPlugin {
6879
7113
  }
6880
7114
 
6881
7115
  throw error;
7116
+ } finally {
7117
+ this.addMediaData.icePhaseCallback = DEFAULT_ICE_PHASE_CALLBACK;
6882
7118
  }
6883
7119
  }
6884
7120
 
@@ -7896,6 +8132,7 @@ export default class Meeting extends StatelessWebexPlugin {
7896
8132
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
7897
8133
  correlationId: this.correlationId,
7898
8134
  muted,
8135
+ encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
7899
8136
  });
7900
8137
  };
7901
8138
 
@@ -7957,7 +8194,7 @@ export default class Meeting extends StatelessWebexPlugin {
7957
8194
  * @private
7958
8195
  * @memberof Meeting
7959
8196
  */
7960
- private sendNetworkQualityEvent(res: any) {
8197
+ private sendNetworkQualityEvent(res: {networkQualityScore: number; mediaType: string}) {
7961
8198
  Trigger.trigger(
7962
8199
  this,
7963
8200
  {