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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +7 -2
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/constants.js +11 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/locus-info/selfUtils.js +0 -5
  9. package/dist/locus-info/selfUtils.js.map +1 -1
  10. package/dist/media/MediaConnectionAwaiter.js +70 -15
  11. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  12. package/dist/media/index.js +18 -9
  13. package/dist/media/index.js.map +1 -1
  14. package/dist/meeting/connectionStateHandler.js +67 -0
  15. package/dist/meeting/connectionStateHandler.js.map +1 -0
  16. package/dist/meeting/index.js +576 -374
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/locusMediaRequest.js +7 -0
  19. package/dist/meeting/locusMediaRequest.js.map +1 -1
  20. package/dist/meeting/muteState.js +6 -1
  21. package/dist/meeting/muteState.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meeting-info/index.js +4 -4
  25. package/dist/meeting-info/index.js.map +1 -1
  26. package/dist/meeting-info/meeting-info-v2.js +2 -2
  27. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  28. package/dist/meeting-info/util.js +17 -17
  29. package/dist/meeting-info/util.js.map +1 -1
  30. package/dist/meeting-info/utilv2.js +16 -16
  31. package/dist/meeting-info/utilv2.js.map +1 -1
  32. package/dist/meetings/collection.js +1 -1
  33. package/dist/meetings/collection.js.map +1 -1
  34. package/dist/meetings/index.js +41 -35
  35. package/dist/meetings/index.js.map +1 -1
  36. package/dist/meetings/meetings.types.js +8 -0
  37. package/dist/meetings/meetings.types.js.map +1 -1
  38. package/dist/meetings/util.js +3 -2
  39. package/dist/meetings/util.js.map +1 -1
  40. package/dist/metrics/constants.js +2 -1
  41. package/dist/metrics/constants.js.map +1 -1
  42. package/dist/metrics/index.js +57 -0
  43. package/dist/metrics/index.js.map +1 -1
  44. package/dist/personal-meeting-room/index.js +1 -1
  45. package/dist/personal-meeting-room/index.js.map +1 -1
  46. package/dist/reachability/clusterReachability.js +108 -53
  47. package/dist/reachability/clusterReachability.js.map +1 -1
  48. package/dist/reachability/index.js +546 -115
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/reconnection-manager/index.js +1 -1
  51. package/dist/reconnection-manager/index.js.map +1 -1
  52. package/dist/rtcMetrics/index.js +26 -6
  53. package/dist/rtcMetrics/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +11 -3
  55. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  56. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  57. package/dist/types/meeting/index.d.ts +28 -8
  58. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  59. package/dist/types/meeting-info/index.d.ts +3 -2
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  61. package/dist/types/meeting-info/util.d.ts +5 -4
  62. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  63. package/dist/types/meetings/collection.d.ts +3 -2
  64. package/dist/types/meetings/index.d.ts +6 -4
  65. package/dist/types/meetings/meetings.types.d.ts +9 -0
  66. package/dist/types/metrics/constants.d.ts +1 -0
  67. package/dist/types/metrics/index.d.ts +15 -0
  68. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  69. package/dist/types/reachability/index.d.ts +107 -4
  70. package/dist/types/rtcMetrics/index.d.ts +11 -1
  71. package/dist/webinar/index.js +1 -1
  72. package/package.json +23 -23
  73. package/src/breakouts/index.ts +7 -1
  74. package/src/constants.ts +13 -17
  75. package/src/locus-info/selfUtils.ts +0 -5
  76. package/src/media/MediaConnectionAwaiter.ts +89 -14
  77. package/src/media/index.ts +18 -9
  78. package/src/meeting/connectionStateHandler.ts +65 -0
  79. package/src/meeting/index.ts +541 -298
  80. package/src/meeting/locusMediaRequest.ts +5 -0
  81. package/src/meeting/muteState.ts +6 -1
  82. package/src/meeting/util.ts +1 -0
  83. package/src/meeting-info/index.ts +9 -6
  84. package/src/meeting-info/meeting-info-v2.ts +4 -4
  85. package/src/meeting-info/util.ts +23 -28
  86. package/src/meeting-info/utilv2.ts +18 -24
  87. package/src/meetings/collection.ts +3 -3
  88. package/src/meetings/index.ts +43 -43
  89. package/src/meetings/meetings.types.ts +11 -0
  90. package/src/meetings/util.ts +5 -4
  91. package/src/metrics/constants.ts +1 -0
  92. package/src/metrics/index.ts +44 -0
  93. package/src/personal-meeting-room/index.ts +2 -2
  94. package/src/reachability/clusterReachability.ts +86 -25
  95. package/src/reachability/index.ts +364 -30
  96. package/src/reconnection-manager/index.ts +1 -1
  97. package/src/rtcMetrics/index.ts +25 -5
  98. package/test/unit/spec/breakouts/index.ts +51 -32
  99. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  100. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  101. package/test/unit/spec/media/index.ts +75 -34
  102. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  103. package/test/unit/spec/meeting/index.js +807 -185
  104. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  105. package/test/unit/spec/meeting/muteState.js +24 -0
  106. package/test/unit/spec/meeting-info/index.js +4 -4
  107. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  108. package/test/unit/spec/meeting-info/request.js +2 -2
  109. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  110. package/test/unit/spec/meetings/index.js +44 -3
  111. package/test/unit/spec/metrics/index.js +126 -0
  112. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  113. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  114. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  115. package/test/unit/spec/reachability/index.ts +1398 -131
  116. package/test/unit/spec/rtcMetrics/index.ts +32 -0
  117. package/dist/mediaQualityMetrics/config.js +0 -321
  118. package/dist/mediaQualityMetrics/config.js.map +0 -1
  119. package/dist/networkQualityMonitor/index.js +0 -227
  120. package/dist/networkQualityMonitor/index.js.map +0 -1
  121. package/dist/statsAnalyzer/global.js +0 -44
  122. package/dist/statsAnalyzer/global.js.map +0 -1
  123. package/dist/statsAnalyzer/index.js +0 -1072
  124. package/dist/statsAnalyzer/index.js.map +0 -1
  125. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  126. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  127. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  128. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  129. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  130. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  131. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  132. package/src/mediaQualityMetrics/config.ts +0 -255
  133. package/src/networkQualityMonitor/index.ts +0 -211
  134. package/src/statsAnalyzer/global.ts +0 -37
  135. package/src/statsAnalyzer/index.ts +0 -1318
  136. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  137. package/test/unit/spec/networkQualityMonitor/index.js +0 -99
  138. package/test/unit/spec/stats-analyzer/index.js +0 -1819
@@ -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,11 @@ 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
+ import RtcMetrics from '../rtcMetrics';
159
+
160
+ // default callback so we don't call an undefined function, but in practice it should never be used
161
+ const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
156
162
 
157
163
  const logRequest = (request: any, {logText = ''}) => {
158
164
  LoggerProxy.logger.info(`${logText} - sending request`);
@@ -526,7 +532,7 @@ export default class Meeting extends StatelessWebexPlugin {
526
532
  conversationUrl: string;
527
533
  callStateForMetrics: CallStateForMetrics;
528
534
  destination: string;
529
- destinationType: string;
535
+ destinationType: DESTINATION_TYPE;
530
536
  deviceUrl: string;
531
537
  hostId: string;
532
538
  id: string;
@@ -644,6 +650,10 @@ export default class Meeting extends StatelessWebexPlugin {
644
650
  })}event#${EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION}`
645
651
  );
646
652
 
653
+ if (this.getCurUserType() !== 'host') {
654
+ delete payload.spokenLanguages;
655
+ }
656
+
647
657
  // @ts-ignore
648
658
  this.trigger(EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION, payload);
649
659
  },
@@ -674,12 +684,20 @@ export default class Meeting extends StatelessWebexPlugin {
674
684
  },
675
685
  };
676
686
 
677
- private retriedWithTurnServer: boolean;
687
+ private addMediaData: {
688
+ retriedWithTurnServer: boolean;
689
+ icePhaseCallback: () => string;
690
+ };
691
+
678
692
  private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
679
693
  private deferSDPAnswer?: Defer; // used for waiting for a response
680
694
  private sdpResponseTimer?: ReturnType<typeof setTimeout>;
681
695
  private hasMediaConnectionConnectedAtLeastOnce: boolean;
682
696
  private joinWithMediaRetryInfo?: {isRetry: boolean; prevJoinResponse?: any};
697
+ private connectionStateHandler?: ConnectionStateHandler;
698
+ private iceCandidateErrors: Map<string, number>;
699
+ private iceCandidatesCount: number;
700
+ private rtcMetrics?: RtcMetrics;
683
701
 
684
702
  /**
685
703
  * @param {Object} attrs
@@ -1445,13 +1463,19 @@ export default class Meeting extends StatelessWebexPlugin {
1445
1463
  this.turnServerUsed = false;
1446
1464
 
1447
1465
  /**
1448
- * Whether retry was done using TURN Discovery.
1466
+ * Contains information used during the addMedia() operation:
1467
+ * retriedWithTurnServer - whether retry was done using TURN Discovery
1468
+ * icePhaseCallback - callback for determining the value for icePhase when sending failure event to CA
1469
+ *
1449
1470
  * @instance
1450
- * @type {boolean}
1471
+ * @type {Object}
1451
1472
  * @private
1452
1473
  * @memberof Meeting
1453
1474
  */
1454
- this.retriedWithTurnServer = false;
1475
+ this.addMediaData = {
1476
+ retriedWithTurnServer: false,
1477
+ icePhaseCallback: DEFAULT_ICE_PHASE_CALLBACK,
1478
+ };
1455
1479
 
1456
1480
  /**
1457
1481
  * Whether or not the media connection has ever successfully connected.
@@ -1470,6 +1494,33 @@ export default class Meeting extends StatelessWebexPlugin {
1470
1494
  * @memberof Meeting
1471
1495
  */
1472
1496
  this.joinWithMediaRetryInfo = {isRetry: false, prevJoinResponse: undefined};
1497
+
1498
+ /**
1499
+ * Connection state handler
1500
+ * @instance
1501
+ * @type {ConnectionStateHandler}
1502
+ * @private
1503
+ * @memberof Meeting
1504
+ */
1505
+ this.connectionStateHandler = undefined;
1506
+
1507
+ /**
1508
+ * ICE Candidates errors map
1509
+ * @instance
1510
+ * @type {Map<[number, string], number>}
1511
+ * @private
1512
+ * @memberof Meeting
1513
+ */
1514
+ this.iceCandidateErrors = new Map();
1515
+
1516
+ /**
1517
+ * Gathered ICE Candidates count
1518
+ * @instance
1519
+ * @type {number}
1520
+ * @private
1521
+ * @memberof Meeting
1522
+ */
1523
+ this.iceCandidatesCount = 0;
1473
1524
  }
1474
1525
 
1475
1526
  /**
@@ -1718,7 +1769,7 @@ export default class Meeting extends StatelessWebexPlugin {
1718
1769
  }
1719
1770
 
1720
1771
  const isStartingSpaceInstantV2Meeting =
1721
- this.destinationType === _CONVERSATION_URL_ &&
1772
+ this.destinationType === DESTINATION_TYPE.CONVERSATION_URL &&
1722
1773
  // @ts-ignore - config coming from registerPlugin
1723
1774
  this.config.experimental.enableAdhocMeetings &&
1724
1775
  // @ts-ignore
@@ -1727,7 +1778,9 @@ export default class Meeting extends StatelessWebexPlugin {
1727
1778
  const destination = isStartingSpaceInstantV2Meeting
1728
1779
  ? this.meetingInfo.meetingJoinUrl
1729
1780
  : this.destination;
1730
- const destinationType = isStartingSpaceInstantV2Meeting ? _MEETING_LINK_ : this.destinationType;
1781
+ const destinationType = isStartingSpaceInstantV2Meeting
1782
+ ? DESTINATION_TYPE.MEETING_LINK
1783
+ : this.destinationType;
1731
1784
 
1732
1785
  const permissionTokenExpiryInfo = this.getPermissionTokenExpiryInfo();
1733
1786
 
@@ -3105,6 +3158,7 @@ export default class Meeting extends StatelessWebexPlugin {
3105
3158
  options: {meetingId: this.id},
3106
3159
  });
3107
3160
  }
3161
+ this.rtcMetrics?.sendNextMetrics();
3108
3162
  this.updateLLMConnection();
3109
3163
  });
3110
3164
 
@@ -3114,6 +3168,9 @@ export default class Meeting extends StatelessWebexPlugin {
3114
3168
  correlation_id: this.correlationId,
3115
3169
  locus_id: this.locusId,
3116
3170
  });
3171
+ LoggerProxy.logger.info(
3172
+ 'Meeting:index#setUpLocusInfoSelfListener --> MEDIA_INACTIVITY received, reconnecting...'
3173
+ );
3117
3174
  this.reconnect();
3118
3175
  });
3119
3176
 
@@ -4242,8 +4299,6 @@ export default class Meeting extends StatelessWebexPlugin {
4242
4299
  * @memberof Meeting
4243
4300
  */
4244
4301
  public closePeerConnections() {
4245
- this.locusMediaRequest = undefined;
4246
-
4247
4302
  if (this.mediaProperties.webrtcMediaConnection) {
4248
4303
  if (this.remoteMediaManager) {
4249
4304
  this.remoteMediaManager.stop();
@@ -4556,38 +4611,57 @@ export default class Meeting extends StatelessWebexPlugin {
4556
4611
  try {
4557
4612
  let turnServerInfo;
4558
4613
  let turnDiscoverySkippedReason;
4614
+ let forceTurnDiscovery = false;
4559
4615
 
4560
- // @ts-ignore
4561
- joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4562
- const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(this, true);
4616
+ if (!joinResponse) {
4617
+ // This is the 1st attempt or a retry after join request failed -> we need to do a join with TURN discovery
4563
4618
 
4564
- ({turnDiscoverySkippedReason} = turnDiscoveryRequest);
4565
- joinOptions.roapMessage = turnDiscoveryRequest.roapMessage;
4619
+ // @ts-ignore
4620
+ joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4621
+ const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(
4622
+ this,
4623
+ true
4624
+ );
4625
+
4626
+ ({turnDiscoverySkippedReason} = turnDiscoveryRequest);
4627
+ joinOptions.roapMessage = turnDiscoveryRequest.roapMessage;
4566
4628
 
4567
- if (!joinResponse) {
4568
4629
  LoggerProxy.logger.info(
4569
4630
  'Meeting:index#joinWithMedia ---> calling join with joinOptions, ',
4570
4631
  joinOptions
4571
4632
  );
4572
4633
 
4573
4634
  joinResponse = await this.join(joinOptions);
4574
- }
4575
4635
 
4576
- joined = true;
4636
+ joined = true;
4577
4637
 
4578
- if (joinOptions.roapMessage) {
4579
- ({turnServerInfo, turnDiscoverySkippedReason} =
4580
- await this.roap.handleTurnDiscoveryHttpResponse(this, joinResponse));
4638
+ // if we sent out TURN discovery Roap message with join, process the TURN discovery response
4639
+ if (joinOptions.roapMessage) {
4640
+ ({turnServerInfo, turnDiscoverySkippedReason} =
4641
+ await this.roap.handleTurnDiscoveryHttpResponse(this, joinResponse));
4581
4642
 
4582
- this.turnDiscoverySkippedReason = turnDiscoverySkippedReason;
4583
- this.turnServerUsed = !!turnServerInfo;
4643
+ this.turnDiscoverySkippedReason = turnDiscoverySkippedReason;
4644
+ this.turnServerUsed = !!turnServerInfo;
4584
4645
 
4585
- if (turnServerInfo === undefined) {
4586
- this.roap.abortTurnDiscovery();
4646
+ if (turnServerInfo === undefined) {
4647
+ this.roap.abortTurnDiscovery();
4648
+ }
4587
4649
  }
4650
+ } else {
4651
+ // This is a retry, when join succeeded but addMedia failed, so we'll just call addMedia() again,
4652
+ // but we need to ensure that it also does a new TURN discovery
4653
+ forceTurnDiscovery = true;
4654
+ joined = true;
4588
4655
  }
4589
4656
 
4590
- const mediaResponse = await this.addMedia(mediaOptions, turnServerInfo);
4657
+ const mediaResponse = await this.addMediaInternal(
4658
+ () => {
4659
+ return this.joinWithMediaRetryInfo.isRetry ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY';
4660
+ },
4661
+ turnServerInfo,
4662
+ forceTurnDiscovery,
4663
+ mediaOptions
4664
+ );
4591
4665
 
4592
4666
  this.joinWithMediaRetryInfo = {isRetry: false, prevJoinResponse: undefined};
4593
4667
 
@@ -4626,7 +4700,16 @@ export default class Meeting extends StatelessWebexPlugin {
4626
4700
  }
4627
4701
  );
4628
4702
 
4629
- if (!isRetry) {
4703
+ // if this was the first attempt, let's do a retry
4704
+ let shouldRetry = !isRetry;
4705
+
4706
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
4707
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
4708
+ // so there is no point doing a retry
4709
+ shouldRetry = false;
4710
+ }
4711
+
4712
+ if (shouldRetry) {
4630
4713
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4631
4714
  this.joinWithMediaRetryInfo.isRetry = true;
4632
4715
  this.joinWithMediaRetryInfo.prevJoinResponse = joinResponse;
@@ -4786,6 +4869,14 @@ export default class Meeting extends StatelessWebexPlugin {
4786
4869
  reject(new Error('Webex Assistant is not enabled/supported'));
4787
4870
  }
4788
4871
 
4872
+ if (this.getCurUserType() !== 'host') {
4873
+ LoggerProxy.logger.error(
4874
+ 'Meeting:index#setSpokenLanguage --> Only host can set spoken language'
4875
+ );
4876
+
4877
+ reject(new Error('Only host can set spoken language'));
4878
+ }
4879
+
4789
4880
  try {
4790
4881
  const voiceaListenerLanguageUpdate = (payload) => {
4791
4882
  // @ts-ignore
@@ -4839,10 +4930,8 @@ export default class Meeting extends StatelessWebexPlugin {
4839
4930
  this.setUpVoiceaListeners();
4840
4931
  }
4841
4932
 
4842
- if (this.getCurUserType() === 'host') {
4843
- // @ts-ignore
4844
- await this.webex.internal.voicea.turnOnCaptions(options?.spokenLanguage);
4845
- }
4933
+ // @ts-ignore
4934
+ await this.webex.internal.voicea.turnOnCaptions(options?.spokenLanguage);
4846
4935
  } catch (error) {
4847
4936
  LoggerProxy.logger.error(`Meeting:index#startTranscription --> ${error}`);
4848
4937
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
@@ -5141,6 +5230,8 @@ export default class Meeting extends StatelessWebexPlugin {
5141
5230
  return MeetingUtil.joinMeetingOptions(this, options)
5142
5231
  .then((join) => {
5143
5232
  this.meetingFiniteStateMachine.join();
5233
+ this.setupLocusMediaRequest();
5234
+
5144
5235
  LoggerProxy.logger.log('Meeting:index#join --> Success');
5145
5236
 
5146
5237
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
@@ -5233,8 +5324,13 @@ export default class Meeting extends StatelessWebexPlugin {
5233
5324
 
5234
5325
  // @ts-ignore - Fix type
5235
5326
  if (this.webex.internal.llm.isConnected()) {
5236
- // @ts-ignore - Fix type
5237
- if (url === this.webex.internal.llm.getLocusUrl() && isJoined) {
5327
+ if (
5328
+ // @ts-ignore - Fix type
5329
+ url === this.webex.internal.llm.getLocusUrl() &&
5330
+ // @ts-ignore - Fix type
5331
+ datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5332
+ isJoined
5333
+ ) {
5238
5334
  return undefined;
5239
5335
  }
5240
5336
  // @ts-ignore - Fix type
@@ -5634,219 +5730,258 @@ export default class Meeting extends StatelessWebexPlugin {
5634
5730
  * @returns {undefined}
5635
5731
  */
5636
5732
  setupSdpListeners = () => {
5637
- this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_SDP_ANSWER_PROCESSED, () => {
5638
- // @ts-ignore
5639
- const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5733
+ this.mediaProperties.webrtcMediaConnection.on(
5734
+ MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED,
5735
+ () => {
5736
+ // @ts-ignore
5737
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5640
5738
 
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
- });
5739
+ // @ts-ignore
5740
+ this.webex.internal.newMetrics.submitClientEvent({
5741
+ name: 'client.media-engine.remote-sdp-received',
5742
+ options: {meetingId: this.id},
5743
+ });
5744
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, {
5745
+ correlation_id: this.correlationId,
5746
+ latency: cdl.getLocalSDPGenRemoteSDPRecv(),
5747
+ meetingId: this.id,
5748
+ });
5651
5749
 
5652
- if (this.deferSDPAnswer) {
5653
- this.deferSDPAnswer.resolve();
5654
- clearTimeout(this.sdpResponseTimer);
5655
- this.sdpResponseTimer = undefined;
5750
+ if (this.deferSDPAnswer) {
5751
+ this.deferSDPAnswer.resolve();
5752
+ clearTimeout(this.sdpResponseTimer);
5753
+ this.sdpResponseTimer = undefined;
5754
+ }
5656
5755
  }
5657
- });
5756
+ );
5658
5757
 
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
- });
5758
+ this.mediaProperties.webrtcMediaConnection.on(
5759
+ MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED,
5760
+ () => {
5761
+ // @ts-ignore
5762
+ this.webex.internal.newMetrics.submitClientEvent({
5763
+ name: 'client.media-engine.local-sdp-generated',
5764
+ options: {meetingId: this.id},
5765
+ });
5665
5766
 
5666
- // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5667
- this.deferSDPAnswer = new Defer();
5668
- });
5767
+ // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5768
+ this.deferSDPAnswer = new Defer();
5769
+ }
5770
+ );
5669
5771
 
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
- });
5772
+ this.mediaProperties.webrtcMediaConnection.on(
5773
+ MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED,
5774
+ () => {
5775
+ // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer
5776
+ // @ts-ignore
5777
+ this.webex.internal.newMetrics.submitClientEvent({
5778
+ name: 'client.media-engine.remote-sdp-received',
5779
+ options: {meetingId: this.id},
5780
+ });
5781
+ }
5782
+ );
5678
5783
  };
5679
5784
 
5680
5785
  setupMediaConnectionListeners = () => {
5681
5786
  this.setupSdpListeners();
5682
5787
 
5683
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
5788
+ this.mediaProperties.webrtcMediaConnection.on(MediaConnectionEventNames.ROAP_STARTED, () => {
5684
5789
  this.isRoapInProgress = true;
5685
5790
  });
5686
5791
 
5687
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
5792
+ this.mediaProperties.webrtcMediaConnection.on(MediaConnectionEventNames.ROAP_DONE, () => {
5688
5793
  this.mediaNegotiatedEvent();
5689
5794
  this.isRoapInProgress = false;
5690
5795
  this.processNextQueuedMediaUpdate();
5691
5796
  });
5692
5797
 
5693
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
5798
+ this.mediaProperties.webrtcMediaConnection.on(
5799
+ MediaConnectionEventNames.ROAP_FAILURE,
5800
+ this.handleRoapFailure
5801
+ );
5694
5802
 
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}`;
5803
+ this.mediaProperties.webrtcMediaConnection.on(
5804
+ MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND,
5805
+ (event) => {
5806
+ const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
5807
+
5808
+ switch (event.roapMessage.messageType) {
5809
+ case 'OK':
5810
+ logRequest(
5811
+ this.roap.sendRoapOK({
5812
+ seq: event.roapMessage.seq,
5813
+ mediaId: this.mediaId,
5814
+ correlationId: this.correlationId,
5815
+ }),
5816
+ {
5817
+ logText: `${LOG_HEADER} Roap OK`,
5818
+ }
5819
+ );
5820
+ break;
5697
5821
 
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;
5822
+ case 'OFFER':
5823
+ logRequest(
5824
+ this.roap
5825
+ .sendRoapMediaRequest({
5826
+ sdp: event.roapMessage.sdp,
5827
+ seq: event.roapMessage.seq,
5828
+ tieBreaker: event.roapMessage.tieBreaker,
5829
+ meeting: this, // or can pass meeting ID
5830
+ })
5831
+ .then(({roapAnswer}) => {
5832
+ if (roapAnswer) {
5833
+ LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`);
5834
+
5835
+ this.roapMessageReceived(roapAnswer);
5836
+ }
5837
+ }),
5838
+ {
5839
+ logText: `${LOG_HEADER} Roap Offer`,
5840
+ }
5841
+ ).catch((error) => {
5842
+ // @ts-ignore
5843
+ this.webex.internal.newMetrics.submitClientEvent({
5844
+ name: 'client.media-engine.remote-sdp-received',
5845
+ payload: {
5846
+ canProceed: false,
5847
+ errors: [
5848
+ // @ts-ignore
5849
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5850
+ {
5851
+ clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
5852
+ }
5853
+ ),
5854
+ ],
5855
+ },
5856
+ options: {meetingId: this.id, rawError: error},
5857
+ });
5858
+
5859
+ this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
5860
+ clearTimeout(this.sdpResponseTimer);
5861
+ this.sdpResponseTimer = undefined;
5862
+ });
5863
+ break;
5711
5864
 
5712
- case 'OFFER':
5713
- logRequest(
5714
- this.roap
5715
- .sendRoapMediaRequest({
5865
+ case 'ANSWER':
5866
+ logRequest(
5867
+ this.roap.sendRoapAnswer({
5716
5868
  sdp: event.roapMessage.sdp,
5717
5869
  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
- }
5870
+ mediaId: this.mediaId,
5871
+ correlationId: this.correlationId,
5727
5872
  }),
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;
5873
+ {
5874
+ logText: `${LOG_HEADER} Roap Answer`,
5875
+ }
5876
+ ).catch((error) => {
5877
+ const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
5878
+ const data = {
5879
+ correlation_id: this.correlationId,
5880
+ locus_id: this.locusUrl.split('/').pop(),
5881
+ reason: error.message,
5882
+ stack: error.stack,
5883
+ };
5884
+ const metadata = {
5885
+ type: error.name,
5886
+ };
5764
5887
 
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,
5888
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
5774
5889
  });
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})`,
5890
+ break;
5891
+
5892
+ case 'ERROR':
5893
+ if (
5894
+ event.roapMessage.errorType === ErrorType.CONFLICT ||
5895
+ event.roapMessage.errorType === ErrorType.DOUBLECONFLICT
5896
+ ) {
5897
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
5898
+ correlation_id: this.correlationId,
5899
+ locus_id: this.locusUrl.split('/').pop(),
5900
+ sequence: event.roapMessage.seq,
5901
+ });
5785
5902
  }
5786
- );
5787
- break;
5903
+ logRequest(
5904
+ this.roap.sendRoapError({
5905
+ seq: event.roapMessage.seq,
5906
+ errorType: event.roapMessage.errorType,
5907
+ mediaId: this.mediaId,
5908
+ correlationId: this.correlationId,
5909
+ }),
5910
+ {
5911
+ logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`,
5912
+ }
5913
+ );
5914
+ break;
5788
5915
 
5789
- default:
5790
- LoggerProxy.logger.error(
5791
- `${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}`
5792
- );
5793
- break;
5916
+ default:
5917
+ LoggerProxy.logger.error(
5918
+ `${LOG_HEADER} Unsupported message type: ${event.roapMessage.messageType}`
5919
+ );
5920
+ break;
5921
+ }
5794
5922
  }
5795
- });
5923
+ );
5796
5924
 
5797
5925
  // 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]));
5926
+ this.mediaProperties.webrtcMediaConnection.on(
5927
+ MediaConnectionEventNames.REMOTE_TRACK_ADDED,
5928
+ (event) => {
5929
+ LoggerProxy.logger.log(
5930
+ `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(
5931
+ event
5932
+ )}`
5933
+ );
5808
5934
 
5809
- // eslint-disable-next-line @typescript-eslint/no-shadow
5810
- let eventType;
5935
+ if (event.track) {
5936
+ const mediaTrack = event.track;
5937
+ const remoteStream = new RemoteStream(MediaUtil.createMediaStream([mediaTrack]));
5938
+
5939
+ // eslint-disable-next-line @typescript-eslint/no-shadow
5940
+ let eventType;
5941
+
5942
+ switch (event.type) {
5943
+ case RemoteTrackType.AUDIO:
5944
+ eventType = EVENT_TYPES.REMOTE_AUDIO;
5945
+ this.mediaProperties.setRemoteAudioStream(remoteStream);
5946
+ break;
5947
+ case RemoteTrackType.VIDEO:
5948
+ eventType = EVENT_TYPES.REMOTE_VIDEO;
5949
+ this.mediaProperties.setRemoteVideoStream(remoteStream);
5950
+ break;
5951
+ case RemoteTrackType.SCREENSHARE_VIDEO:
5952
+ eventType = EVENT_TYPES.REMOTE_SHARE;
5953
+ this.mediaProperties.setRemoteShareStream(remoteStream);
5954
+ break;
5955
+ default: {
5956
+ LoggerProxy.logger.log(
5957
+ 'Meeting:index#setupMediaConnectionListeners --> unexpected track'
5958
+ );
5959
+ }
5960
+ }
5811
5961
 
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'
5962
+ if (eventType && mediaTrack) {
5963
+ Trigger.trigger(
5964
+ this,
5965
+ {
5966
+ file: 'meeting/index',
5967
+ function: 'setupRemoteTrackListener:MediaConnectionEventNames.REMOTE_TRACK_ADDED',
5968
+ },
5969
+ EVENT_TRIGGERS.MEDIA_READY,
5970
+ {
5971
+ type: eventType,
5972
+ stream: remoteStream.outputStream,
5973
+ }
5828
5974
  );
5829
5975
  }
5830
5976
  }
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
5977
  }
5847
- });
5978
+ );
5848
5979
 
5849
- this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5980
+ this.connectionStateHandler = new ConnectionStateHandler(
5981
+ this.mediaProperties.webrtcMediaConnection
5982
+ );
5983
+
5984
+ this.connectionStateHandler.on(ConnectionStateEvent.stateChanged, (event) => {
5850
5985
  const connectionFailed = () => {
5851
5986
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5852
5987
  correlation_id: this.correlationId,
@@ -5875,7 +6010,6 @@ export default class Meeting extends StatelessWebexPlugin {
5875
6010
 
5876
6011
  // @ts-ignore
5877
6012
  const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5878
-
5879
6013
  switch (event.state) {
5880
6014
  case ConnectionState.Connecting:
5881
6015
  if (!this.hasMediaConnectionConnectedAtLeastOnce) {
@@ -5932,25 +6066,28 @@ export default class Meeting extends StatelessWebexPlugin {
5932
6066
  }
5933
6067
  });
5934
6068
 
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
- });
6069
+ this.mediaProperties.webrtcMediaConnection.on(
6070
+ MediaConnectionEventNames.ACTIVE_SPEAKERS_CHANGED,
6071
+ (csis) => {
6072
+ Trigger.trigger(
6073
+ this,
6074
+ {
6075
+ file: 'meeting/index',
6076
+ function: 'setupMediaConnectionListeners',
6077
+ },
6078
+ EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED,
6079
+ {
6080
+ memberIds: csis
6081
+ // @ts-ignore
6082
+ .map((csi) => this.members.findMemberByCsi(csi)?.id)
6083
+ .filter((item) => item !== undefined),
6084
+ }
6085
+ );
6086
+ }
6087
+ );
5951
6088
 
5952
6089
  this.mediaProperties.webrtcMediaConnection.on(
5953
- Event.VIDEO_SOURCES_COUNT_CHANGED,
6090
+ MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED,
5954
6091
  (numTotalSources, numLiveSources, mediaContent) => {
5955
6092
  Trigger.trigger(
5956
6093
  this,
@@ -5973,7 +6110,7 @@ export default class Meeting extends StatelessWebexPlugin {
5973
6110
  );
5974
6111
 
5975
6112
  this.mediaProperties.webrtcMediaConnection.on(
5976
- Event.AUDIO_SOURCES_COUNT_CHANGED,
6113
+ MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED,
5977
6114
  (numTotalSources, numLiveSources, mediaContent) => {
5978
6115
  Trigger.trigger(
5979
6116
  this,
@@ -5990,6 +6127,45 @@ export default class Meeting extends StatelessWebexPlugin {
5990
6127
  );
5991
6128
  }
5992
6129
  );
6130
+
6131
+ this.iceCandidateErrors.clear();
6132
+ this.mediaProperties.webrtcMediaConnection.on(
6133
+ MediaConnectionEventNames.ICE_CANDIDATE_ERROR,
6134
+ (event) => {
6135
+ const {errorCode} = event.error;
6136
+ let {errorText} = event.error;
6137
+
6138
+ if (
6139
+ errorCode === 600 &&
6140
+ errorText === 'Address not associated with the desired network interface.'
6141
+ ) {
6142
+ return;
6143
+ }
6144
+
6145
+ if (errorText.endsWith('.')) {
6146
+ errorText = errorText.slice(0, -1);
6147
+ }
6148
+
6149
+ errorText = errorText.toLowerCase();
6150
+ errorText = errorText.replace(/ /g, '_');
6151
+
6152
+ const error = `${errorCode}_${errorText}`;
6153
+
6154
+ const count = this.iceCandidateErrors.get(error) || 0;
6155
+
6156
+ this.iceCandidateErrors.set(error, count + 1);
6157
+ }
6158
+ );
6159
+
6160
+ this.iceCandidatesCount = 0;
6161
+ this.mediaProperties.webrtcMediaConnection.on(
6162
+ MediaConnectionEventNames.ICE_CANDIDATE,
6163
+ (event) => {
6164
+ if (event.candidate) {
6165
+ this.iceCandidatesCount += 1;
6166
+ }
6167
+ }
6168
+ );
5993
6169
  };
5994
6170
 
5995
6171
  /**
@@ -5999,7 +6175,7 @@ export default class Meeting extends StatelessWebexPlugin {
5999
6175
  * @memberof Meetings
6000
6176
  */
6001
6177
  setupStatsAnalyzerEventHandlers = () => {
6002
- this.statsAnalyzer.on(StatsAnalyzerEvents.MEDIA_QUALITY, (options) => {
6178
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.MEDIA_QUALITY, (options) => {
6003
6179
  // TODO: might have to send the same event to the developer
6004
6180
  // Add ip address info if geo hint is present
6005
6181
  // @ts-ignore fix type
@@ -6013,14 +6189,15 @@ export default class Meeting extends StatelessWebexPlugin {
6013
6189
  name: 'client.mediaquality.event',
6014
6190
  options: {
6015
6191
  meetingId: this.id,
6016
- networkType: options.networkType,
6192
+ networkType: options.data.networkType,
6017
6193
  },
6018
6194
  payload: {
6019
6195
  intervals: [options.data],
6020
6196
  },
6021
6197
  });
6022
6198
  });
6023
- this.statsAnalyzer.on(StatsAnalyzerEvents.LOCAL_MEDIA_STARTED, (data) => {
6199
+
6200
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED, (data) => {
6024
6201
  Trigger.trigger(
6025
6202
  this,
6026
6203
  {
@@ -6034,28 +6211,28 @@ export default class Meeting extends StatelessWebexPlugin {
6034
6211
  this.webex.internal.newMetrics.submitClientEvent({
6035
6212
  name: 'client.media.tx.start',
6036
6213
  payload: {
6037
- mediaType: data.type,
6038
- shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
6214
+ mediaType: data.mediaType,
6215
+ shareInstanceId: data.mediaType === 'share' ? this.localShareInstanceId : undefined,
6039
6216
  },
6040
6217
  options: {
6041
6218
  meetingId: this.id,
6042
6219
  },
6043
6220
  });
6044
6221
  });
6045
- this.statsAnalyzer.on(StatsAnalyzerEvents.LOCAL_MEDIA_STOPPED, (data) => {
6222
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED, (data) => {
6046
6223
  // @ts-ignore
6047
6224
  this.webex.internal.newMetrics.submitClientEvent({
6048
6225
  name: 'client.media.tx.stop',
6049
6226
  payload: {
6050
- mediaType: data.type,
6051
- shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
6227
+ mediaType: data.mediaType,
6228
+ shareInstanceId: data.mediaType === 'share' ? this.localShareInstanceId : undefined,
6052
6229
  },
6053
6230
  options: {
6054
6231
  meetingId: this.id,
6055
6232
  },
6056
6233
  });
6057
6234
  });
6058
- this.statsAnalyzer.on(StatsAnalyzerEvents.REMOTE_MEDIA_STARTED, (data) => {
6235
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED, (data) => {
6059
6236
  Trigger.trigger(
6060
6237
  this,
6061
6238
  {
@@ -6069,15 +6246,15 @@ export default class Meeting extends StatelessWebexPlugin {
6069
6246
  this.webex.internal.newMetrics.submitClientEvent({
6070
6247
  name: 'client.media.rx.start',
6071
6248
  payload: {
6072
- mediaType: data.type,
6073
- shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
6249
+ mediaType: data.mediaType,
6250
+ shareInstanceId: data.mediaType === 'share' ? this.remoteShareInstanceId : undefined,
6074
6251
  },
6075
6252
  options: {
6076
6253
  meetingId: this.id,
6077
6254
  },
6078
6255
  });
6079
6256
 
6080
- if (data.type === 'share') {
6257
+ if (data.mediaType === 'share') {
6081
6258
  // @ts-ignore
6082
6259
  this.webex.internal.newMetrics.submitClientEvent({
6083
6260
  name: 'client.media.render.start',
@@ -6091,20 +6268,20 @@ export default class Meeting extends StatelessWebexPlugin {
6091
6268
  });
6092
6269
  }
6093
6270
  });
6094
- this.statsAnalyzer.on(StatsAnalyzerEvents.REMOTE_MEDIA_STOPPED, (data) => {
6271
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED, (data) => {
6095
6272
  // @ts-ignore
6096
6273
  this.webex.internal.newMetrics.submitClientEvent({
6097
6274
  name: 'client.media.rx.stop',
6098
6275
  payload: {
6099
- mediaType: data.type,
6100
- shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
6276
+ mediaType: data.mediaType,
6277
+ shareInstanceId: data.mediaType === 'share' ? this.remoteShareInstanceId : undefined,
6101
6278
  },
6102
6279
  options: {
6103
6280
  meetingId: this.id,
6104
6281
  },
6105
6282
  });
6106
6283
 
6107
- if (data.type === 'share') {
6284
+ if (data.mediaType === 'share') {
6108
6285
  // @ts-ignore
6109
6286
  this.webex.internal.newMetrics.submitClientEvent({
6110
6287
  name: 'client.media.render.stop',
@@ -6133,14 +6310,17 @@ export default class Meeting extends StatelessWebexPlugin {
6133
6310
  * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
6134
6311
  */
6135
6312
  private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
6313
+ this.rtcMetrics = this.isMultistream
6314
+ ? // @ts-ignore
6315
+ new RtcMetrics(this.webex, this.id, this.correlationId)
6316
+ : undefined;
6317
+
6136
6318
  const mc = Media.createMediaConnection(
6137
6319
  this.isMultistream,
6138
6320
  this.getMediaConnectionDebugId(),
6139
- // @ts-ignore
6140
- this.webex,
6141
6321
  this.id,
6142
- this.correlationId,
6143
6322
  {
6323
+ rtcMetrics: this.rtcMetrics,
6144
6324
  mediaProperties: this.mediaProperties,
6145
6325
  remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6146
6326
  // @ts-ignore - config coming from registerPlugin
@@ -6264,6 +6444,8 @@ export default class Meeting extends StatelessWebexPlugin {
6264
6444
  try {
6265
6445
  await this.mediaProperties.waitForMediaConnectionConnected();
6266
6446
  } catch (error) {
6447
+ const {iceConnected} = error;
6448
+
6267
6449
  if (!this.hasMediaConnectionConnectedAtLeastOnce) {
6268
6450
  // Only send CA event for join flow if we haven't successfully connected media yet
6269
6451
  // @ts-ignore
@@ -6271,7 +6453,7 @@ export default class Meeting extends StatelessWebexPlugin {
6271
6453
  name: 'client.ice.end',
6272
6454
  payload: {
6273
6455
  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',
6456
+ icePhase: this.addMediaData.icePhaseCallback(),
6275
6457
  errors: [
6276
6458
  // @ts-ignore
6277
6459
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
@@ -6283,13 +6465,13 @@ export default class Meeting extends StatelessWebexPlugin {
6283
6465
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6284
6466
  ?.signalingState ||
6285
6467
  'unknown',
6286
- iceConnectionState:
6287
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6288
- ?.iceConnectionState ||
6289
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6290
- ?.iceConnectionState ||
6291
- 'unknown',
6468
+ iceConnected,
6292
6469
  turnServerUsed: this.turnServerUsed,
6470
+ unreachable:
6471
+ // @ts-ignore
6472
+ await this.webex.meetings.reachability
6473
+ .isWebexMediaBackendUnreachable()
6474
+ .catch(() => false),
6293
6475
  }),
6294
6476
  }
6295
6477
  ),
@@ -6317,15 +6499,15 @@ export default class Meeting extends StatelessWebexPlugin {
6317
6499
  if (this.config.stats.enableStatsAnalyzer) {
6318
6500
  // @ts-ignore - config coming from registerPlugin
6319
6501
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
6320
- this.statsAnalyzer = new StatsAnalyzer(
6502
+ this.statsAnalyzer = new StatsAnalyzer({
6321
6503
  // @ts-ignore - config coming from registerPlugin
6322
- this.config.stats,
6323
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
6324
- this.networkQualityMonitor
6325
- );
6504
+ config: this.config.stats,
6505
+ networkQualityMonitor: this.networkQualityMonitor,
6506
+ isMultistream: this.isMultistream,
6507
+ });
6326
6508
  this.setupStatsAnalyzerEventHandlers();
6327
6509
  this.networkQualityMonitor.on(
6328
- EVENT_TRIGGERS.NETWORK_QUALITY,
6510
+ NetworkQualityEventNames.NETWORK_QUALITY,
6329
6511
  this.sendNetworkQualityEvent.bind(this)
6330
6512
  );
6331
6513
  }
@@ -6374,6 +6556,21 @@ export default class Meeting extends StatelessWebexPlugin {
6374
6556
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
6375
6557
  } seconds`
6376
6558
  );
6559
+ // @ts-ignore
6560
+ this.webex.internal.newMetrics.submitClientEvent({
6561
+ name: 'client.media-engine.remote-sdp-received',
6562
+ payload: {
6563
+ canProceed: false,
6564
+ errors: [
6565
+ // @ts-ignore
6566
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode({
6567
+ clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6568
+ }),
6569
+ ],
6570
+ },
6571
+ options: {meetingId: this.id, rawError: new Error('Timeout waiting for SDP answer')},
6572
+ });
6573
+
6377
6574
  deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
6378
6575
  }, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
6379
6576
 
@@ -6422,7 +6619,7 @@ export default class Meeting extends StatelessWebexPlugin {
6422
6619
  remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6423
6620
  bundlePolicy?: BundlePolicy
6424
6621
  ): Promise<void> {
6425
- this.retriedWithTurnServer = true;
6622
+ this.addMediaData.retriedWithTurnServer = true;
6426
6623
  const LOG_HEADER = 'Meeting:index#addMedia():retryWithForcedTurnDiscovery -->';
6427
6624
 
6428
6625
  await this.cleanUpBeforeRetryWithTurnServer();
@@ -6517,7 +6714,7 @@ export default class Meeting extends StatelessWebexPlugin {
6517
6714
  correlation_id: this.correlationId,
6518
6715
  latency: cdl.getTurnDiscoveryTime(),
6519
6716
  turnServerUsed: this.turnServerUsed,
6520
- retriedWithTurnServer: this.retriedWithTurnServer,
6717
+ retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
6521
6718
  });
6522
6719
  }
6523
6720
 
@@ -6541,7 +6738,8 @@ export default class Meeting extends StatelessWebexPlugin {
6541
6738
  turnServerInfo?: TurnServerInfo
6542
6739
  ): Promise<void> {
6543
6740
  const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
6544
- const isReconnecting = this.isMoveToInProgress || this.retriedWithTurnServer;
6741
+ const isReconnecting =
6742
+ this.isMoveToInProgress || !!this.locusMediaRequest?.isConfluenceCreated();
6545
6743
 
6546
6744
  // We are forcing turn discovery if the case is moveTo and a turn server was used already
6547
6745
  if (this.isMoveToInProgress && this.turnServerUsed) {
@@ -6663,24 +6861,80 @@ export default class Meeting extends StatelessWebexPlugin {
6663
6861
  }
6664
6862
  }
6665
6863
 
6864
+ /**
6865
+ * Creates an instance of LocusMediaRequest for this meeting - it is needed for doing any calls
6866
+ * to Locus /media API (these are used for sending Roap messages and updating audio/video mute status).
6867
+ *
6868
+ * @returns {void}
6869
+ */
6870
+ private setupLocusMediaRequest() {
6871
+ this.locusMediaRequest = new LocusMediaRequest(
6872
+ {
6873
+ correlationId: this.correlationId,
6874
+ meetingId: this.id,
6875
+ device: {
6876
+ url: this.deviceUrl,
6877
+ // @ts-ignore
6878
+ deviceType: this.config.deviceType,
6879
+ // @ts-ignore
6880
+ countryCode: this.webex.meetings.geoHintInfo?.countryCode,
6881
+ // @ts-ignore
6882
+ regionCode: this.webex.meetings.geoHintInfo?.regionCode,
6883
+ },
6884
+ preferTranscoding: !this.isMultistream,
6885
+ },
6886
+ {
6887
+ // @ts-ignore
6888
+ parent: this.webex,
6889
+ }
6890
+ );
6891
+ }
6892
+
6666
6893
  /**
6667
6894
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
6668
6895
  *
6669
6896
  * @param {AddMediaOptions} options
6670
- * @param {TurnServerInfo} turnServerInfo - TURN server information (used only internally by the SDK)
6671
6897
  * @returns {Promise<void>}
6672
6898
  * @public
6673
6899
  * @memberof Meeting
6674
6900
  */
6675
- async addMedia(
6676
- options: AddMediaOptions = {},
6677
- turnServerInfo: TurnServerInfo = undefined
6901
+ addMedia(options: AddMediaOptions = {}): Promise<void> {
6902
+ return this.addMediaInternal(
6903
+ () => (this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY'),
6904
+ undefined,
6905
+ false,
6906
+ options
6907
+ );
6908
+ }
6909
+
6910
+ /**
6911
+ * Internal version of addMedia() with some more arguments for finer control of its behavior
6912
+ *
6913
+ * @param {Function} icePhaseCallback - callback to determine the icePhase for CA "client.ice.end" failure events
6914
+ * @param {TurnServerInfo} turnServerInfo - TURN server information
6915
+ * @param {boolean} forceTurnDiscovery - if true, TURN discovery will be done
6916
+ * @param {AddMediaOptions} options - same as options of the public addMedia() method
6917
+ * @returns {Promise<void>}
6918
+ * @protected
6919
+ * @memberof Meeting
6920
+ */
6921
+ protected async addMediaInternal(
6922
+ icePhaseCallback: () => string,
6923
+ turnServerInfo: TurnServerInfo,
6924
+ forceTurnDiscovery,
6925
+ options: AddMediaOptions = {}
6678
6926
  ): Promise<void> {
6679
- this.retriedWithTurnServer = false;
6927
+ this.addMediaData.retriedWithTurnServer = false;
6928
+ this.addMediaData.icePhaseCallback = icePhaseCallback;
6929
+
6680
6930
  this.hasMediaConnectionConnectedAtLeastOnce = false;
6681
6931
  const LOG_HEADER = 'Meeting:index#addMedia -->';
6682
6932
  LoggerProxy.logger.info(
6683
- `${LOG_HEADER} called with: ${JSON.stringify(options)}, ${JSON.stringify(turnServerInfo)}`
6933
+ `${LOG_HEADER} called with: options=${JSON.stringify(
6934
+ options
6935
+ )}, turnServerInfo=${JSON.stringify(
6936
+ turnServerInfo
6937
+ )}, forceTurnDiscovery=${forceTurnDiscovery}`
6684
6938
  );
6685
6939
 
6686
6940
  if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
@@ -6744,27 +6998,6 @@ export default class Meeting extends StatelessWebexPlugin {
6744
6998
  receiveShare: shareAudioEnabled || shareVideoEnabled,
6745
6999
  });
6746
7000
 
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
7001
  this.audio = createMuteState(AUDIO, this, audioEnabled);
6769
7002
  this.video = createMuteState(VIDEO, this, videoEnabled);
6770
7003
 
@@ -6778,7 +7011,7 @@ export default class Meeting extends StatelessWebexPlugin {
6778
7011
  await this.establishMediaConnection(
6779
7012
  remoteMediaManagerConfig,
6780
7013
  bundlePolicy,
6781
- false,
7014
+ forceTurnDiscovery,
6782
7015
  turnServerInfo
6783
7016
  );
6784
7017
 
@@ -6796,6 +7029,7 @@ export default class Meeting extends StatelessWebexPlugin {
6796
7029
  await this.mediaProperties.getCurrentConnectionInfo();
6797
7030
  // @ts-ignore
6798
7031
  const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
7032
+ const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
6799
7033
 
6800
7034
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
6801
7035
  correlation_id: this.correlationId,
@@ -6804,9 +7038,11 @@ export default class Meeting extends StatelessWebexPlugin {
6804
7038
  selectedCandidatePairChanges,
6805
7039
  numTransports,
6806
7040
  isMultistream: this.isMultistream,
6807
- retriedWithTurnServer: this.retriedWithTurnServer,
7041
+ retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
6808
7042
  isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
6809
7043
  ...reachabilityStats,
7044
+ ...iceCandidateErrors,
7045
+ iceCandidatesCount: this.iceCandidatesCount,
6810
7046
  });
6811
7047
  // @ts-ignore
6812
7048
  this.webex.internal.newMetrics.submitClientEvent({
@@ -6830,6 +7066,8 @@ export default class Meeting extends StatelessWebexPlugin {
6830
7066
  const {selectedCandidatePairChanges, numTransports} =
6831
7067
  await this.mediaProperties.getCurrentConnectionInfo();
6832
7068
 
7069
+ const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
7070
+
6833
7071
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
6834
7072
  correlation_id: this.correlationId,
6835
7073
  locus_id: this.locusUrl.split('/').pop(),
@@ -6840,7 +7078,7 @@ export default class Meeting extends StatelessWebexPlugin {
6840
7078
  numTransports,
6841
7079
  turnDiscoverySkippedReason: this.turnDiscoverySkippedReason,
6842
7080
  turnServerUsed: this.turnServerUsed,
6843
- retriedWithTurnServer: this.retriedWithTurnServer,
7081
+ retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
6844
7082
  isMultistream: this.isMultistream,
6845
7083
  isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
6846
7084
  signalingState:
@@ -6859,6 +7097,8 @@ export default class Meeting extends StatelessWebexPlugin {
6859
7097
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
6860
7098
  'unknown',
6861
7099
  ...reachabilityMetrics,
7100
+ ...iceCandidateErrors,
7101
+ iceCandidatesCount: this.iceCandidatesCount,
6862
7102
  });
6863
7103
 
6864
7104
  await this.cleanUpOnAddMediaFailure();
@@ -6879,6 +7119,8 @@ export default class Meeting extends StatelessWebexPlugin {
6879
7119
  }
6880
7120
 
6881
7121
  throw error;
7122
+ } finally {
7123
+ this.addMediaData.icePhaseCallback = DEFAULT_ICE_PHASE_CALLBACK;
6882
7124
  }
6883
7125
  }
6884
7126
 
@@ -7896,6 +8138,7 @@ export default class Meeting extends StatelessWebexPlugin {
7896
8138
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
7897
8139
  correlationId: this.correlationId,
7898
8140
  muted,
8141
+ encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
7899
8142
  });
7900
8143
  };
7901
8144
 
@@ -7957,7 +8200,7 @@ export default class Meeting extends StatelessWebexPlugin {
7957
8200
  * @private
7958
8201
  * @memberof Meeting
7959
8202
  */
7960
- private sendNetworkQualityEvent(res: any) {
8203
+ private sendNetworkQualityEvent(res: {networkQualityScore: number; mediaType: string}) {
7961
8204
  Trigger.trigger(
7962
8205
  this,
7963
8206
  {