@webex/plugin-meetings 3.3.1 → 3.4.0

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