@webex/plugin-meetings 3.0.0-stream-classes.4 → 3.0.0-test.1

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 (239) hide show
  1. package/README.md +12 -0
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/common/errors/no-meeting-info.js +51 -0
  5. package/dist/common/errors/no-meeting-info.js.map +1 -0
  6. package/dist/common/errors/reclaim-host-role-errors.js +158 -0
  7. package/dist/common/errors/reclaim-host-role-errors.js.map +1 -0
  8. package/dist/common/errors/webex-errors.js +23 -3
  9. package/dist/common/errors/webex-errors.js.map +1 -1
  10. package/dist/common/logs/request.js +5 -1
  11. package/dist/common/logs/request.js.map +1 -1
  12. package/dist/config.js +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/constants.js +69 -9
  15. package/dist/constants.js.map +1 -1
  16. package/dist/index.js +11 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/interceptors/index.js +15 -0
  19. package/dist/interceptors/index.js.map +1 -0
  20. package/dist/interceptors/locusRetry.js +93 -0
  21. package/dist/interceptors/locusRetry.js.map +1 -0
  22. package/dist/interpretation/index.js +16 -2
  23. package/dist/interpretation/index.js.map +1 -1
  24. package/dist/interpretation/siLanguage.js +1 -1
  25. package/dist/locus-info/index.js +40 -11
  26. package/dist/locus-info/index.js.map +1 -1
  27. package/dist/locus-info/mediaSharesUtils.js +15 -1
  28. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  29. package/dist/locus-info/parser.js +42 -21
  30. package/dist/locus-info/parser.js.map +1 -1
  31. package/dist/media/index.js +10 -6
  32. package/dist/media/index.js.map +1 -1
  33. package/dist/media/properties.js +13 -3
  34. package/dist/media/properties.js.map +1 -1
  35. package/dist/mediaQualityMetrics/config.js +135 -330
  36. package/dist/mediaQualityMetrics/config.js.map +1 -1
  37. package/dist/meeting/in-meeting-actions.js +4 -0
  38. package/dist/meeting/in-meeting-actions.js.map +1 -1
  39. package/dist/meeting/index.js +2187 -1074
  40. package/dist/meeting/index.js.map +1 -1
  41. package/dist/meeting/muteState.js +37 -25
  42. package/dist/meeting/muteState.js.map +1 -1
  43. package/dist/meeting/request.js +34 -19
  44. package/dist/meeting/request.js.map +1 -1
  45. package/dist/meeting/util.js +71 -0
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meeting-info/index.js +48 -23
  48. package/dist/meeting-info/index.js.map +1 -1
  49. package/dist/meeting-info/meeting-info-v2.js +25 -4
  50. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  51. package/dist/meeting-info/utilv2.js +1 -1
  52. package/dist/meeting-info/utilv2.js.map +1 -1
  53. package/dist/meetings/collection.js +17 -0
  54. package/dist/meetings/collection.js.map +1 -1
  55. package/dist/meetings/index.js +142 -57
  56. package/dist/meetings/index.js.map +1 -1
  57. package/dist/meetings/util.js +2 -6
  58. package/dist/meetings/util.js.map +1 -1
  59. package/dist/member/index.js +9 -0
  60. package/dist/member/index.js.map +1 -1
  61. package/dist/member/util.js +11 -0
  62. package/dist/member/util.js.map +1 -1
  63. package/dist/members/index.js +17 -1
  64. package/dist/members/index.js.map +1 -1
  65. package/dist/members/types.js.map +1 -1
  66. package/dist/members/util.js +15 -4
  67. package/dist/members/util.js.map +1 -1
  68. package/dist/metrics/constants.js +15 -1
  69. package/dist/metrics/constants.js.map +1 -1
  70. package/dist/multistream/mediaRequestManager.js +1 -1
  71. package/dist/multistream/mediaRequestManager.js.map +1 -1
  72. package/dist/multistream/remoteMediaGroup.js +16 -2
  73. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  74. package/dist/multistream/remoteMediaManager.js +222 -73
  75. package/dist/multistream/remoteMediaManager.js.map +1 -1
  76. package/dist/multistream/sendSlotManager.js +22 -0
  77. package/dist/multistream/sendSlotManager.js.map +1 -1
  78. package/dist/reachability/clusterReachability.js +356 -0
  79. package/dist/reachability/clusterReachability.js.map +1 -0
  80. package/dist/reachability/index.js +262 -432
  81. package/dist/reachability/index.js.map +1 -1
  82. package/dist/reachability/request.js +1 -1
  83. package/dist/reachability/request.js.map +1 -1
  84. package/dist/reachability/util.js +29 -0
  85. package/dist/reachability/util.js.map +1 -0
  86. package/dist/reconnection-manager/index.js +113 -96
  87. package/dist/reconnection-manager/index.js.map +1 -1
  88. package/dist/roap/index.js +57 -25
  89. package/dist/roap/index.js.map +1 -1
  90. package/dist/roap/request.js +5 -13
  91. package/dist/roap/request.js.map +1 -1
  92. package/dist/roap/turnDiscovery.js +173 -81
  93. package/dist/roap/turnDiscovery.js.map +1 -1
  94. package/dist/rtcMetrics/index.js +68 -6
  95. package/dist/rtcMetrics/index.js.map +1 -1
  96. package/dist/statsAnalyzer/index.js +338 -289
  97. package/dist/statsAnalyzer/index.js.map +1 -1
  98. package/dist/statsAnalyzer/mqaUtil.js +296 -156
  99. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  100. package/dist/types/common/errors/no-meeting-info.d.ts +14 -0
  101. package/dist/types/common/errors/reclaim-host-role-errors.d.ts +60 -0
  102. package/dist/types/common/errors/webex-errors.d.ts +13 -1
  103. package/dist/types/common/logs/request.d.ts +2 -0
  104. package/dist/types/config.d.ts +1 -1
  105. package/dist/types/constants.d.ts +66 -13
  106. package/dist/types/index.d.ts +1 -1
  107. package/dist/types/interceptors/index.d.ts +2 -0
  108. package/dist/types/interceptors/locusRetry.d.ts +27 -0
  109. package/dist/types/locus-info/index.d.ts +1 -1
  110. package/dist/types/locus-info/parser.d.ts +3 -2
  111. package/dist/types/mediaQualityMetrics/config.d.ts +99 -223
  112. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  113. package/dist/types/meeting/index.d.ts +285 -34
  114. package/dist/types/meeting/locusMediaRequest.d.ts +1 -2
  115. package/dist/types/meeting/muteState.d.ts +2 -8
  116. package/dist/types/meeting/request.d.ts +4 -1
  117. package/dist/types/meeting/util.d.ts +25 -1
  118. package/dist/types/meeting-info/index.d.ts +7 -0
  119. package/dist/types/meeting-info/meeting-info-v2.d.ts +1 -0
  120. package/dist/types/meetings/collection.d.ts +9 -0
  121. package/dist/types/meetings/index.d.ts +42 -14
  122. package/dist/types/member/index.d.ts +1 -0
  123. package/dist/types/members/types.d.ts +1 -0
  124. package/dist/types/members/util.d.ts +5 -0
  125. package/dist/types/metrics/constants.d.ts +15 -0
  126. package/dist/types/multistream/mediaRequestManager.d.ts +2 -0
  127. package/dist/types/multistream/remoteMediaGroup.d.ts +2 -0
  128. package/dist/types/multistream/remoteMediaManager.d.ts +25 -1
  129. package/dist/types/multistream/sendSlotManager.d.ts +9 -0
  130. package/dist/types/reachability/clusterReachability.d.ts +109 -0
  131. package/dist/types/reachability/index.d.ts +59 -112
  132. package/dist/types/reachability/request.d.ts +1 -1
  133. package/dist/types/reachability/util.d.ts +8 -0
  134. package/dist/types/reconnection-manager/index.d.ts +10 -0
  135. package/dist/types/roap/index.d.ts +2 -1
  136. package/dist/types/roap/request.d.ts +2 -1
  137. package/dist/types/roap/turnDiscovery.d.ts +21 -4
  138. package/dist/types/rtcMetrics/index.d.ts +15 -1
  139. package/dist/types/statsAnalyzer/index.d.ts +28 -11
  140. package/dist/types/statsAnalyzer/mqaUtil.d.ts +28 -4
  141. package/dist/types/webinar/collection.d.ts +16 -0
  142. package/dist/types/webinar/index.d.ts +5 -0
  143. package/dist/webinar/collection.js +44 -0
  144. package/dist/webinar/collection.js.map +1 -0
  145. package/dist/webinar/index.js +69 -0
  146. package/dist/webinar/index.js.map +1 -0
  147. package/package.json +3 -2
  148. package/src/common/errors/no-meeting-info.ts +24 -0
  149. package/src/common/errors/reclaim-host-role-errors.ts +134 -0
  150. package/src/common/errors/webex-errors.ts +19 -2
  151. package/src/common/logs/request.ts +5 -1
  152. package/src/config.ts +1 -1
  153. package/src/constants.ts +71 -6
  154. package/src/index.ts +5 -0
  155. package/src/interceptors/index.ts +3 -0
  156. package/src/interceptors/locusRetry.ts +67 -0
  157. package/src/interpretation/index.ts +18 -1
  158. package/src/locus-info/index.ts +52 -16
  159. package/src/locus-info/mediaSharesUtils.ts +16 -0
  160. package/src/locus-info/parser.ts +47 -21
  161. package/src/media/index.ts +8 -6
  162. package/src/media/properties.ts +17 -2
  163. package/src/mediaQualityMetrics/config.ts +103 -238
  164. package/src/meeting/in-meeting-actions.ts +8 -0
  165. package/src/meeting/index.ts +1510 -529
  166. package/src/meeting/muteState.ts +34 -20
  167. package/src/meeting/request.ts +19 -1
  168. package/src/meeting/util.ts +97 -0
  169. package/src/meeting-info/index.ts +47 -20
  170. package/src/meeting-info/meeting-info-v2.ts +27 -5
  171. package/src/meeting-info/utilv2.ts +1 -1
  172. package/src/meetings/collection.ts +13 -0
  173. package/src/meetings/index.ts +112 -31
  174. package/src/meetings/util.ts +2 -8
  175. package/src/member/index.ts +9 -0
  176. package/src/member/util.ts +14 -0
  177. package/src/members/index.ts +29 -2
  178. package/src/members/types.ts +1 -0
  179. package/src/members/util.ts +15 -1
  180. package/src/metrics/constants.ts +14 -0
  181. package/src/multistream/mediaRequestManager.ts +4 -1
  182. package/src/multistream/remoteMediaGroup.ts +19 -0
  183. package/src/multistream/remoteMediaManager.ts +141 -18
  184. package/src/multistream/sendSlotManager.ts +29 -0
  185. package/src/reachability/clusterReachability.ts +320 -0
  186. package/src/reachability/index.ts +221 -382
  187. package/src/reachability/request.ts +1 -1
  188. package/src/reachability/util.ts +24 -0
  189. package/src/reconnection-manager/index.ts +87 -83
  190. package/src/roap/index.ts +60 -24
  191. package/src/roap/request.ts +3 -16
  192. package/src/roap/turnDiscovery.ts +112 -39
  193. package/src/rtcMetrics/index.ts +71 -5
  194. package/src/statsAnalyzer/index.ts +430 -427
  195. package/src/statsAnalyzer/mqaUtil.ts +317 -168
  196. package/src/webinar/collection.ts +31 -0
  197. package/src/webinar/index.ts +62 -0
  198. package/test/integration/spec/converged-space-meetings.js +7 -7
  199. package/test/integration/spec/journey.js +86 -104
  200. package/test/integration/spec/space-meeting.js +9 -9
  201. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  202. package/test/unit/spec/interpretation/index.ts +36 -3
  203. package/test/unit/spec/locus-info/index.js +205 -12
  204. package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
  205. package/test/unit/spec/locus-info/mediaSharesUtils.ts +10 -0
  206. package/test/unit/spec/locus-info/parser.js +54 -13
  207. package/test/unit/spec/media/index.ts +20 -4
  208. package/test/unit/spec/media/properties.ts +2 -2
  209. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  210. package/test/unit/spec/meeting/index.js +4027 -1075
  211. package/test/unit/spec/meeting/muteState.js +219 -67
  212. package/test/unit/spec/meeting/request.js +63 -12
  213. package/test/unit/spec/meeting/utils.js +93 -0
  214. package/test/unit/spec/meeting-info/index.js +180 -61
  215. package/test/unit/spec/meeting-info/meetinginfov2.js +196 -53
  216. package/test/unit/spec/meetings/collection.js +12 -0
  217. package/test/unit/spec/meetings/index.js +619 -206
  218. package/test/unit/spec/meetings/utils.js +35 -12
  219. package/test/unit/spec/member/index.js +8 -7
  220. package/test/unit/spec/member/util.js +32 -0
  221. package/test/unit/spec/members/index.js +130 -17
  222. package/test/unit/spec/members/utils.js +26 -0
  223. package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
  224. package/test/unit/spec/multistream/remoteMediaGroup.ts +80 -1
  225. package/test/unit/spec/multistream/remoteMediaManager.ts +210 -3
  226. package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
  227. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  228. package/test/unit/spec/reachability/index.ts +505 -135
  229. package/test/unit/spec/reachability/util.ts +40 -0
  230. package/test/unit/spec/reconnection-manager/index.js +74 -17
  231. package/test/unit/spec/roap/index.ts +181 -61
  232. package/test/unit/spec/roap/request.ts +27 -3
  233. package/test/unit/spec/roap/turnDiscovery.ts +362 -101
  234. package/test/unit/spec/rtcMetrics/index.ts +57 -3
  235. package/test/unit/spec/stats-analyzer/index.js +1225 -12
  236. package/test/unit/spec/webinar/collection.ts +13 -0
  237. package/test/unit/spec/webinar/index.ts +60 -0
  238. package/test/utils/integrationTestUtils.js +4 -4
  239. package/test/utils/webex-test-users.js +12 -4
@@ -3,10 +3,12 @@ import {cloneDeep, isEqual, isEmpty} from 'lodash';
3
3
  import jwt from 'jsonwebtoken';
4
4
  // @ts-ignore - Fix this
5
5
  import {StatelessWebexPlugin} from '@webex/webex-core';
6
+ // @ts-ignore - Types not available for @webex/common
7
+ import {Defer} from '@webex/common';
6
8
  import {
7
9
  ClientEvent,
8
10
  ClientEventLeaveReason,
9
- CALL_DIAGNOSTIC_CONFIG,
11
+ CallDiagnosticUtils,
10
12
  } from '@webex/internal-plugin-metrics';
11
13
  import {
12
14
  ConnectionState,
@@ -16,6 +18,7 @@ import {
16
18
  MediaContent,
17
19
  MediaType,
18
20
  RemoteTrackType,
21
+ RoapMessage,
19
22
  } from '@webex/internal-media-core';
20
23
 
21
24
  import {
@@ -35,6 +38,7 @@ import {
35
38
  UserInLobbyError,
36
39
  NoMediaEstablishedYetError,
37
40
  UserNotJoinedError,
41
+ AddMediaFailed,
38
42
  } from '../common/errors/webex-errors';
39
43
  import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer';
40
44
  import NetworkQualityMonitor from '../networkQualityMonitor';
@@ -51,6 +55,7 @@ import ReconnectionManager from '../reconnection-manager';
51
55
  import MeetingRequest from './request';
52
56
  import Members from '../members/index';
53
57
  import MeetingUtil from './util';
58
+ import MeetingsUtil from '../meetings/util';
54
59
  import RecordingUtil from '../recording-controller/util';
55
60
  import ControlsOptionsUtil from '../controls-options-manager/util';
56
61
  import MediaUtil from '../media/util';
@@ -61,9 +66,10 @@ import CaptchaError from '../common/errors/captcha-error';
61
66
  import ReconnectionError from '../common/errors/reconnection';
62
67
  import ReconnectInProgress from '../common/errors/reconnection-in-progress';
63
68
  import {
64
- _CALL_,
69
+ _CONVERSATION_URL_,
65
70
  _INCOMING_,
66
71
  _JOIN_,
72
+ _MEETING_LINK_,
67
73
  AUDIO,
68
74
  CONTENT,
69
75
  DISPLAY_HINTS,
@@ -95,6 +101,11 @@ import {
95
101
  SELF_ROLES,
96
102
  INTERPRETATION,
97
103
  SELF_POLICY,
104
+ MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
105
+ MEETING_PERMISSION_TOKEN_REFRESH_REASON,
106
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
107
+ RECONNECTION,
108
+ NAMED_MEDIA_GROUP_TYPE_AUDIO,
98
109
  } from '../constants';
99
110
  import BEHAVIORAL_METRICS from '../metrics/constants';
100
111
  import ParameterError from '../common/errors/parameter';
@@ -122,6 +133,7 @@ import {
122
133
  import Breakouts from '../breakouts';
123
134
  import SimultaneousInterpretation from '../interpretation';
124
135
  import Annotation from '../annotation';
136
+ import Webinar from '../webinar';
125
137
 
126
138
  import InMeetingActions from './in-meeting-actions';
127
139
  import {REACTION_RELAY_TYPES} from '../reactions/constants';
@@ -167,6 +179,12 @@ export type AddMediaOptions = {
167
179
  allowMediaInLobby?: boolean; // allows adding media when in the lobby
168
180
  };
169
181
 
182
+ export type CallStateForMetrics = {
183
+ correlationId?: string;
184
+ joinTrigger?: string;
185
+ loginType?: string;
186
+ };
187
+
170
188
  export const MEDIA_UPDATE_TYPE = {
171
189
  TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
172
190
  SHARE_FLOOR_REQUEST: 'SHARE_FLOOR_REQUEST',
@@ -179,6 +197,13 @@ export enum ScreenShareFloorStatus {
179
197
  RELEASED = 'floor_released',
180
198
  }
181
199
 
200
+ type FetchMeetingInfoParams = {
201
+ password?: string;
202
+ captchaCode?: string;
203
+ extraParams?: Record<string, any>;
204
+ sendCAevents?: boolean;
205
+ };
206
+
182
207
  /**
183
208
  * MediaDirection
184
209
  * @typedef {Object} MediaDirection
@@ -457,8 +482,9 @@ export default class Meeting extends StatelessWebexPlugin {
457
482
  breakouts: any;
458
483
  simultaneousInterpretation: any;
459
484
  annotation: any;
485
+ webinar: any;
460
486
  conversationUrl: string;
461
- correlationId: string;
487
+ callStateForMetrics: CallStateForMetrics;
462
488
  destination: string;
463
489
  destinationType: string;
464
490
  deviceUrl: string;
@@ -514,8 +540,9 @@ export default class Meeting extends StatelessWebexPlugin {
514
540
 
515
541
  meetingInfoFailureReason: string;
516
542
  meetingInfoFailureCode?: number;
543
+ meetingInfoExtraParams?: Record<string, any>;
517
544
  networkQualityMonitor: NetworkQualityMonitor;
518
- networkStatus: string;
545
+ networkStatus?: NETWORK_STATUS;
519
546
  passwordStatus: string;
520
547
  queuedMediaUpdates: any[];
521
548
  recording: any;
@@ -525,6 +552,7 @@ export default class Meeting extends StatelessWebexPlugin {
525
552
  requiredCaptcha: any;
526
553
  receiveSlotManager: ReceiveSlotManager;
527
554
  selfUserPolicies: any;
555
+ enforceVBGImagesURL: string;
528
556
  shareStatus: string;
529
557
  screenShareFloorState: ScreenShareFloorStatus;
530
558
  statsAnalyzer: StatsAnalyzer;
@@ -544,19 +572,29 @@ export default class Meeting extends StatelessWebexPlugin {
544
572
  meetingJoinUrl: any;
545
573
  meetingNumber: any;
546
574
  meetingState: any;
547
- permissionToken: any;
575
+ permissionToken: string;
576
+ permissionTokenPayload: any;
577
+ permissionTokenReceivedLocalTime: number;
548
578
  resourceId: any;
549
579
  resourceUrl: string;
550
580
  selfId: string;
551
581
  state: any;
552
- localAudioStreamMuteStateHandler: (muted: boolean) => void;
553
- localVideoStreamMuteStateHandler: (muted: boolean) => void;
582
+ localAudioStreamMuteStateHandler: () => void;
583
+ localVideoStreamMuteStateHandler: () => void;
554
584
  localOutputTrackChangeHandler: () => void;
555
585
  roles: any[];
556
586
  environment: string;
557
587
  namespace = MEETINGS;
558
588
  allowMediaInLobby: boolean;
589
+ localShareInstanceId: string;
590
+ remoteShareInstanceId: string;
591
+ turnDiscoverySkippedReason: string;
592
+ turnServerUsed: boolean;
593
+ private retriedWithTurnServer: boolean;
559
594
  private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
595
+ private deferSDPAnswer?: Defer; // used for waiting for a response
596
+ private sdpResponseTimer?: ReturnType<typeof setTimeout>;
597
+ private hasMediaConnectionConnectedAtLeastOnce: boolean;
560
598
 
561
599
  /**
562
600
  * @param {Object} attrs
@@ -591,20 +629,22 @@ export default class Meeting extends StatelessWebexPlugin {
591
629
  */
592
630
  this.id = uuid.v4();
593
631
  /**
594
- * Correlation ID used for network tracking of meeting
632
+ * Call state used for metrics
595
633
  * @instance
596
- * @type {String}
634
+ * @type {CallStateForMetrics}
597
635
  * @readonly
598
636
  * @public
599
637
  * @memberof Meeting
600
638
  */
601
- if (attrs.correlationId) {
639
+ this.callStateForMetrics = attrs.callStateForMetrics || {};
640
+ const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
641
+ if (correlationId) {
602
642
  LoggerProxy.logger.log(
603
- `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${this.correlationId}`
643
+ `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${correlationId}`
604
644
  );
605
- this.correlationId = attrs.correlationId;
645
+ this.callStateForMetrics.correlationId = correlationId;
606
646
  } else {
607
- this.correlationId = this.id;
647
+ this.callStateForMetrics.correlationId = this.id;
608
648
  }
609
649
  /**
610
650
  * @instance
@@ -672,6 +712,14 @@ export default class Meeting extends StatelessWebexPlugin {
672
712
  */
673
713
  // @ts-ignore
674
714
  this.annotation = new Annotation({parent: this.webex});
715
+ /**
716
+ * @instance
717
+ * @type {Webinar}
718
+ * @public
719
+ * @memberof Meeting
720
+ */
721
+ // @ts-ignore
722
+ this.webinar = new Webinar({}, {parent: this.webex});
675
723
  /**
676
724
  * helper class for managing receive slots (for multistream media connections)
677
725
  */
@@ -1068,13 +1116,14 @@ export default class Meeting extends StatelessWebexPlugin {
1068
1116
  */
1069
1117
  this.networkQualityMonitor = null;
1070
1118
  /**
1119
+ * Indicates network status of the webrtc media connection
1071
1120
  * @instance
1072
1121
  * @type {String}
1073
1122
  * @readonly
1074
1123
  * @public
1075
1124
  * @memberof Meeting
1076
1125
  */
1077
- this.networkStatus = null;
1126
+ this.networkStatus = undefined;
1078
1127
  /**
1079
1128
  * Passing only info as we send basic info for meeting added event
1080
1129
  * @instance
@@ -1193,6 +1242,24 @@ export default class Meeting extends StatelessWebexPlugin {
1193
1242
  */
1194
1243
  this.keepAliveTimerId = null;
1195
1244
 
1245
+ /**
1246
+ * id for tracking Local Share instances in Call Analyzer
1247
+ * @instance
1248
+ * @type {String}
1249
+ * @private
1250
+ * @memberof Meeting
1251
+ */
1252
+ this.localShareInstanceId = null;
1253
+
1254
+ /**
1255
+ * id for tracking Remote Share instances in Call Analyzer
1256
+ * @instance
1257
+ * @type {String}
1258
+ * @private
1259
+ * @memberof Meeting
1260
+ */
1261
+ this.remoteShareInstanceId = null;
1262
+
1196
1263
  /**
1197
1264
  * The class that helps to control recording functions: start, stop, pause, resume, etc
1198
1265
  * @instance
@@ -1228,12 +1295,12 @@ export default class Meeting extends StatelessWebexPlugin {
1228
1295
  */
1229
1296
  this.remoteMediaManager = null;
1230
1297
 
1231
- this.localAudioStreamMuteStateHandler = (muted: boolean) => {
1232
- this.audio.handleLocalStreamMuteStateChange(this, muted);
1298
+ this.localAudioStreamMuteStateHandler = () => {
1299
+ this.audio.handleLocalStreamMuteStateChange(this);
1233
1300
  };
1234
1301
 
1235
- this.localVideoStreamMuteStateHandler = (muted: boolean) => {
1236
- this.video.handleLocalStreamMuteStateChange(this, muted);
1302
+ this.localVideoStreamMuteStateHandler = () => {
1303
+ this.video.handleLocalStreamMuteStateChange(this);
1237
1304
  };
1238
1305
 
1239
1306
  // The handling of output track changes should be done inside
@@ -1245,6 +1312,60 @@ export default class Meeting extends StatelessWebexPlugin {
1245
1312
  this.updateTranscodedMediaConnection();
1246
1313
  }
1247
1314
  };
1315
+
1316
+ /**
1317
+ * Promise that exists if SDP offer has been generated, and resolves once sdp answer is received.
1318
+ * @instance
1319
+ * @type {Defer}
1320
+ * @private
1321
+ * @memberof Meeting
1322
+ */
1323
+ this.deferSDPAnswer = undefined;
1324
+
1325
+ /**
1326
+ * Timer for waiting for sdp answer.
1327
+ * @instance
1328
+ * @type {ReturnType<typeof setTimeout>}
1329
+ * @private
1330
+ * @memberof Meeting
1331
+ */
1332
+ this.sdpResponseTimer = undefined;
1333
+
1334
+ /**
1335
+ * Reason why TURN discovery is skipped.
1336
+ * @instance
1337
+ * @type {string}
1338
+ * @public
1339
+ * @memberof Meeting
1340
+ */
1341
+ this.turnDiscoverySkippedReason = undefined;
1342
+
1343
+ /**
1344
+ * Whether TURN discovery is used or not.
1345
+ * @instance
1346
+ * @type {boolean}
1347
+ * @public
1348
+ * @memberof Meeting
1349
+ */
1350
+ this.turnServerUsed = false;
1351
+
1352
+ /**
1353
+ * Whether retry was done using TURN Discovery.
1354
+ * @instance
1355
+ * @type {boolean}
1356
+ * @private
1357
+ * @memberof Meeting
1358
+ */
1359
+ this.retriedWithTurnServer = false;
1360
+
1361
+ /**
1362
+ * Whether or not the media connection has ever successfully connected.
1363
+ * @instance
1364
+ * @type {boolean}
1365
+ * @private
1366
+ * @memberof Meeting
1367
+ */
1368
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
1248
1369
  }
1249
1370
 
1250
1371
  /**
@@ -1278,23 +1399,87 @@ export default class Meeting extends StatelessWebexPlugin {
1278
1399
  }
1279
1400
 
1280
1401
  /**
1281
- * Fetches meeting information.
1282
- * @param {Object} options
1283
- * @param {String} [options.password] optional
1284
- * @param {String} [options.captchaCode] optional
1285
- * @public
1286
- * @memberof Meeting
1287
- * @returns {Promise}
1402
+ * Getter - Returns callStateForMetrics.correlationId
1403
+ * @returns {string}
1288
1404
  */
1289
- public async fetchMeetingInfo({
1290
- password = null,
1291
- captchaCode = null,
1292
- extraParams = {},
1293
- }: {
1294
- password?: string;
1295
- captchaCode?: string;
1296
- extraParams?: Record<string, any>;
1297
- }) {
1405
+ get correlationId() {
1406
+ return this.callStateForMetrics.correlationId;
1407
+ }
1408
+
1409
+ /**
1410
+ * Setter - sets callStateForMetrics.correlationId
1411
+ * @param {string} correlationId
1412
+ */
1413
+ set correlationId(correlationId: string) {
1414
+ this.callStateForMetrics.correlationId = correlationId;
1415
+ }
1416
+
1417
+ /**
1418
+ * Set meeting info and trigger `MEETING_INFO_AVAILABLE` event
1419
+ * @param {any} info
1420
+ * @param {string} [meetingLookupUrl] Lookup url, defined when the meeting info fetched
1421
+ * @returns {void}
1422
+ */
1423
+ private setMeetingInfo(info, meetingLookupUrl) {
1424
+ this.meetingInfo = info ? {...info, meetingLookupUrl} : null;
1425
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1426
+
1427
+ this.requiredCaptcha = null;
1428
+ if (
1429
+ this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
1430
+ this.passwordStatus === PASSWORD_STATUS.VERIFIED
1431
+ ) {
1432
+ this.passwordStatus = PASSWORD_STATUS.VERIFIED;
1433
+ } else {
1434
+ this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1435
+ }
1436
+
1437
+ Trigger.trigger(
1438
+ this,
1439
+ {
1440
+ file: 'meetings',
1441
+ function: 'fetchMeetingInfo',
1442
+ },
1443
+ EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
1444
+ );
1445
+
1446
+ this.updateMeetingActions();
1447
+ }
1448
+
1449
+ /**
1450
+ * Add pre-fetched meeting info
1451
+ *
1452
+ * The passed meeting info should be be complete, e.g.: fetched after password or captcha provided
1453
+ *
1454
+ * @param {Object} meetingInfo - Complete meeting info
1455
+ * @param {FetchMeetingInfoParams} fetchParams - Fetch parameters for validation
1456
+ * @param {String|undefined} meetingLookupUrl - Lookup url, defined when the meeting info fetched
1457
+ * @returns {Promise<void>}
1458
+ */
1459
+ public async injectMeetingInfo(
1460
+ meetingInfo: any,
1461
+ fetchParams: FetchMeetingInfoParams,
1462
+ meetingLookupUrl: string | undefined
1463
+ ): Promise<void> {
1464
+ await this.prepForFetchMeetingInfo(fetchParams, 'injectMeetingInfo');
1465
+
1466
+ this.parseMeetingInfo(meetingInfo, this.destination);
1467
+ this.setMeetingInfo(meetingInfo, meetingLookupUrl);
1468
+ }
1469
+
1470
+ /**
1471
+ * Validate fetch parameters and clear the fetchMeetingInfoTimeout timeout
1472
+ *
1473
+ * @param {FetchMeetingInfoParams} fetchParams - fetch parameters for validation
1474
+ * @param {String} caller - Name of the caller for logging
1475
+ *
1476
+ * @returns {Promise<void>}
1477
+ * @private
1478
+ */
1479
+ private prepForFetchMeetingInfo(
1480
+ {password = null, captchaCode = null, extraParams = {}}: FetchMeetingInfoParams,
1481
+ caller: string
1482
+ ): Promise<void> {
1298
1483
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1299
1484
  if (this.fetchMeetingInfoTimeoutId) {
1300
1485
  clearTimeout(this.fetchMeetingInfoTimeoutId);
@@ -1302,7 +1487,7 @@ export default class Meeting extends StatelessWebexPlugin {
1302
1487
  }
1303
1488
  if (captchaCode && !this.requiredCaptcha) {
1304
1489
  return Promise.reject(
1305
- new Error('fetchMeetingInfo() called with captchaCode when captcha was not required')
1490
+ new Error(`${caller}() called with captchaCode when captcha was not required`)
1306
1491
  );
1307
1492
  }
1308
1493
  if (
@@ -1311,50 +1496,47 @@ export default class Meeting extends StatelessWebexPlugin {
1311
1496
  this.passwordStatus !== PASSWORD_STATUS.UNKNOWN
1312
1497
  ) {
1313
1498
  return Promise.reject(
1314
- new Error('fetchMeetingInfo() called with password when password was not required')
1499
+ new Error(`${caller}() called with password when password was not required`)
1315
1500
  );
1316
1501
  }
1317
1502
 
1503
+ this.meetingInfoExtraParams = cloneDeep(extraParams);
1504
+
1505
+ return Promise.resolve();
1506
+ }
1507
+
1508
+ /**
1509
+ * Internal method for fetching meeting info
1510
+ *
1511
+ * @returns {Promise}
1512
+ */
1513
+ private async fetchMeetingInfoInternal({
1514
+ destination,
1515
+ destinationType,
1516
+ password = null,
1517
+ captchaCode = null,
1518
+ extraParams = {},
1519
+ sendCAevents = false,
1520
+ }): Promise<void> {
1318
1521
  try {
1319
1522
  const captchaInfo = captchaCode
1320
1523
  ? {code: captchaCode, id: this.requiredCaptcha.captchaId}
1321
1524
  : null;
1322
1525
 
1323
1526
  const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(
1324
- this.destination,
1325
- this.destinationType,
1527
+ destination,
1528
+ destinationType,
1326
1529
  password,
1327
1530
  captchaInfo,
1328
1531
  // @ts-ignore - config coming from registerPlugin
1329
1532
  this.config.installedOrgID,
1330
1533
  this.locusId,
1331
1534
  extraParams,
1332
- {meetingId: this.id}
1333
- );
1334
-
1335
- this.parseMeetingInfo(info, this.destination);
1336
- this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1337
- this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1338
- this.requiredCaptcha = null;
1339
- if (
1340
- this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
1341
- this.passwordStatus === PASSWORD_STATUS.VERIFIED
1342
- ) {
1343
- this.passwordStatus = PASSWORD_STATUS.VERIFIED;
1344
- } else {
1345
- this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1346
- }
1347
-
1348
- Trigger.trigger(
1349
- this,
1350
- {
1351
- file: 'meetings',
1352
- function: 'fetchMeetingInfo',
1353
- },
1354
- EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
1535
+ {meetingId: this.id, sendCAevents}
1355
1536
  );
1356
1537
 
1357
- this.updateMeetingActions();
1538
+ this.parseMeetingInfo(info?.body, this.destination, info?.errors);
1539
+ this.setMeetingInfo(info?.body, info?.url);
1358
1540
 
1359
1541
  return Promise.resolve();
1360
1542
  } catch (err) {
@@ -1416,19 +1598,113 @@ export default class Meeting extends StatelessWebexPlugin {
1416
1598
  }
1417
1599
  }
1418
1600
 
1601
+ /**
1602
+ * Refreshes the meeting info permission token (it's required for joining meetings)
1603
+ *
1604
+ * @param {string} [reason] used for metrics and logging purposes (optional)
1605
+ * @returns {Promise}
1606
+ */
1607
+ public async refreshPermissionToken(reason?: string): Promise<void> {
1608
+ if (!this.meetingInfo?.permissionToken) {
1609
+ LoggerProxy.logger.info(
1610
+ `Meeting:index#refreshPermissionToken --> cannot refresh the permission token, because we don't have it (reason=${reason})`
1611
+ );
1612
+
1613
+ return;
1614
+ }
1615
+
1616
+ const isStartingSpaceInstantV2Meeting =
1617
+ this.destinationType === _CONVERSATION_URL_ &&
1618
+ // @ts-ignore - config coming from registerPlugin
1619
+ this.config.experimental.enableAdhocMeetings &&
1620
+ // @ts-ignore
1621
+ this.webex.meetings.preferredWebexSite;
1622
+
1623
+ const destination = isStartingSpaceInstantV2Meeting
1624
+ ? this.meetingInfo.meetingJoinUrl
1625
+ : this.destination;
1626
+ const destinationType = isStartingSpaceInstantV2Meeting ? _MEETING_LINK_ : this.destinationType;
1627
+
1628
+ const permissionTokenExpiryInfo = this.getPermissionTokenExpiryInfo();
1629
+
1630
+ const timeLeft = permissionTokenExpiryInfo?.timeLeft;
1631
+ const expiryTime = permissionTokenExpiryInfo?.expiryTime;
1632
+ const currentTime = permissionTokenExpiryInfo?.currentTime;
1633
+
1634
+ LoggerProxy.logger.info(
1635
+ `Meeting:index#refreshPermissionToken --> refreshing permission token, destinationType=${destinationType}, timeLeft=${timeLeft}, permissionTokenExpiry=${expiryTime}, currentTimestamp=${currentTime},reason=${reason}`
1636
+ );
1637
+
1638
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
1639
+ correlationId: this.correlationId,
1640
+ timeLeft,
1641
+ expiryTime,
1642
+ currentTime,
1643
+ reason,
1644
+ destinationType,
1645
+ });
1646
+
1647
+ try {
1648
+ await this.fetchMeetingInfoInternal({
1649
+ destination,
1650
+ destinationType,
1651
+ extraParams: {
1652
+ ...this.meetingInfoExtraParams,
1653
+ permissionToken: this.meetingInfo.permissionToken,
1654
+ },
1655
+ sendCAevents: true, // because if we're refreshing the permissionToken, it means that user is intending to join that meeting, so we want CA events
1656
+ });
1657
+ } catch (error) {
1658
+ LoggerProxy.logger.info(
1659
+ 'Meeting:index#refreshPermissionToken --> failed to refresh the permission token:',
1660
+ error
1661
+ );
1662
+
1663
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH_ERROR, {
1664
+ correlationId: this.correlationId,
1665
+ reason: error.message,
1666
+ stack: error.stack,
1667
+ });
1668
+
1669
+ throw error;
1670
+ }
1671
+ }
1672
+
1673
+ /**
1674
+ * Fetches meeting information.
1675
+ * @param {Object} options
1676
+ * @param {String} [options.password] optional
1677
+ * @param {String} [options.captchaCode] optional
1678
+ * @param {Boolean} [options.sendCAevents] optional - Whether to submit Call Analyzer events or not. Default: false.
1679
+ * @public
1680
+ * @memberof Meeting
1681
+ * @returns {Promise}
1682
+ */
1683
+ public async fetchMeetingInfo(options: FetchMeetingInfoParams) {
1684
+ await this.prepForFetchMeetingInfo(options, 'fetchMeetingInfo');
1685
+
1686
+ return this.fetchMeetingInfoInternal({
1687
+ destination: this.destination,
1688
+ destinationType: this.destinationType,
1689
+ ...options,
1690
+ });
1691
+ }
1692
+
1419
1693
  /**
1420
1694
  * Checks if the supplied password/host key is correct. It returns a promise with information whether the
1421
1695
  * password and captcha code were correct or not.
1422
1696
  * @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
1423
1697
  * @param {String} captchaCode - can be undefined if captcha was not required by the server
1698
+ * @param {Boolean} sendCAevents - whether Call Analyzer events should be sent when fetching meeting information
1424
1699
  * @public
1425
1700
  * @memberof Meeting
1426
1701
  * @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
1427
1702
  */
1428
- public verifyPassword(password: string, captchaCode: string) {
1703
+ public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
1429
1704
  return this.fetchMeetingInfo({
1430
1705
  password,
1431
1706
  captchaCode,
1707
+ sendCAevents,
1432
1708
  })
1433
1709
  .then(() => {
1434
1710
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
@@ -1759,12 +2035,12 @@ export default class Meeting extends StatelessWebexPlugin {
1759
2035
 
1760
2036
  /**
1761
2037
  * sets the network status on meeting object
1762
- * @param {String} networkStatus
2038
+ * @param {NETWORK_STATUS} networkStatus
1763
2039
  * @private
1764
2040
  * @returns {undefined}
1765
2041
  * @memberof Meeting
1766
2042
  */
1767
- private setNetworkStatus(networkStatus: string) {
2043
+ private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
1768
2044
  if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
1769
2045
  Trigger.trigger(
1770
2046
  this,
@@ -2019,16 +2295,6 @@ export default class Meeting extends StatelessWebexPlugin {
2019
2295
  }
2020
2296
  );
2021
2297
 
2022
- this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2023
- this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2024
- // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2025
- // which means main session is not active for the attendee
2026
- if (error?.statusCode === 403) {
2027
- this.locusInfo.clearMainSessionLocusCache();
2028
- }
2029
- });
2030
- });
2031
-
2032
2298
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
2033
2299
  Trigger.trigger(
2034
2300
  this,
@@ -2152,6 +2418,7 @@ export default class Meeting extends StatelessWebexPlugin {
2152
2418
  if (
2153
2419
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2154
2420
  contentShare.disposition === previousContentShare?.disposition &&
2421
+ contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
2155
2422
  whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
2156
2423
  whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
2157
2424
  whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
@@ -2174,11 +2441,21 @@ export default class Meeting extends StatelessWebexPlugin {
2174
2441
  // LOCAL - check if we started sharing content
2175
2442
  else if (
2176
2443
  this.selfId === contentShare.beneficiaryId &&
2177
- contentShare.disposition === FLOOR_ACTION.GRANTED
2444
+ contentShare.disposition === FLOOR_ACTION.GRANTED &&
2445
+ contentShare.deviceUrlSharing === this.deviceUrl
2178
2446
  ) {
2179
2447
  // CONTENT - sharing content local
2180
2448
  newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2181
2449
  }
2450
+ // SAME USER REMOTE - check if same user started sharing content from another client
2451
+ else if (
2452
+ this.selfId === contentShare.beneficiaryId &&
2453
+ contentShare.disposition === FLOOR_ACTION.GRANTED &&
2454
+ contentShare.deviceUrlSharing !== this.deviceUrl
2455
+ ) {
2456
+ // CONTENT - same user sharing content remote
2457
+ newShareStatus = SHARE_STATUS.REMOTE_SHARE_ACTIVE;
2458
+ }
2182
2459
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
2183
2460
  // There is no concept of local/remote share for whiteboard
2184
2461
  // It does not matter who requested to share the whiteboard, everyone gets the same view
@@ -2252,6 +2529,8 @@ export default class Meeting extends StatelessWebexPlugin {
2252
2529
  switch (newShareStatus) {
2253
2530
  case SHARE_STATUS.REMOTE_SHARE_ACTIVE: {
2254
2531
  const sendStartedSharingRemote = () => {
2532
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2533
+
2255
2534
  Trigger.trigger(
2256
2535
  this,
2257
2536
  {
@@ -2262,7 +2541,7 @@ export default class Meeting extends StatelessWebexPlugin {
2262
2541
  {
2263
2542
  memberId: contentShare.beneficiaryId,
2264
2543
  url: contentShare.url,
2265
- shareInstanceId: contentShare.shareInstanceId,
2544
+ shareInstanceId: this.remoteShareInstanceId,
2266
2545
  annotationInfo: contentShare.annotation,
2267
2546
  }
2268
2547
  );
@@ -2299,6 +2578,7 @@ export default class Meeting extends StatelessWebexPlugin {
2299
2578
  name: 'client.share.floor-granted.local',
2300
2579
  payload: {
2301
2580
  mediaType: 'share',
2581
+ shareInstanceId: this.localShareInstanceId,
2302
2582
  },
2303
2583
  options: {meetingId: this.id},
2304
2584
  });
@@ -2341,6 +2621,8 @@ export default class Meeting extends StatelessWebexPlugin {
2341
2621
  } else if (newShareStatus === SHARE_STATUS.REMOTE_SHARE_ACTIVE) {
2342
2622
  // if we got here, then some remote participant has stolen
2343
2623
  // the presentation from another remote participant
2624
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2625
+
2344
2626
  Trigger.trigger(
2345
2627
  this,
2346
2628
  {
@@ -2351,7 +2633,7 @@ export default class Meeting extends StatelessWebexPlugin {
2351
2633
  {
2352
2634
  memberId: contentShare.beneficiaryId,
2353
2635
  url: contentShare.url,
2354
- shareInstanceId: contentShare.shareInstanceId,
2636
+ shareInstanceId: this.remoteShareInstanceId,
2355
2637
  annotationInfo: contentShare.annotation,
2356
2638
  }
2357
2639
  );
@@ -2403,6 +2685,7 @@ export default class Meeting extends StatelessWebexPlugin {
2403
2685
  this.locusId = this.locusUrl?.split('/').pop();
2404
2686
  this.recordingController.setLocusUrl(this.locusUrl);
2405
2687
  this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2688
+ this.webinar.locusUrlUpdate(payload);
2406
2689
 
2407
2690
  Trigger.trigger(
2408
2691
  this,
@@ -2432,6 +2715,10 @@ export default class Meeting extends StatelessWebexPlugin {
2432
2715
  this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2433
2716
  this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2434
2717
  this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url);
2718
+ this.webinar.webcastUrlUpdate(payload?.services?.webcast?.url);
2719
+ this.webinar.webinarAttendeesSearchingUrlUpdate(
2720
+ payload?.services?.webinarAttendeesSearching?.url
2721
+ );
2435
2722
  });
2436
2723
  }
2437
2724
 
@@ -2472,12 +2759,24 @@ export default class Meeting extends StatelessWebexPlugin {
2472
2759
  );
2473
2760
  }
2474
2761
  });
2475
- this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, () => {
2762
+ this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, ({isInitializing}) => {
2476
2763
  this.updateMeetingActions();
2477
2764
  this.recordingController.setDisplayHints(this.userDisplayHints);
2478
2765
  this.recordingController.setUserPolicy(this.selfUserPolicies);
2479
2766
  this.controlsOptionsManager.setDisplayHints(this.userDisplayHints);
2480
2767
  this.handleDataChannelUrlChange(this.datachannelUrl);
2768
+
2769
+ if (!isInitializing) {
2770
+ // send updated trigger only if locus is not initializing the meeting
2771
+ Trigger.trigger(
2772
+ this,
2773
+ {
2774
+ file: 'meetings',
2775
+ function: 'setUpLocusInfoMeetingInfoListener',
2776
+ },
2777
+ EVENT_TRIGGERS.MEETING_INFO_UPDATED
2778
+ );
2779
+ }
2481
2780
  });
2482
2781
  }
2483
2782
 
@@ -2703,7 +3002,7 @@ export default class Meeting extends StatelessWebexPlugin {
2703
3002
  });
2704
3003
 
2705
3004
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
2706
- this.simultaneousInterpretation.updateSelfInterpretation(payload);
3005
+ const targetChanged = this.simultaneousInterpretation.updateSelfInterpretation(payload);
2707
3006
  Trigger.trigger(
2708
3007
  this,
2709
3008
  {
@@ -2712,6 +3011,9 @@ export default class Meeting extends StatelessWebexPlugin {
2712
3011
  },
2713
3012
  EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
2714
3013
  );
3014
+ if (targetChanged && this.mediaProperties.audioStream) {
3015
+ this.setSendNamedMediaGroup(MediaType.AudioMain);
3016
+ }
2715
3017
  });
2716
3018
 
2717
3019
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
@@ -2722,6 +3024,7 @@ export default class Meeting extends StatelessWebexPlugin {
2722
3024
  this.simultaneousInterpretation.updateCanManageInterpreters(
2723
3025
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
2724
3026
  );
3027
+ this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR));
2725
3028
  Trigger.trigger(
2726
3029
  this,
2727
3030
  {
@@ -2963,30 +3266,40 @@ export default class Meeting extends StatelessWebexPlugin {
2963
3266
  /**
2964
3267
  * Sets the meeting info on the class instance
2965
3268
  * @param {Object} meetingInfo
2966
- * @param {Object} meetingInfo.body
2967
- * @param {String} meetingInfo.body.conversationUrl
2968
- * @param {String} meetingInfo.body.locusUrl
2969
- * @param {String} meetingInfo.body.sipUri
2970
- * @param {Object} meetingInfo.body.owner
3269
+ * @param {String} meetingInfo.conversationUrl
3270
+ * @param {String} meetingInfo.locusUrl
3271
+ * @param {String} meetingInfo.sipUri
3272
+ * @param {String} [meetingInfo.sipUrl]
3273
+ * @param {String} [meetingInfo.sipMeetingUri]
3274
+ * @param {String} [meetingInfo.meetingNumber]
3275
+ * @param {String} [meetingInfo.meetingJoinUrl]
3276
+ * @param {String} [meetingInfo.hostId]
3277
+ * @param {String} [meetingInfo.permissionToken]
3278
+ * @param {String} [meetingInfo.channel]
3279
+ * @param {Object} meetingInfo.owner
2971
3280
  * @param {Object | String} destination locus object with meeting data or destination string (sip url, meeting link, etc)
3281
+ * @param {Object | String} errors Meeting info request error
2972
3282
  * @returns {undefined}
2973
3283
  * @private
2974
3284
  * @memberof Meeting
2975
3285
  */
2976
3286
  parseMeetingInfo(
2977
- meetingInfo:
2978
- | {
2979
- body: {
2980
- conversationUrl: string;
2981
- locusUrl: string;
2982
- sipUri: string;
2983
- owner: object;
2984
- };
2985
- }
2986
- | any,
2987
- destination: object | string | null = null
3287
+ meetingInfo: {
3288
+ conversationUrl: string;
3289
+ locusUrl: string;
3290
+ sipUri: string;
3291
+ owner: object;
3292
+ sipUrl?: string;
3293
+ sipMeetingUri?: string;
3294
+ meetingNumber?: string;
3295
+ meetingJoinUrl?: string;
3296
+ hostId?: string;
3297
+ permissionToken?: string;
3298
+ channel?: string;
3299
+ },
3300
+ destination: object | string | null = null,
3301
+ errors: any = undefined
2988
3302
  ) {
2989
- const webexMeetingInfo = meetingInfo?.body;
2990
3303
  // We try to use as much info from Locus meeting object, stored in destination
2991
3304
 
2992
3305
  let locusMeetingObject;
@@ -2996,39 +3309,31 @@ export default class Meeting extends StatelessWebexPlugin {
2996
3309
  }
2997
3310
 
2998
3311
  // MeetingInfo will be undefined for 1:1 calls
2999
- if (
3000
- locusMeetingObject ||
3001
- (webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))
3002
- ) {
3312
+ if (locusMeetingObject || (meetingInfo && !(errors?.length > 0))) {
3003
3313
  this.conversationUrl =
3004
- locusMeetingObject?.conversationUrl ||
3005
- webexMeetingInfo?.conversationUrl ||
3006
- this.conversationUrl;
3007
- this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
3314
+ locusMeetingObject?.conversationUrl || meetingInfo?.conversationUrl || this.conversationUrl;
3315
+ this.locusUrl = locusMeetingObject?.url || meetingInfo?.locusUrl || this.locusUrl;
3008
3316
  // @ts-ignore - config coming from registerPlugin
3009
3317
  this.setSipUri(
3010
3318
  // @ts-ignore
3011
3319
  this.config.experimental.enableUnifiedMeetings
3012
- ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
3013
- : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
3320
+ ? locusMeetingObject?.info.sipUri || meetingInfo?.sipUrl
3321
+ : locusMeetingObject?.info.sipUri || meetingInfo?.sipMeetingUri || this.sipUri
3014
3322
  );
3015
3323
  // @ts-ignore - config coming from registerPlugin
3016
3324
  if (this.config.experimental.enableUnifiedMeetings) {
3017
- this.meetingNumber =
3018
- locusMeetingObject?.info.webExMeetingId || webexMeetingInfo?.meetingNumber;
3019
- this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
3325
+ this.meetingNumber = locusMeetingObject?.info.webExMeetingId || meetingInfo?.meetingNumber;
3326
+ this.meetingJoinUrl = meetingInfo?.meetingJoinUrl;
3020
3327
  }
3021
3328
  this.owner =
3022
- locusMeetingObject?.info.owner ||
3023
- webexMeetingInfo?.owner ||
3024
- webexMeetingInfo?.hostId ||
3025
- this.owner;
3026
- this.permissionToken = webexMeetingInfo?.permissionToken;
3027
- this.setSelfUserPolicies(this.permissionToken);
3329
+ locusMeetingObject?.info.owner || meetingInfo?.owner || meetingInfo?.hostId || this.owner;
3330
+ this.permissionToken = meetingInfo?.permissionToken;
3331
+ this.setPermissionTokenPayload(meetingInfo?.permissionToken);
3332
+ this.setSelfUserPolicies();
3028
3333
  // Need to populate environment when sending CA event
3029
- this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
3334
+ this.environment = locusMeetingObject?.info.channel || meetingInfo?.channel;
3030
3335
  }
3031
- MeetingUtil.parseInterpretationInfo(this, webexMeetingInfo);
3336
+ MeetingUtil.parseInterpretationInfo(this, meetingInfo);
3032
3337
  }
3033
3338
 
3034
3339
  /**
@@ -3080,6 +3385,11 @@ export default class Meeting extends StatelessWebexPlugin {
3080
3385
  }) &&
3081
3386
  this.meetingInfo?.video?.supportHDV) ||
3082
3387
  !this.arePolicyRestrictionsSupported(),
3388
+ enforceVirtualBackground:
3389
+ ControlsOptionsUtil.hasPolicies({
3390
+ requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
3391
+ policies: this.selfUserPolicies,
3392
+ }) && this.arePolicyRestrictionsSupported(),
3083
3393
  supportHQV:
3084
3394
  (ControlsOptionsUtil.hasPolicies({
3085
3395
  requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
@@ -3233,6 +3543,10 @@ export default class Meeting extends StatelessWebexPlugin {
3233
3543
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
3234
3544
  policies: this.selfUserPolicies,
3235
3545
  }),
3546
+ canChat: ControlsOptionsUtil.hasPolicies({
3547
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
3548
+ policies: this.selfUserPolicies,
3549
+ }),
3236
3550
  canShareApplication:
3237
3551
  (ControlsOptionsUtil.hasHints({
3238
3552
  requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
@@ -3288,11 +3602,22 @@ export default class Meeting extends StatelessWebexPlugin {
3288
3602
 
3289
3603
  /**
3290
3604
  * Sets the self user policies based on the contents of the permission token
3605
+ * @returns {void}
3606
+ */
3607
+ setSelfUserPolicies() {
3608
+ this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
3609
+ this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
3610
+ }
3611
+
3612
+ /**
3613
+ * Sets the permission token payload on the class instance
3614
+ *
3291
3615
  * @param {String} permissionToken
3292
3616
  * @returns {void}
3293
3617
  */
3294
- setSelfUserPolicies(permissionToken: string) {
3295
- this.selfUserPolicies = jwt.decode(permissionToken)?.permission?.userPolicies;
3618
+ public setPermissionTokenPayload(permissionToken: string) {
3619
+ this.permissionTokenPayload = jwt.decode(permissionToken);
3620
+ this.permissionTokenReceivedLocalTime = new Date().getTime();
3296
3621
  }
3297
3622
 
3298
3623
  /**
@@ -3386,8 +3711,7 @@ export default class Meeting extends StatelessWebexPlugin {
3386
3711
  * @memberof Meeting
3387
3712
  */
3388
3713
  closeRemoteStreams() {
3389
- const {remoteAudioStream, remoteVideoStream, remoteShareStream, shareAudioStream} =
3390
- this.mediaProperties;
3714
+ const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
3391
3715
 
3392
3716
  /**
3393
3717
  * Triggers an event to the developer
@@ -3428,7 +3752,6 @@ export default class Meeting extends StatelessWebexPlugin {
3428
3752
  stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
3429
3753
  stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
3430
3754
  stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
3431
- stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
3432
3755
  ]);
3433
3756
  }
3434
3757
 
@@ -3442,7 +3765,14 @@ export default class Meeting extends StatelessWebexPlugin {
3442
3765
  private async setLocalAudioStream(localStream?: LocalMicrophoneStream) {
3443
3766
  const oldStream = this.mediaProperties.audioStream;
3444
3767
 
3445
- oldStream?.off(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3768
+ oldStream?.off(
3769
+ LocalStreamEventNames.UserMuteStateChange,
3770
+ this.localAudioStreamMuteStateHandler
3771
+ );
3772
+ oldStream?.off(
3773
+ LocalStreamEventNames.SystemMuteStateChange,
3774
+ this.localAudioStreamMuteStateHandler
3775
+ );
3446
3776
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3447
3777
 
3448
3778
  // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
@@ -3450,7 +3780,14 @@ export default class Meeting extends StatelessWebexPlugin {
3450
3780
 
3451
3781
  this.audio.handleLocalStreamChange(this);
3452
3782
 
3453
- localStream?.on(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3783
+ localStream?.on(
3784
+ LocalStreamEventNames.UserMuteStateChange,
3785
+ this.localAudioStreamMuteStateHandler
3786
+ );
3787
+ localStream?.on(
3788
+ LocalStreamEventNames.SystemMuteStateChange,
3789
+ this.localAudioStreamMuteStateHandler
3790
+ );
3454
3791
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3455
3792
 
3456
3793
  if (!this.isMultistream || !localStream) {
@@ -3470,7 +3807,14 @@ export default class Meeting extends StatelessWebexPlugin {
3470
3807
  private async setLocalVideoStream(localStream?: LocalCameraStream) {
3471
3808
  const oldStream = this.mediaProperties.videoStream;
3472
3809
 
3473
- oldStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3810
+ oldStream?.off(
3811
+ LocalStreamEventNames.UserMuteStateChange,
3812
+ this.localVideoStreamMuteStateHandler
3813
+ );
3814
+ oldStream?.off(
3815
+ LocalStreamEventNames.SystemMuteStateChange,
3816
+ this.localVideoStreamMuteStateHandler
3817
+ );
3474
3818
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3475
3819
 
3476
3820
  // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
@@ -3478,7 +3822,14 @@ export default class Meeting extends StatelessWebexPlugin {
3478
3822
 
3479
3823
  this.video.handleLocalStreamChange(this);
3480
3824
 
3481
- localStream?.on(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3825
+ localStream?.on(
3826
+ LocalStreamEventNames.UserMuteStateChange,
3827
+ this.localVideoStreamMuteStateHandler
3828
+ );
3829
+ localStream?.on(
3830
+ LocalStreamEventNames.SystemMuteStateChange,
3831
+ this.localVideoStreamMuteStateHandler
3832
+ );
3482
3833
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3483
3834
 
3484
3835
  if (!this.isMultistream || !localStream) {
@@ -3499,11 +3850,19 @@ export default class Meeting extends StatelessWebexPlugin {
3499
3850
  private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3500
3851
  const oldStream = this.mediaProperties.shareVideoStream;
3501
3852
 
3853
+ oldStream?.off(
3854
+ LocalStreamEventNames.SystemMuteStateChange,
3855
+ this.handleShareVideoStreamMuteStateChange
3856
+ );
3502
3857
  oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3503
3858
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3504
3859
 
3505
3860
  this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3506
3861
 
3862
+ localDisplayStream?.on(
3863
+ LocalStreamEventNames.SystemMuteStateChange,
3864
+ this.handleShareVideoStreamMuteStateChange
3865
+ );
3507
3866
  localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3508
3867
  localDisplayStream?.on(
3509
3868
  LocalStreamEventNames.OutputTrackChange,
@@ -3559,7 +3918,7 @@ export default class Meeting extends StatelessWebexPlugin {
3559
3918
  functionName: string;
3560
3919
  isPublished: boolean;
3561
3920
  mediaType: MediaType;
3562
- stream: MediaStream;
3921
+ stream: LocalStream;
3563
3922
  }) {
3564
3923
  const {functionName, isPublished, mediaType, stream} = options;
3565
3924
  Trigger.trigger(
@@ -3587,18 +3946,37 @@ export default class Meeting extends StatelessWebexPlugin {
3587
3946
  public cleanupLocalStreams() {
3588
3947
  const {audioStream, videoStream, shareAudioStream, shareVideoStream} = this.mediaProperties;
3589
3948
 
3590
- audioStream?.off(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3949
+ audioStream?.off(
3950
+ LocalStreamEventNames.UserMuteStateChange,
3951
+ this.localAudioStreamMuteStateHandler
3952
+ );
3953
+ audioStream?.off(
3954
+ LocalStreamEventNames.SystemMuteStateChange,
3955
+ this.localAudioStreamMuteStateHandler
3956
+ );
3591
3957
  audioStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3592
3958
 
3593
- videoStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3959
+ videoStream?.off(
3960
+ LocalStreamEventNames.UserMuteStateChange,
3961
+ this.localVideoStreamMuteStateHandler
3962
+ );
3963
+ videoStream?.off(
3964
+ LocalStreamEventNames.SystemMuteStateChange,
3965
+ this.localVideoStreamMuteStateHandler
3966
+ );
3594
3967
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3595
3968
 
3596
- shareAudioStream?.off(StreamEventNames.MuteStateChange, this.handleShareAudioStreamEnded);
3969
+ shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3597
3970
  shareAudioStream?.off(
3598
3971
  LocalStreamEventNames.OutputTrackChange,
3599
3972
  this.localOutputTrackChangeHandler
3600
3973
  );
3601
- shareVideoStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamEnded);
3974
+
3975
+ shareVideoStream?.off(
3976
+ LocalStreamEventNames.SystemMuteStateChange,
3977
+ this.handleShareVideoStreamMuteStateChange
3978
+ );
3979
+ shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3602
3980
  shareVideoStream?.off(
3603
3981
  LocalStreamEventNames.OutputTrackChange,
3604
3982
  this.localOutputTrackChangeHandler
@@ -3712,6 +4090,7 @@ export default class Meeting extends StatelessWebexPlugin {
3712
4090
  this.receiveSlotManager.reset();
3713
4091
  this.mediaProperties.webrtcMediaConnection.close();
3714
4092
  this.sendSlotManager.reset();
4093
+ this.setNetworkStatus(undefined);
3715
4094
  }
3716
4095
 
3717
4096
  this.audio = null;
@@ -3733,18 +4112,31 @@ export default class Meeting extends StatelessWebexPlugin {
3733
4112
  if (this.config.reconnection.detection) {
3734
4113
  // @ts-ignore
3735
4114
  this.webex.internal.mercury.off(ONLINE);
4115
+ // @ts-ignore
4116
+ this.webex.internal.mercury.off(OFFLINE);
3736
4117
  }
3737
4118
  }
3738
4119
 
3739
4120
  /**
3740
- * Convenience method to set the correlation id for the Meeting
3741
- * @param {String} id correlation id to set on the class
4121
+ * Convenience method to set the correlation id for the callStateForMetrics
4122
+ * @param {String} id correlation id to set on the callStateForMetrics
3742
4123
  * @returns {undefined}
3743
- * @private
4124
+ * @public
4125
+ * @memberof Meeting
4126
+ */
4127
+ public setCorrelationId(id: string) {
4128
+ this.callStateForMetrics.correlationId = id;
4129
+ }
4130
+
4131
+ /**
4132
+ * Update the callStateForMetrics
4133
+ * @param {CallStateForMetrics} callStateForMetrics updated values for callStateForMetrics
4134
+ * @returns {undefined}
4135
+ * @public
3744
4136
  * @memberof Meeting
3745
4137
  */
3746
- private setCorrelationId(id: string) {
3747
- this.correlationId = id;
4138
+ public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
4139
+ this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
3748
4140
  }
3749
4141
 
3750
4142
  /**
@@ -3981,9 +4373,17 @@ export default class Meeting extends StatelessWebexPlugin {
3981
4373
  ) {
3982
4374
  const {mediaOptions, joinOptions} = options;
3983
4375
 
3984
- return this.join(joinOptions)
3985
- .then((joinResponse) =>
3986
- this.addMedia(mediaOptions).then((mediaResponse) => ({
4376
+ if (!mediaOptions?.allowMediaInLobby) {
4377
+ return Promise.reject(
4378
+ new ParameterError('joinWithMedia() can only be used with allowMediaInLobby set to true')
4379
+ );
4380
+ }
4381
+
4382
+ LoggerProxy.logger.info('Meeting:index#joinWithMedia called');
4383
+
4384
+ return this.join(joinOptions)
4385
+ .then((joinResponse) =>
4386
+ this.addMedia(mediaOptions).then((mediaResponse) => ({
3987
4387
  join: joinResponse,
3988
4388
  media: mediaResponse,
3989
4389
  }))
@@ -4062,6 +4462,8 @@ export default class Meeting extends StatelessWebexPlugin {
4062
4462
 
4063
4463
  return this.reconnectionManager
4064
4464
  .reconnect(options)
4465
+ .then(() => this.waitForRemoteSDPAnswer())
4466
+ .then(() => this.waitForMediaConnectionConnected())
4065
4467
  .then(() => {
4066
4468
  Trigger.trigger(
4067
4469
  this,
@@ -4072,6 +4474,18 @@ export default class Meeting extends StatelessWebexPlugin {
4072
4474
  EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
4073
4475
  );
4074
4476
  LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
4477
+
4478
+ // @ts-ignore
4479
+ this.webex.internal.newMetrics.submitClientEvent({
4480
+ name: 'client.media.recovered',
4481
+ payload: {
4482
+ recoveredBy: 'new',
4483
+ },
4484
+ options: {
4485
+ meetingId: this.id,
4486
+ },
4487
+ });
4488
+ this.reconnectionManager.setStatus(RECONNECTION.STATE.COMPLETE);
4075
4489
  })
4076
4490
  .catch((error) => {
4077
4491
  Trigger.trigger(
@@ -4327,7 +4741,7 @@ export default class Meeting extends StatelessWebexPlugin {
4327
4741
  * if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
4328
4742
  * Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
4329
4743
  */
4330
- public join(options: any = {}) {
4744
+ public async join(options: any = {}) {
4331
4745
  // @ts-ignore - fix type
4332
4746
  if (!this.webex.meetings.registered) {
4333
4747
  const errorMessage = 'Meeting:index#join --> Device not registered';
@@ -4381,27 +4795,14 @@ export default class Meeting extends StatelessWebexPlugin {
4381
4795
  // @ts-ignore
4382
4796
  this.webex.internal.newMetrics.submitClientEvent({
4383
4797
  name: 'client.call.initiated',
4384
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
4798
+ payload: {
4799
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
4800
+ isRoapCallEnabled: true,
4801
+ pstnAudioType: options?.pstnAudioType,
4802
+ },
4385
4803
  options: {meetingId: this.id},
4386
4804
  });
4387
4805
 
4388
- if (!isEmpty(this.meetingInfo)) {
4389
- // @ts-ignore
4390
- this.webex.internal.newMetrics.submitClientEvent({
4391
- name: 'client.meetinginfo.request',
4392
- options: {meetingId: this.id},
4393
- });
4394
-
4395
- // @ts-ignore
4396
- this.webex.internal.newMetrics.submitClientEvent({
4397
- name: 'client.meetinginfo.response',
4398
- payload: {
4399
- identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
4400
- },
4401
- options: {meetingId: this.id},
4402
- });
4403
- }
4404
-
4405
4806
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4406
4807
 
4407
4808
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -4455,44 +4856,55 @@ export default class Meeting extends StatelessWebexPlugin {
4455
4856
 
4456
4857
  this.isMultistream = !!options.enableMultistream;
4457
4858
 
4859
+ try {
4860
+ // refresh the permission token if its about to expire in 10sec
4861
+ await this.checkAndRefreshPermissionToken(
4862
+ MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
4863
+ MEETING_PERMISSION_TOKEN_REFRESH_REASON
4864
+ );
4865
+ } catch (error) {
4866
+ LoggerProxy.logger.error('Meeting:index#join --> Failed to refresh permission token:', error);
4867
+
4868
+ if (
4869
+ error instanceof CaptchaError ||
4870
+ error instanceof PasswordError ||
4871
+ error instanceof PermissionError
4872
+ ) {
4873
+ this.meetingFiniteStateMachine.fail(error);
4874
+
4875
+ // Upload logs on refreshpermissionToken refresh Failure
4876
+ Trigger.trigger(
4877
+ this,
4878
+ {
4879
+ file: 'meeting/index',
4880
+ function: 'join',
4881
+ },
4882
+ EVENTS.REQUEST_UPLOAD_LOGS,
4883
+ this
4884
+ );
4885
+
4886
+ joinFailed(error);
4887
+
4888
+ this.deferJoin = undefined;
4889
+
4890
+ // if refresh permission token requires captcha, password or permission, we are throwing the errors
4891
+ // and bubble it up to client
4892
+ return Promise.reject(error);
4893
+ }
4894
+ }
4895
+
4458
4896
  return MeetingUtil.joinMeetingOptions(this, options)
4459
4897
  .then((join) => {
4460
4898
  this.meetingFiniteStateMachine.join();
4461
4899
  LoggerProxy.logger.log('Meeting:index#join --> Success');
4462
4900
 
4463
- return join;
4464
- })
4465
- .then((join) => {
4466
- joinSuccess(join);
4467
- this.deferJoin = undefined;
4468
4901
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
4469
4902
  correlation_id: this.correlationId,
4470
4903
  });
4471
4904
 
4472
- return join;
4473
- })
4474
- .then(async (join) => {
4475
- // @ts-ignore - config coming from registerPlugin
4476
- if (this.config.enableAutomaticLLM) {
4477
- await this.updateLLMConnection();
4478
- }
4905
+ joinSuccess(join);
4479
4906
 
4480
- return join;
4481
- })
4482
- .then(async (join) => {
4483
- if (isBrowser) {
4484
- // @ts-ignore - config coming from registerPlugin
4485
- if (this.config.receiveTranscription || options.receiveTranscription) {
4486
- if (this.isTranscriptionSupported()) {
4487
- await this.receiveTranscription();
4488
- LoggerProxy.logger.info('Meeting:index#join --> enabled to recieve transcription!');
4489
- }
4490
- }
4491
- } else {
4492
- LoggerProxy.logger.error(
4493
- 'Meeting:index#join --> Receving transcription is not supported on this platform'
4494
- );
4495
- }
4907
+ this.deferJoin = undefined;
4496
4908
 
4497
4909
  return join;
4498
4910
  })
@@ -4528,9 +4940,59 @@ export default class Meeting extends StatelessWebexPlugin {
4528
4940
  );
4529
4941
 
4530
4942
  joinFailed(error);
4943
+
4531
4944
  this.deferJoin = undefined;
4532
4945
 
4533
4946
  return Promise.reject(error);
4947
+ })
4948
+ .then((join) => {
4949
+ // @ts-ignore - config coming from registerPlugin
4950
+ if (this.config.enableAutomaticLLM) {
4951
+ this.updateLLMConnection().catch((error) => {
4952
+ LoggerProxy.logger.error('Meeting:index#join --> Update LLM Connection Failed', error);
4953
+
4954
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LLM_CONNECTION_AFTER_JOIN_FAILURE, {
4955
+ correlation_id: this.correlationId,
4956
+ reason: error?.message,
4957
+ stack: error.stack,
4958
+ });
4959
+ });
4960
+ }
4961
+
4962
+ return join;
4963
+ })
4964
+ .then((join) => {
4965
+ if (isBrowser) {
4966
+ // @ts-ignore - config coming from registerPlugin
4967
+ if (this.config.receiveTranscription || options.receiveTranscription) {
4968
+ if (this.isTranscriptionSupported()) {
4969
+ LoggerProxy.logger.info(
4970
+ 'Meeting:index#join --> Attempting to enabled to receive transcription!'
4971
+ );
4972
+ this.receiveTranscription().catch((error) => {
4973
+ LoggerProxy.logger.error(
4974
+ 'Meeting:index#join --> Receive Transcription Failed',
4975
+ error
4976
+ );
4977
+
4978
+ Metrics.sendBehavioralMetric(
4979
+ BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_AFTER_JOIN_FAILURE,
4980
+ {
4981
+ correlation_id: this.correlationId,
4982
+ reason: error?.message,
4983
+ stack: error.stack,
4984
+ }
4985
+ );
4986
+ });
4987
+ }
4988
+ }
4989
+ } else {
4990
+ LoggerProxy.logger.error(
4991
+ 'Meeting:index#join --> Receving transcription is not supported on this platform'
4992
+ );
4993
+ }
4994
+
4995
+ return join;
4534
4996
  });
4535
4997
  }
4536
4998
 
@@ -4910,7 +5372,74 @@ export default class Meeting extends StatelessWebexPlugin {
4910
5372
  }
4911
5373
  };
4912
5374
 
5375
+ /**
5376
+ * Handles an incoming Roap message
5377
+ * @internal
5378
+ * @param {RoapMessage} roapMessage roap message
5379
+ * @returns {undefined}
5380
+ */
5381
+ public roapMessageReceived = (roapMessage: RoapMessage) => {
5382
+ const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5383
+
5384
+ this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5385
+
5386
+ if (mediaServer) {
5387
+ this.mediaProperties.webrtcMediaConnection.mediaServer = mediaServer;
5388
+ }
5389
+ };
5390
+
5391
+ /**
5392
+ * This function makes sure we send the right metrics when local and remote SDPs are processed/generated
5393
+ *
5394
+ * @returns {undefined}
5395
+ */
5396
+ setupSdpListeners = () => {
5397
+ this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_SDP_ANSWER_PROCESSED, () => {
5398
+ // @ts-ignore
5399
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5400
+
5401
+ // @ts-ignore
5402
+ this.webex.internal.newMetrics.submitClientEvent({
5403
+ name: 'client.media-engine.remote-sdp-received',
5404
+ options: {meetingId: this.id},
5405
+ });
5406
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, {
5407
+ correlation_id: this.correlationId,
5408
+ latency: cdl.getLocalSDPGenRemoteSDPRecv(),
5409
+ meetingId: this.id,
5410
+ });
5411
+
5412
+ if (this.deferSDPAnswer) {
5413
+ this.deferSDPAnswer.resolve();
5414
+ clearTimeout(this.sdpResponseTimer);
5415
+ this.sdpResponseTimer = undefined;
5416
+ }
5417
+ });
5418
+
5419
+ this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_OFFER_GENERATED, () => {
5420
+ // @ts-ignore
5421
+ this.webex.internal.newMetrics.submitClientEvent({
5422
+ name: 'client.media-engine.local-sdp-generated',
5423
+ options: {meetingId: this.id},
5424
+ });
5425
+
5426
+ // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5427
+ this.deferSDPAnswer = new Defer();
5428
+ });
5429
+
5430
+ this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_ANSWER_GENERATED, () => {
5431
+ // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer
5432
+ // @ts-ignore
5433
+ this.webex.internal.newMetrics.submitClientEvent({
5434
+ name: 'client.media-engine.remote-sdp-received',
5435
+ options: {meetingId: this.id},
5436
+ });
5437
+ });
5438
+ };
5439
+
4913
5440
  setupMediaConnectionListeners = () => {
5441
+ this.setupSdpListeners();
5442
+
4914
5443
  this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4915
5444
  this.isRoapInProgress = true;
4916
5445
  });
@@ -4928,12 +5457,6 @@ export default class Meeting extends StatelessWebexPlugin {
4928
5457
 
4929
5458
  switch (event.roapMessage.messageType) {
4930
5459
  case 'OK':
4931
- // @ts-ignore
4932
- this.webex.internal.newMetrics.submitClientEvent({
4933
- name: 'client.media-engine.remote-sdp-received',
4934
- options: {meetingId: this.id},
4935
- });
4936
-
4937
5460
  logRequest(
4938
5461
  this.roap.sendRoapOK({
4939
5462
  seq: event.roapMessage.seq,
@@ -4947,33 +5470,32 @@ export default class Meeting extends StatelessWebexPlugin {
4947
5470
  break;
4948
5471
 
4949
5472
  case 'OFFER':
4950
- // @ts-ignore
4951
- this.webex.internal.newMetrics.submitClientEvent({
4952
- name: 'client.media-engine.local-sdp-generated',
4953
- options: {meetingId: this.id},
4954
- });
4955
-
4956
5473
  logRequest(
4957
- this.roap.sendRoapMediaRequest({
4958
- sdp: event.roapMessage.sdp,
4959
- seq: event.roapMessage.seq,
4960
- tieBreaker: event.roapMessage.tieBreaker,
4961
- meeting: this, // or can pass meeting ID
4962
- reconnect: this.reconnectionManager.isReconnectInProgress(),
4963
- }),
5474
+ this.roap
5475
+ .sendRoapMediaRequest({
5476
+ sdp: event.roapMessage.sdp,
5477
+ seq: event.roapMessage.seq,
5478
+ tieBreaker: event.roapMessage.tieBreaker,
5479
+ meeting: this, // or can pass meeting ID
5480
+ })
5481
+ .then(({roapAnswer}) => {
5482
+ if (roapAnswer) {
5483
+ LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`);
5484
+
5485
+ this.roapMessageReceived(roapAnswer);
5486
+ }
5487
+ }),
4964
5488
  {
4965
5489
  logText: `${LOG_HEADER} Roap Offer`,
4966
5490
  }
4967
- );
5491
+ ).catch(() => {
5492
+ this.deferSDPAnswer.reject();
5493
+ clearTimeout(this.sdpResponseTimer);
5494
+ this.sdpResponseTimer = undefined;
5495
+ });
4968
5496
  break;
4969
5497
 
4970
5498
  case 'ANSWER':
4971
- // @ts-ignore
4972
- this.webex.internal.newMetrics.submitClientEvent({
4973
- name: 'client.media-engine.remote-sdp-received',
4974
- options: {meetingId: this.id},
4975
- });
4976
-
4977
5499
  logRequest(
4978
5500
  this.roap.sendRoapAnswer({
4979
5501
  sdp: event.roapMessage.sdp,
@@ -5086,68 +5608,71 @@ export default class Meeting extends StatelessWebexPlugin {
5086
5608
 
5087
5609
  this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5088
5610
  const connectionFailed = () => {
5089
- // we know the media connection failed and browser will not attempt to recover it any more
5090
- // so reset the timer as it's not needed anymore, we want to reconnect immediately
5091
- this.reconnectionManager.resetReconnectionTimer();
5092
-
5093
- this.reconnect({networkDisconnect: true});
5094
- // @ts-ignore
5095
- this.webex.internal.newMetrics.submitClientEvent({
5096
- name: 'client.ice.end',
5097
- payload: {
5098
- canProceed: false,
5099
- icePhase: 'IN_MEETING',
5100
- errors: [
5101
- // @ts-ignore
5102
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5103
- CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE
5104
- ),
5105
- ],
5106
- },
5107
- options: {
5108
- meetingId: this.id,
5109
- },
5110
- });
5111
-
5112
- this.uploadLogs({
5113
- file: 'peer-connection-manager/index',
5114
- function: 'connectionFailed',
5115
- });
5116
-
5117
5611
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5118
5612
  correlation_id: this.correlationId,
5119
5613
  locus_id: this.locusId,
5614
+ networkStatus: this.networkStatus,
5615
+ hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
5120
5616
  });
5617
+
5618
+ if (this.hasMediaConnectionConnectedAtLeastOnce) {
5619
+ // we know the media connection failed and browser will not attempt to recover it any more
5620
+ // so reset the timer as it's not needed anymore, we want to reconnect immediately
5621
+ this.reconnectionManager.resetReconnectionTimer();
5622
+
5623
+ this.reconnect({networkDisconnect: true});
5624
+
5625
+ this.uploadLogs({
5626
+ file: 'peer-connection-manager/index',
5627
+ function: 'connectionFailed',
5628
+ });
5629
+ }
5121
5630
  };
5122
5631
 
5123
5632
  LoggerProxy.logger.info(
5124
5633
  `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
5125
5634
  );
5635
+
5636
+ // @ts-ignore
5637
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5638
+
5126
5639
  switch (event.state) {
5127
5640
  case ConnectionState.Connecting:
5128
- // @ts-ignore
5129
- this.webex.internal.newMetrics.submitClientEvent({
5130
- name: 'client.ice.start',
5131
- options: {
5132
- meetingId: this.id,
5133
- },
5134
- });
5641
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5642
+ // Only send CA event for join flow if we haven't successfully connected media yet
5643
+ // @ts-ignore
5644
+ this.webex.internal.newMetrics.submitClientEvent({
5645
+ name: 'client.ice.start',
5646
+ options: {
5647
+ meetingId: this.id,
5648
+ },
5649
+ });
5650
+ }
5135
5651
  break;
5136
5652
  case ConnectionState.Connected:
5137
- // @ts-ignore
5138
- this.webex.internal.newMetrics.submitClientEvent({
5139
- name: 'client.ice.end',
5140
- options: {
5141
- meetingId: this.id,
5142
- },
5143
- });
5653
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5654
+ // Only send CA event for join flow if we haven't successfully connected media yet
5655
+ // @ts-ignore
5656
+ this.webex.internal.newMetrics.submitClientEvent({
5657
+ name: 'client.ice.end',
5658
+ payload: {
5659
+ canProceed: true,
5660
+ icePhase: 'JOIN_MEETING_FINAL',
5661
+ },
5662
+ options: {
5663
+ meetingId: this.id,
5664
+ },
5665
+ });
5666
+ }
5144
5667
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5145
5668
  correlation_id: this.correlationId,
5146
5669
  locus_id: this.locusId,
5670
+ latency: cdl.getICESetupTime(),
5147
5671
  });
5148
5672
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
5149
5673
  this.reconnectionManager.iceReconnected();
5150
5674
  this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
5675
+ this.hasMediaConnectionConnectedAtLeastOnce = true;
5151
5676
  break;
5152
5677
  case ConnectionState.Disconnected:
5153
5678
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
@@ -5268,7 +5793,10 @@ export default class Meeting extends StatelessWebexPlugin {
5268
5793
  // @ts-ignore
5269
5794
  this.webex.internal.newMetrics.submitClientEvent({
5270
5795
  name: 'client.media.tx.start',
5271
- payload: {mediaType: data.type},
5796
+ payload: {
5797
+ mediaType: data.type,
5798
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5799
+ },
5272
5800
  options: {
5273
5801
  meetingId: this.id,
5274
5802
  },
@@ -5278,7 +5806,10 @@ export default class Meeting extends StatelessWebexPlugin {
5278
5806
  // @ts-ignore
5279
5807
  this.webex.internal.newMetrics.submitClientEvent({
5280
5808
  name: 'client.media.tx.stop',
5281
- payload: {mediaType: data.type},
5809
+ payload: {
5810
+ mediaType: data.type,
5811
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5812
+ },
5282
5813
  options: {
5283
5814
  meetingId: this.id,
5284
5815
  },
@@ -5297,7 +5828,10 @@ export default class Meeting extends StatelessWebexPlugin {
5297
5828
  // @ts-ignore
5298
5829
  this.webex.internal.newMetrics.submitClientEvent({
5299
5830
  name: 'client.media.rx.start',
5300
- payload: {mediaType: data.type},
5831
+ payload: {
5832
+ mediaType: data.type,
5833
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5834
+ },
5301
5835
  options: {
5302
5836
  meetingId: this.id,
5303
5837
  },
@@ -5307,7 +5841,10 @@ export default class Meeting extends StatelessWebexPlugin {
5307
5841
  // @ts-ignore
5308
5842
  this.webex.internal.newMetrics.submitClientEvent({
5309
5843
  name: 'client.media.rx.stop',
5310
- payload: {mediaType: data.type},
5844
+ payload: {
5845
+ mediaType: data.type,
5846
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5847
+ },
5311
5848
  options: {
5312
5849
  meetingId: this.id,
5313
5850
  },
@@ -5360,14 +5897,15 @@ export default class Meeting extends StatelessWebexPlugin {
5360
5897
  this.mediaProperties.mediaDirection.receiveShare,
5361
5898
  ];
5362
5899
 
5363
- this.sendSlotManager.createSlot(mc, MediaType.VideoMain, audioEnabled);
5364
- this.sendSlotManager.createSlot(mc, MediaType.AudioMain, videoEnabled);
5900
+ this.sendSlotManager.createSlot(mc, MediaType.VideoMain, videoEnabled);
5901
+ this.sendSlotManager.createSlot(mc, MediaType.AudioMain, audioEnabled);
5365
5902
  this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
5366
5903
  this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
5367
5904
  }
5368
5905
 
5369
5906
  // publish the streams
5370
5907
  if (this.mediaProperties.audioStream) {
5908
+ this.setSendNamedMediaGroup(MediaType.AudioMain);
5371
5909
  await this.publishStream(MediaType.AudioMain, this.mediaProperties.audioStream);
5372
5910
  }
5373
5911
  if (this.mediaProperties.videoStream) {
@@ -5383,51 +5921,470 @@ export default class Meeting extends StatelessWebexPlugin {
5383
5921
  return mc;
5384
5922
  }
5385
5923
 
5386
- /**
5387
- * Listens for an event emitted by eventEmitter and emits it from the meeting object
5388
- *
5389
- * @private
5390
- * @param {*} eventEmitter object from which to forward the event
5391
- * @param {*} eventTypeToForward which event type to listen on and to forward
5392
- * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
5393
- * @returns {void}
5394
- */
5395
- forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
5396
- eventEmitter.on(eventTypeToForward, (data) =>
5397
- Trigger.trigger(
5398
- this,
5399
- {
5400
- file: 'meetings',
5401
- function: 'addMedia',
5402
- },
5403
- meetingEventType,
5404
- data
5405
- )
5406
- );
5924
+ /**
5925
+ * Listens for an event emitted by eventEmitter and emits it from the meeting object
5926
+ *
5927
+ * @private
5928
+ * @param {*} eventEmitter object from which to forward the event
5929
+ * @param {*} eventTypeToForward which event type to listen on and to forward
5930
+ * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
5931
+ * @returns {void}
5932
+ */
5933
+ forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
5934
+ eventEmitter.on(eventTypeToForward, (data) =>
5935
+ Trigger.trigger(
5936
+ this,
5937
+ {
5938
+ file: 'meetings',
5939
+ function: 'addMedia',
5940
+ },
5941
+ meetingEventType,
5942
+ data
5943
+ )
5944
+ );
5945
+ }
5946
+
5947
+ /**
5948
+ * Sets up all the references to local streams in this.mediaProperties before creating media connection
5949
+ * and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages.
5950
+ *
5951
+ * @private
5952
+ * @param {LocalStreams} localStreams
5953
+ * @returns {Promise<void>}
5954
+ */
5955
+ private async setUpLocalStreamReferences(localStreams: LocalStreams) {
5956
+ const setUpStreamPromises = [];
5957
+
5958
+ if (localStreams?.microphone) {
5959
+ setUpStreamPromises.push(this.setLocalAudioStream(localStreams.microphone));
5960
+ }
5961
+ if (localStreams?.camera) {
5962
+ setUpStreamPromises.push(this.setLocalVideoStream(localStreams.camera));
5963
+ }
5964
+ if (localStreams?.screenShare?.video) {
5965
+ setUpStreamPromises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5966
+ }
5967
+ if (localStreams?.screenShare?.audio) {
5968
+ setUpStreamPromises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5969
+ }
5970
+
5971
+ try {
5972
+ await Promise.all(setUpStreamPromises);
5973
+ } catch (error) {
5974
+ LoggerProxy.logger.error(
5975
+ `Meeting:index#addMedia():setUpLocalStreamReferences --> Error , `,
5976
+ error
5977
+ );
5978
+
5979
+ throw error;
5980
+ }
5981
+ }
5982
+
5983
+ /**
5984
+ * Calls mediaProperties.waitForMediaConnectionConnected() and sends CA client.ice.end metric on failure
5985
+ *
5986
+ * @private
5987
+ * @returns {Promise<void>}
5988
+ */
5989
+ private async waitForMediaConnectionConnected(): Promise<void> {
5990
+ try {
5991
+ await this.mediaProperties.waitForMediaConnectionConnected();
5992
+ } catch (error) {
5993
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5994
+ // Only send CA event for join flow if we haven't successfully connected media yet
5995
+ // @ts-ignore
5996
+ this.webex.internal.newMetrics.submitClientEvent({
5997
+ name: 'client.ice.end',
5998
+ payload: {
5999
+ canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
6000
+ icePhase: this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY',
6001
+ errors: [
6002
+ // @ts-ignore
6003
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6004
+ {
6005
+ clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
6006
+ signalingState:
6007
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6008
+ ?.signalingState ||
6009
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6010
+ ?.signalingState ||
6011
+ 'unknown',
6012
+ iceConnectionState:
6013
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6014
+ ?.iceConnectionState ||
6015
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6016
+ ?.iceConnectionState ||
6017
+ 'unknown',
6018
+ turnServerUsed: this.turnServerUsed,
6019
+ }),
6020
+ }
6021
+ ),
6022
+ ],
6023
+ },
6024
+ options: {
6025
+ meetingId: this.id,
6026
+ },
6027
+ });
6028
+ }
6029
+ throw new Error(
6030
+ `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
6031
+ );
6032
+ }
6033
+ }
6034
+
6035
+ /**
6036
+ * Enables statsAnalyser if config allows it
6037
+ *
6038
+ * @private
6039
+ * @returns {void}
6040
+ */
6041
+ private createStatsAnalyzer() {
6042
+ // @ts-ignore - config coming from registerPlugin
6043
+ if (this.config.stats.enableStatsAnalyzer) {
6044
+ // @ts-ignore - config coming from registerPlugin
6045
+ this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
6046
+ this.statsAnalyzer = new StatsAnalyzer(
6047
+ // @ts-ignore - config coming from registerPlugin
6048
+ this.config.stats,
6049
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
6050
+ this.networkQualityMonitor
6051
+ );
6052
+ this.setupStatsAnalyzerEventHandlers();
6053
+ this.networkQualityMonitor.on(
6054
+ EVENT_TRIGGERS.NETWORK_QUALITY,
6055
+ this.sendNetworkQualityEvent.bind(this)
6056
+ );
6057
+ }
6058
+ }
6059
+
6060
+ /**
6061
+ * Handles device logging
6062
+ *
6063
+ * @private
6064
+ * @static
6065
+ * @returns {Promise<void>}
6066
+ */
6067
+ private static async handleDeviceLogging(): Promise<void> {
6068
+ try {
6069
+ const devices = await getDevices();
6070
+
6071
+ MeetingUtil.handleDeviceLogging(devices);
6072
+ } catch {
6073
+ // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
6074
+ }
6075
+ }
6076
+
6077
+ /**
6078
+ * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6079
+ * once the remote sdp answer has been received.
6080
+ *
6081
+ * @private
6082
+ * @returns {Promise<void>}
6083
+ */
6084
+ private async waitForRemoteSDPAnswer(): Promise<void> {
6085
+ const LOG_HEADER = 'Meeting:index#addMedia():waitForRemoteSDPAnswer -->';
6086
+
6087
+ if (!this.deferSDPAnswer) {
6088
+ LoggerProxy.logger.warn(`${LOG_HEADER} offer not created yet`);
6089
+
6090
+ return Promise.reject(
6091
+ new Error('waitForRemoteSDPAnswer() called before local sdp offer created')
6092
+ );
6093
+ }
6094
+
6095
+ const {deferSDPAnswer} = this;
6096
+
6097
+ this.sdpResponseTimer = setTimeout(() => {
6098
+ LoggerProxy.logger.warn(
6099
+ `${LOG_HEADER} timeout! no REMOTE SDP ANSWER received within ${
6100
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
6101
+ } seconds`
6102
+ );
6103
+ deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
6104
+ }, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
6105
+
6106
+ LoggerProxy.logger.info(`${LOG_HEADER} waiting for REMOTE SDP ANSWER...`);
6107
+
6108
+ return deferSDPAnswer.promise;
6109
+ }
6110
+
6111
+ /**
6112
+ * Calls establishMediaConnection with isForced = true to force turn discovery to happen
6113
+ *
6114
+ * @private
6115
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6116
+ * @param {BundlePolicy} [bundlePolicy]
6117
+ * @returns {Promise<void>}
6118
+ */
6119
+ private async retryEstablishMediaConnectionWithForcedTurnDiscovery(
6120
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6121
+ bundlePolicy?: BundlePolicy
6122
+ ): Promise<void> {
6123
+ const LOG_HEADER =
6124
+ 'Meeting:index#addMedia():retryEstablishMediaConnectionWithForcedTurnDiscovery -->';
6125
+
6126
+ try {
6127
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, true);
6128
+ } catch (err) {
6129
+ LoggerProxy.logger.error(
6130
+ `${LOG_HEADER} retry with TURN-TLS failed, media connection unable to connect, `,
6131
+ err
6132
+ );
6133
+
6134
+ throw err;
6135
+ }
6136
+ }
6137
+
6138
+ /**
6139
+ * Does relevant clean up before retrying to establish media connection
6140
+ * and performs the retry with forced turn discovery
6141
+ *
6142
+ * @private
6143
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6144
+ * @param {BundlePolicy} [bundlePolicy]
6145
+ * @returns {Promise<void>}
6146
+ */
6147
+ private async retryWithForcedTurnDiscovery(
6148
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6149
+ bundlePolicy?: BundlePolicy
6150
+ ): Promise<void> {
6151
+ this.retriedWithTurnServer = true;
6152
+ const LOG_HEADER = 'Meeting:index#addMedia():retryWithForcedTurnDiscovery -->';
6153
+
6154
+ await this.cleanUpBeforeRetryWithTurnServer();
6155
+
6156
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_RETRY, {
6157
+ correlation_id: this.correlationId,
6158
+ state: this.state,
6159
+ meetingState: this.meetingState,
6160
+ reason: 'forcingTurnTls',
6161
+ });
6162
+
6163
+ if (this.state === MEETING_STATE.STATES.LEFT) {
6164
+ LoggerProxy.logger.info(
6165
+ `${LOG_HEADER} meeting state was LEFT after first attempt to establish media connection. Attempting to rejoin. `
6166
+ );
6167
+ await this.join({rejoin: true});
6168
+ }
6169
+
6170
+ await this.retryEstablishMediaConnectionWithForcedTurnDiscovery(
6171
+ remoteMediaManagerConfig,
6172
+ bundlePolicy
6173
+ );
6174
+ }
6175
+
6176
+ /**
6177
+ * If waitForMediaConnectionConnected() fails when we haven't done turn discovery then we
6178
+ * attempt to establish a media connection again, but this time using turn discovery. If we
6179
+ * used turn discovery on the first pass we do not attempt connection again.
6180
+ *
6181
+ * @private
6182
+ * @param {Error} error
6183
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6184
+ * @param {BundlePolicy} [bundlePolicy]
6185
+ * @returns {Promise<void>}
6186
+ */
6187
+ private async handleWaitForMediaConnectionConnectedError(
6188
+ error: Error,
6189
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6190
+ bundlePolicy?: BundlePolicy
6191
+ ): Promise<void> {
6192
+ const LOG_HEADER = 'Meeting:index#addMedia():handleWaitForMediaConnectionConnectedError -->';
6193
+
6194
+ // @ts-ignore - config coming from registerPlugin
6195
+ if (!this.turnServerUsed) {
6196
+ LoggerProxy.logger.info(
6197
+ `${LOG_HEADER} error waiting for media to connect on UDP, TCP, retrying using TURN-TLS, `,
6198
+ error
6199
+ );
6200
+
6201
+ await this.retryWithForcedTurnDiscovery(remoteMediaManagerConfig, bundlePolicy);
6202
+ } else {
6203
+ LoggerProxy.logger.error(
6204
+ `${LOG_HEADER} error waiting for media to connect using UDP, TCP and TURN-TLS`,
6205
+ error
6206
+ );
6207
+
6208
+ throw new AddMediaFailed();
6209
+ }
6210
+ }
6211
+
6212
+ /**
6213
+ * Does TURN discovery, SDP offer/answer exhange, establishes ICE connection and DTLS handshake.
6214
+ *
6215
+ * @private
6216
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6217
+ * @param {BundlePolicy} [bundlePolicy]
6218
+ * @param {boolean} [isForced] - let isForced be true to do turn discovery regardless of reachability results
6219
+ * @returns {Promise<void>}
6220
+ */
6221
+ private async establishMediaConnection(
6222
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6223
+ bundlePolicy?: BundlePolicy,
6224
+ isForced?: boolean
6225
+ ): Promise<void> {
6226
+ const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
6227
+ // @ts-ignore
6228
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
6229
+ const isRetry = this.retriedWithTurnServer;
6230
+
6231
+ try {
6232
+ // @ts-ignore
6233
+ this.webex.internal.newMetrics.submitInternalEvent({
6234
+ name: 'internal.client.add-media.turn-discovery.start',
6235
+ });
6236
+
6237
+ const turnDiscoveryObject = await this.roap.doTurnDiscovery(this, isRetry, isForced);
6238
+
6239
+ this.turnDiscoverySkippedReason = turnDiscoveryObject?.turnDiscoverySkippedReason;
6240
+ this.turnServerUsed = !this.turnDiscoverySkippedReason;
6241
+
6242
+ // @ts-ignore
6243
+ this.webex.internal.newMetrics.submitInternalEvent({
6244
+ name: 'internal.client.add-media.turn-discovery.end',
6245
+ });
6246
+
6247
+ const {turnServerInfo} = turnDiscoveryObject;
6248
+
6249
+ if (this.turnServerUsed && turnServerInfo) {
6250
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
6251
+ correlation_id: this.correlationId,
6252
+ latency: cdl.getTurnDiscoveryTime(),
6253
+ turnServerUsed: this.turnServerUsed,
6254
+ retriedWithTurnServer: this.retriedWithTurnServer,
6255
+ });
6256
+ }
6257
+
6258
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
6259
+
6260
+ LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6261
+
6262
+ if (this.isMultistream) {
6263
+ this.remoteMediaManager = new RemoteMediaManager(
6264
+ this.receiveSlotManager,
6265
+ this.mediaRequestManagers,
6266
+ remoteMediaManagerConfig
6267
+ );
6268
+
6269
+ this.forwardEvent(
6270
+ this.remoteMediaManager,
6271
+ RemoteMediaManagerEvent.AudioCreated,
6272
+ EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
6273
+ );
6274
+ this.forwardEvent(
6275
+ this.remoteMediaManager,
6276
+ RemoteMediaManagerEvent.InterpretationAudioCreated,
6277
+ EVENT_TRIGGERS.REMOTE_MEDIA_INTERPRETATION_AUDIO_CREATED
6278
+ );
6279
+ this.forwardEvent(
6280
+ this.remoteMediaManager,
6281
+ RemoteMediaManagerEvent.ScreenShareAudioCreated,
6282
+ EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
6283
+ );
6284
+ this.forwardEvent(
6285
+ this.remoteMediaManager,
6286
+ RemoteMediaManagerEvent.VideoLayoutChanged,
6287
+ EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
6288
+ );
6289
+
6290
+ await this.remoteMediaManager.start();
6291
+ }
6292
+
6293
+ await mc.initiateOffer();
6294
+
6295
+ await this.waitForRemoteSDPAnswer();
6296
+
6297
+ this.handleMediaLogging(this.mediaProperties);
6298
+ } catch (error) {
6299
+ LoggerProxy.logger.error(`${LOG_HEADER} error establishing media connection, `, error);
6300
+
6301
+ throw error;
6302
+ }
6303
+
6304
+ try {
6305
+ await this.waitForMediaConnectionConnected();
6306
+ } catch (error) {
6307
+ await this.handleWaitForMediaConnectionConnectedError(
6308
+ error,
6309
+ remoteMediaManagerConfig,
6310
+ bundlePolicy
6311
+ );
6312
+ }
6313
+ }
6314
+
6315
+ /**
6316
+ * Cleans up stats analyzer, peer connection, and turns off listeners
6317
+ *
6318
+ * @private
6319
+ * @returns {Promise<void>}
6320
+ */
6321
+ private async cleanUpOnAddMediaFailure(): Promise<void> {
6322
+ if (this.statsAnalyzer) {
6323
+ await this.statsAnalyzer.stopAnalyzer();
6324
+ }
6325
+
6326
+ this.statsAnalyzer = null;
6327
+
6328
+ // when media fails, we want to upload a webrtc dump to see whats going on
6329
+ // this function is async, but returns once the stats have been gathered
6330
+ await this.forceSendStatsReport({callFrom: 'addMedia'});
6331
+
6332
+ if (this.mediaProperties.webrtcMediaConnection) {
6333
+ this.closePeerConnections();
6334
+ this.unsetPeerConnections();
6335
+ }
6336
+ }
6337
+
6338
+ /**
6339
+ * Sends stats report, closes peer connection and cleans up any media connection
6340
+ * related things before trying to establish media connection again with turn server
6341
+ *
6342
+ * @private
6343
+ * @returns {Promise<void>}
6344
+ */
6345
+ private async cleanUpBeforeRetryWithTurnServer(): Promise<void> {
6346
+ // when media fails, we want to upload a webrtc dump to see whats going on
6347
+ // this function is async, but returns once the stats have been gathered
6348
+ await this.forceSendStatsReport({callFrom: 'cleanUpBeforeRetryWithTurnServer'});
6349
+
6350
+ if (this.mediaProperties.webrtcMediaConnection) {
6351
+ if (this.remoteMediaManager) {
6352
+ this.remoteMediaManager.stop();
6353
+ this.remoteMediaManager = null;
6354
+ }
6355
+
6356
+ Object.values(this.mediaRequestManagers).forEach((mediaRequestManager) =>
6357
+ mediaRequestManager.reset()
6358
+ );
6359
+
6360
+ this.receiveSlotManager.reset();
6361
+ this.mediaProperties.webrtcMediaConnection.close();
6362
+ this.sendSlotManager.reset();
6363
+
6364
+ this.mediaProperties.unsetPeerConnection();
6365
+ }
5407
6366
  }
5408
6367
 
5409
6368
  /**
5410
6369
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
5411
6370
  *
5412
6371
  * @param {AddMediaOptions} options
5413
- * @returns {Promise}
6372
+ * @returns {Promise<void>}
5414
6373
  * @public
5415
6374
  * @memberof Meeting
5416
6375
  */
5417
- addMedia(options: AddMediaOptions = {}) {
6376
+ async addMedia(options: AddMediaOptions = {}): Promise<void> {
6377
+ this.retriedWithTurnServer = false;
6378
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
5418
6379
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5419
-
5420
- let turnDiscoverySkippedReason;
5421
- let turnServerUsed = false;
5422
-
5423
6380
  LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5424
6381
 
5425
- if (this.meetingState !== FULL_STATE.ACTIVE) {
5426
- return Promise.reject(new MeetingNotActiveError());
6382
+ if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
6383
+ throw new MeetingNotActiveError();
5427
6384
  }
5428
6385
 
5429
6386
  if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
5430
- return Promise.reject(new UserNotJoinedError());
6387
+ throw new UserNotJoinedError();
5431
6388
  }
5432
6389
 
5433
6390
  const {
@@ -5446,7 +6403,7 @@ export default class Meeting extends StatelessWebexPlugin {
5446
6403
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
5447
6404
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
5448
6405
  if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
5449
- return Promise.reject(new UserInLobbyError());
6406
+ throw new UserInLobbyError();
5450
6407
  }
5451
6408
 
5452
6409
  // @ts-ignore
@@ -5506,235 +6463,100 @@ export default class Meeting extends StatelessWebexPlugin {
5506
6463
 
5507
6464
  this.audio = createMuteState(AUDIO, this, audioEnabled);
5508
6465
  this.video = createMuteState(VIDEO, this, videoEnabled);
5509
- const promises = [];
5510
-
5511
- // setup all the references to local streams in this.mediaProperties before creating media connection
5512
- // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5513
- if (localStreams?.microphone) {
5514
- promises.push(this.setLocalAudioStream(localStreams.microphone));
5515
- }
5516
- if (localStreams?.camera) {
5517
- promises.push(this.setLocalVideoStream(localStreams.camera));
5518
- }
5519
- if (localStreams?.screenShare?.video) {
5520
- promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5521
- }
5522
- if (localStreams?.screenShare?.audio) {
5523
- promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5524
- }
5525
-
5526
- return Promise.all(promises)
5527
- .then(() => this.roap.doTurnDiscovery(this, false))
5528
- .then(async (turnDiscoveryObject) => {
5529
- ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5530
- turnServerUsed = !turnDiscoverySkippedReason;
5531
-
5532
- const {turnServerInfo} = turnDiscoveryObject;
5533
6466
 
5534
- const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5535
-
5536
- if (this.isMultistream) {
5537
- this.remoteMediaManager = new RemoteMediaManager(
5538
- this.receiveSlotManager,
5539
- this.mediaRequestManagers,
5540
- remoteMediaManagerConfig
5541
- );
6467
+ try {
6468
+ await this.setUpLocalStreamReferences(localStreams);
5542
6469
 
5543
- this.forwardEvent(
5544
- this.remoteMediaManager,
5545
- RemoteMediaManagerEvent.AudioCreated,
5546
- EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
5547
- );
5548
- this.forwardEvent(
5549
- this.remoteMediaManager,
5550
- RemoteMediaManagerEvent.ScreenShareAudioCreated,
5551
- EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
5552
- );
5553
- this.forwardEvent(
5554
- this.remoteMediaManager,
5555
- RemoteMediaManagerEvent.VideoLayoutChanged,
5556
- EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5557
- );
6470
+ this.setMercuryListener();
5558
6471
 
5559
- await this.remoteMediaManager.start();
5560
- }
6472
+ this.createStatsAnalyzer();
5561
6473
 
5562
- await mc.initiateOffer();
5563
- })
5564
- .then(() => {
5565
- this.setMercuryListener();
5566
- })
5567
- .then(
5568
- () =>
5569
- getDevices()
5570
- .then((devices) => {
5571
- MeetingUtil.handleDeviceLogging(devices);
5572
- })
5573
- .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
5574
- )
5575
- .then(() => {
5576
- this.handleMediaLogging(this.mediaProperties);
5577
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6474
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
5578
6475
 
5579
- // @ts-ignore - config coming from registerPlugin
5580
- if (this.config.stats.enableStatsAnalyzer) {
5581
- // @ts-ignore - config coming from registerPlugin
5582
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5583
- this.statsAnalyzer = new StatsAnalyzer(
5584
- // @ts-ignore - config coming from registerPlugin
5585
- this.config.stats,
5586
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5587
- this.networkQualityMonitor
5588
- );
5589
- this.setupStatsAnalyzerEventHandlers();
5590
- this.networkQualityMonitor.on(
5591
- EVENT_TRIGGERS.NETWORK_QUALITY,
5592
- this.sendNetworkQualityEvent.bind(this)
5593
- );
5594
- }
5595
- })
5596
- .catch((error) => {
5597
- LoggerProxy.logger.error(
5598
- `${LOG_HEADER} Error adding media , setting up peerconnection, `,
5599
- error
5600
- );
6476
+ await Meeting.handleDeviceLogging();
5601
6477
 
5602
- throw error;
5603
- })
5604
- .then(
5605
- () =>
5606
- new Promise<void>((resolve, reject) => {
5607
- let timerCount = 0;
5608
-
5609
- // eslint-disable-next-line func-names
5610
- // eslint-disable-next-line prefer-arrow-callback
5611
- if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5612
- resolve();
5613
- }
5614
- const joiningTimer = setInterval(() => {
5615
- timerCount += 1;
5616
- if (this.meetingState === FULL_STATE.ACTIVE) {
5617
- clearInterval(joiningTimer);
5618
- resolve();
5619
- }
6478
+ if (this.mediaProperties.hasLocalShareStream()) {
6479
+ await this.enqueueScreenShareFloorRequest();
6480
+ }
5620
6481
 
5621
- if (timerCount === 4) {
5622
- clearInterval(joiningTimer);
5623
- reject(new Error('Meeting is still not active '));
5624
- }
5625
- }, 1000);
5626
- })
5627
- )
5628
- .then(() =>
5629
- this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5630
- // @ts-ignore
5631
- this.webex.internal.newMetrics.submitClientEvent({
5632
- name: 'client.ice.end',
5633
- payload: {
5634
- canProceed: false,
5635
- icePhase: 'JOIN_MEETING_FINAL',
5636
- errors: [
5637
- // @ts-ignore
5638
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5639
- CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE
5640
- ),
5641
- ],
5642
- },
5643
- options: {
5644
- meetingId: this.id,
5645
- },
5646
- });
5647
- throw new Error(
5648
- `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5649
- );
5650
- })
5651
- )
5652
- .then(() => {
5653
- if (this.mediaProperties.hasLocalShareStream()) {
5654
- return this.enqueueScreenShareFloorRequest();
5655
- }
6482
+ const connectionType = await this.mediaProperties.getCurrentConnectionType();
6483
+ // @ts-ignore
6484
+ const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5656
6485
 
5657
- return Promise.resolve();
5658
- })
5659
- .then(() => this.mediaProperties.getCurrentConnectionType())
5660
- .then((connectionType) => {
5661
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
5662
- correlation_id: this.correlationId,
5663
- locus_id: this.locusUrl.split('/').pop(),
5664
- connectionType,
5665
- isMultistream: this.isMultistream,
5666
- });
5667
- // @ts-ignore
5668
- this.webex.internal.newMetrics.submitClientEvent({
5669
- name: 'client.media-engine.ready',
5670
- options: {
5671
- meetingId: this.id,
5672
- },
5673
- });
5674
- })
5675
- .catch((error) => {
5676
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5677
- correlation_id: this.correlationId,
5678
- locus_id: this.locusUrl.split('/').pop(),
5679
- reason: error.message,
5680
- stack: error.stack,
5681
- code: error.code,
5682
- turnDiscoverySkippedReason,
5683
- turnServerUsed,
5684
- isMultistream: this.isMultistream,
5685
- signalingState:
5686
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5687
- ?.signalingState ||
5688
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5689
- 'unknown',
5690
- connectionState:
5691
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5692
- ?.connectionState ||
5693
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5694
- 'unknown',
5695
- iceConnectionState:
5696
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5697
- ?.iceConnectionState ||
5698
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5699
- 'unknown',
5700
- });
6486
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
6487
+ correlation_id: this.correlationId,
6488
+ locus_id: this.locusUrl.split('/').pop(),
6489
+ connectionType,
6490
+ isMultistream: this.isMultistream,
6491
+ retriedWithTurnServer: this.retriedWithTurnServer,
6492
+ ...reachabilityStats,
6493
+ });
6494
+ // @ts-ignore
6495
+ this.webex.internal.newMetrics.submitClientEvent({
6496
+ name: 'client.media-engine.ready',
6497
+ options: {
6498
+ meetingId: this.id,
6499
+ },
6500
+ });
6501
+ LoggerProxy.logger.info(
6502
+ `${LOG_HEADER} successfully established media connection, type=${connectionType}`
6503
+ );
5701
6504
 
5702
- // Clean up stats analyzer, peer connection, and turn off listeners
5703
- const stopStatsAnalyzer = this.statsAnalyzer
5704
- ? this.statsAnalyzer.stopAnalyzer()
5705
- : Promise.resolve();
6505
+ // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
6506
+ this.remoteMediaManager?.logAllReceiveSlots();
6507
+ } catch (error) {
6508
+ LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
5706
6509
 
5707
- return stopStatsAnalyzer.then(() => {
5708
- this.statsAnalyzer = null;
6510
+ // @ts-ignore
6511
+ const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
5709
6512
 
5710
- if (this.mediaProperties.webrtcMediaConnection) {
5711
- this.closePeerConnections();
5712
- this.unsetPeerConnections();
5713
- }
6513
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
6514
+ correlation_id: this.correlationId,
6515
+ locus_id: this.locusUrl.split('/').pop(),
6516
+ reason: error.message,
6517
+ stack: error.stack,
6518
+ code: error.code,
6519
+ turnDiscoverySkippedReason: this.turnDiscoverySkippedReason,
6520
+ turnServerUsed: this.turnServerUsed,
6521
+ retriedWithTurnServer: this.retriedWithTurnServer,
6522
+ isMultistream: this.isMultistream,
6523
+ signalingState:
6524
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6525
+ ?.signalingState ||
6526
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
6527
+ 'unknown',
6528
+ connectionState:
6529
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6530
+ ?.connectionState ||
6531
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
6532
+ 'unknown',
6533
+ iceConnectionState:
6534
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6535
+ ?.iceConnectionState ||
6536
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
6537
+ 'unknown',
6538
+ ...reachabilityMetrics,
6539
+ });
5714
6540
 
5715
- LoggerProxy.logger.error(
5716
- `${LOG_HEADER} Error adding media failed to initiate PC and send request, `,
5717
- error
5718
- );
6541
+ await this.cleanUpOnAddMediaFailure();
5719
6542
 
5720
- // Upload logs on error while adding media
5721
- Trigger.trigger(
5722
- this,
5723
- {
5724
- file: 'meeting/index',
5725
- function: 'addMedia',
5726
- },
5727
- EVENTS.REQUEST_UPLOAD_LOGS,
5728
- this
5729
- );
6543
+ // Upload logs on error while adding media
6544
+ Trigger.trigger(
6545
+ this,
6546
+ {
6547
+ file: 'meeting/index',
6548
+ function: 'addMedia',
6549
+ },
6550
+ EVENTS.REQUEST_UPLOAD_LOGS,
6551
+ this
6552
+ );
5730
6553
 
5731
- if (error instanceof Errors.SdpError) {
5732
- this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5733
- }
6554
+ if (error instanceof Errors.SdpError) {
6555
+ this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
6556
+ }
5734
6557
 
5735
- throw error;
5736
- });
5737
- });
6558
+ throw error;
6559
+ }
5738
6560
  }
5739
6561
 
5740
6562
  /**
@@ -5748,6 +6570,24 @@ export default class Meeting extends StatelessWebexPlugin {
5748
6570
  return !this.isRoapInProgress;
5749
6571
  }
5750
6572
 
6573
+ /**
6574
+ * media failed, so collect a stats report from webrtc using the wcme connection to grab the rtc stats report
6575
+ * send a webrtc telemetry dump to the configured server using the internal media core check metrics configured callback
6576
+ * @param {String} callFrom - the function calling this function, optional.
6577
+ * @returns {Promise<void>}
6578
+ */
6579
+ private forceSendStatsReport = async ({callFrom}: {callFrom?: string}) => {
6580
+ const LOG_HEADER = `Meeting:index#forceSendStatsReport --> called from ${callFrom} : `;
6581
+ try {
6582
+ await this.mediaProperties?.webrtcMediaConnection?.forceRtcMetricsSend();
6583
+ LoggerProxy.logger.info(
6584
+ `${LOG_HEADER} successfully uploaded available webrtc telemetry statistics`
6585
+ );
6586
+ } catch (e) {
6587
+ LoggerProxy.logger.error(`${LOG_HEADER} failed to upload webrtc telemetry statistics: `, e);
6588
+ }
6589
+ };
6590
+
5751
6591
  /**
5752
6592
  * Enqueues a media update operation.
5753
6593
  * @param {String} mediaUpdateType one of MEDIA_UPDATE_TYPE values
@@ -5872,12 +6712,15 @@ export default class Meeting extends StatelessWebexPlugin {
5872
6712
  return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
5873
6713
  }
5874
6714
 
5875
- if (
5876
- this.isMultistream &&
5877
- (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined)
5878
- ) {
6715
+ if (this.isMultistream) {
6716
+ if (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined) {
6717
+ throw new Error(
6718
+ 'toggling shareAudioEnabled or shareVideoEnabled in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
6719
+ );
6720
+ }
6721
+ } else if (shareAudioEnabled !== undefined) {
5879
6722
  throw new Error(
5880
- 'toggling shareAudioEnabled or shareVideoEnabled in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
6723
+ 'toggling shareAudioEnabled in a transcoded meeting is not supported as of now'
5881
6724
  );
5882
6725
  }
5883
6726
 
@@ -6210,17 +7053,13 @@ export default class Meeting extends StatelessWebexPlugin {
6210
7053
  .catch((error) => {
6211
7054
  LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
6212
7055
 
6213
- Metrics.sendBehavioralMetric(
6214
- // @ts-ignore - check if STOP_WHITEBOARD_SHARE_FAILURE exists
6215
- BEHAVIORAL_METRICS.STOP_WHITEBOARD_SHARE_FAILURE,
6216
- {
6217
- correlation_id: this.correlationId,
6218
- locus_id: this.locusUrl.split('/').pop(),
6219
- reason: error.message,
6220
- stack: error.stack,
6221
- board: {channelUrl},
6222
- }
6223
- );
7056
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_STOP_WHITEBOARD_SHARE_FAILURE, {
7057
+ correlation_id: this.correlationId,
7058
+ locus_id: this.locusUrl.split('/').pop(),
7059
+ reason: error.message,
7060
+ stack: error.stack,
7061
+ board: {channelUrl},
7062
+ });
6224
7063
 
6225
7064
  return Promise.reject(error);
6226
7065
  })
@@ -6257,11 +7096,14 @@ export default class Meeting extends StatelessWebexPlugin {
6257
7096
  if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
6258
7097
  // @ts-ignore
6259
7098
  this.webex.internal.newMetrics.submitClientEvent({
6260
- name: 'client.share.initiated',
7099
+ name: 'client.share.floor-grant.request',
6261
7100
  payload: {
6262
7101
  mediaType: 'share',
7102
+ shareInstanceId: this.localShareInstanceId,
7103
+ },
7104
+ options: {
7105
+ meetingId: this.id,
6263
7106
  },
6264
- options: {meetingId: this.id},
6265
7107
  });
6266
7108
 
6267
7109
  return this.meetingRequest
@@ -6271,10 +7113,16 @@ export default class Meeting extends StatelessWebexPlugin {
6271
7113
  deviceUrl: this.deviceUrl,
6272
7114
  uri: content.url,
6273
7115
  resourceUrl: this.resourceUrl,
7116
+ shareInstanceId: this.localShareInstanceId,
6274
7117
  })
6275
7118
  .then(() => {
6276
7119
  this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
6277
7120
 
7121
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
7122
+ correlation_id: this.correlationId,
7123
+ locus_id: this.locusUrl.split('/').pop(),
7124
+ });
7125
+
6278
7126
  return Promise.resolve();
6279
7127
  })
6280
7128
  .catch((error) => {
@@ -6287,6 +7135,19 @@ export default class Meeting extends StatelessWebexPlugin {
6287
7135
  stack: error.stack,
6288
7136
  });
6289
7137
 
7138
+ // @ts-ignore
7139
+ this.webex.internal.newMetrics.submitClientEvent({
7140
+ name: 'client.share.floor-granted.local',
7141
+ payload: {
7142
+ mediaType: 'share',
7143
+ errors: MeetingUtil.getChangeMeetingFloorErrorPayload(error.message),
7144
+ shareInstanceId: this.localShareInstanceId,
7145
+ },
7146
+ options: {
7147
+ meetingId: this.id,
7148
+ },
7149
+ });
7150
+
6290
7151
  this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
6291
7152
 
6292
7153
  return Promise.reject(error);
@@ -6338,6 +7199,7 @@ export default class Meeting extends StatelessWebexPlugin {
6338
7199
  name: 'client.share.stopped',
6339
7200
  payload: {
6340
7201
  mediaType: 'share',
7202
+ shareInstanceId: this.localShareInstanceId,
6341
7203
  },
6342
7204
  options: {meetingId: this.id},
6343
7205
  });
@@ -6354,6 +7216,7 @@ export default class Meeting extends StatelessWebexPlugin {
6354
7216
  deviceUrl: this.deviceUrl,
6355
7217
  uri: content.url,
6356
7218
  resourceUrl: this.resourceUrl,
7219
+ shareInstanceId: this.localShareInstanceId,
6357
7220
  })
6358
7221
  .catch((error) => {
6359
7222
  LoggerProxy.logger.error('Meeting:index#releaseScreenShareFloor --> Error ', error);
@@ -6558,7 +7421,7 @@ export default class Meeting extends StatelessWebexPlugin {
6558
7421
  if (layoutType) {
6559
7422
  if (!LAYOUT_TYPES.includes(layoutType)) {
6560
7423
  this.rejectWithErrorLog(
6561
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType recieved.'
7424
+ 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
6562
7425
  );
6563
7426
  }
6564
7427
 
@@ -6696,6 +7559,23 @@ export default class Meeting extends StatelessWebexPlugin {
6696
7559
  }
6697
7560
  };
6698
7561
 
7562
+ /**
7563
+ * Functionality for when a share video is muted or unmuted.
7564
+ * @private
7565
+ * @memberof Meeting
7566
+ * @param {boolean} muted
7567
+ * @returns {undefined}
7568
+ */
7569
+ private handleShareVideoStreamMuteStateChange = (muted: boolean) => {
7570
+ LoggerProxy.logger.log(
7571
+ `Meeting:index#handleShareVideoStreamMuteStateChange --> Share video stream mute state changed to muted ${muted}`
7572
+ );
7573
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
7574
+ correlationId: this.correlationId,
7575
+ muted,
7576
+ });
7577
+ };
7578
+
6699
7579
  /**
6700
7580
  * Functionality for when a share video is ended.
6701
7581
  * @private
@@ -7062,10 +7942,12 @@ export default class Meeting extends StatelessWebexPlugin {
7062
7942
  .update({
7063
7943
  // TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
7064
7944
  localTracks: {
7065
- audio: this.mediaProperties.audioStream?.outputTrack || null,
7066
- video: this.mediaProperties.videoStream?.outputTrack || null,
7067
- screenShareVideo: this.mediaProperties.shareVideoStream?.outputTrack || null,
7068
- screenShareAudio: this.mediaProperties.shareAudioStream?.outputTrack || null,
7945
+ audio: this.mediaProperties.audioStream?.outputStream?.getTracks()[0] || null,
7946
+ video: this.mediaProperties.videoStream?.outputStream?.getTracks()[0] || null,
7947
+ screenShareVideo:
7948
+ this.mediaProperties.shareVideoStream?.outputStream?.getTracks()[0] || null,
7949
+ screenShareAudio:
7950
+ this.mediaProperties.shareAudioStream?.outputStream?.getTracks()[0] || null,
7069
7951
  },
7070
7952
  direction: {
7071
7953
  audio: Media.getDirection(
@@ -7103,6 +7985,33 @@ export default class Meeting extends StatelessWebexPlugin {
7103
7985
  });
7104
7986
  }
7105
7987
 
7988
+ /**
7989
+ * set sending named media group which the audio should send to
7990
+ * @param {MediaType} mediaType of the stream
7991
+ * @param {number} languageCode of the stream
7992
+ * @returns {void}
7993
+ */
7994
+ public setSendNamedMediaGroup(mediaType: MediaType, languageCode = 0): void {
7995
+ if (mediaType !== MediaType.AudioMain) {
7996
+ throw new Error(`cannot set send named media group which media type is ${mediaType}`);
7997
+ }
7998
+
7999
+ const value = languageCode || this.simultaneousInterpretation.getTargetLanguageCode();
8000
+ let groups = [];
8001
+
8002
+ if (value) {
8003
+ groups = [
8004
+ {
8005
+ type: NAMED_MEDIA_GROUP_TYPE_AUDIO,
8006
+ value,
8007
+ },
8008
+ ];
8009
+ }
8010
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
8011
+ this.sendSlotManager.setNamedMediaGroups(mediaType, groups);
8012
+ }
8013
+ }
8014
+
7106
8015
  /**
7107
8016
  * Publishes a stream.
7108
8017
  *
@@ -7199,6 +8108,23 @@ export default class Meeting extends StatelessWebexPlugin {
7199
8108
  }
7200
8109
 
7201
8110
  if (floorRequestNeeded) {
8111
+ this.localShareInstanceId = uuid.v4();
8112
+
8113
+ // @ts-ignore
8114
+ this.webex.internal.newMetrics.submitClientEvent({
8115
+ name: 'client.share.initiated',
8116
+ payload: {
8117
+ mediaType: 'share',
8118
+ shareInstanceId: this.localShareInstanceId,
8119
+ },
8120
+ options: {meetingId: this.id},
8121
+ });
8122
+
8123
+ this.statsAnalyzer.updateMediaStatus({
8124
+ expected: {
8125
+ sendShare: true,
8126
+ },
8127
+ });
7202
8128
  // we're sending the http request to Locus to request the screen share floor
7203
8129
  // only after the SDP update, because that's how it's always been done for transcoded meetings
7204
8130
  // and also if sharing from the start, we need confluence to have been created
@@ -7247,9 +8173,64 @@ export default class Meeting extends StatelessWebexPlugin {
7247
8173
  if (!this.mediaProperties.hasLocalShareStream()) {
7248
8174
  try {
7249
8175
  this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
8176
+
8177
+ this.statsAnalyzer.updateMediaStatus({
8178
+ expected: {
8179
+ sendShare: false,
8180
+ },
8181
+ });
7250
8182
  } catch (e) {
7251
8183
  // nothing to do here, error is logged already inside releaseScreenShareFloor()
7252
8184
  }
7253
8185
  }
7254
8186
  }
8187
+
8188
+ /**
8189
+ * Gets permission token expiry information including timeLeft, expiryTime, currentTime
8190
+ * (from the time the function has been fired)
8191
+ *
8192
+ * @returns {object} permissionTokenExpiryInfo
8193
+ * @returns {number} permissionTokenExpiryInfo.timeLeft The time left for token to expire
8194
+ * @returns {number} permissionTokenExpiryInfo.expiryTime The expiry time of permission token from the server
8195
+ * @returns {number} permissionTokenExpiryInfo.currentTime The current time of the local machine
8196
+ */
8197
+ public getPermissionTokenExpiryInfo() {
8198
+ if (!this.permissionTokenPayload) {
8199
+ return undefined;
8200
+ }
8201
+
8202
+ const permissionTokenExpiryFromServer = Number(this.permissionTokenPayload.exp);
8203
+ const permissionTokenIssuedTimeFromServer = Number(this.permissionTokenPayload.iat);
8204
+
8205
+ const shiftInTime = this.permissionTokenReceivedLocalTime - permissionTokenIssuedTimeFromServer;
8206
+
8207
+ // using new Date instead of Date.now() to allow for accurate unit testing
8208
+ // https://github.com/sinonjs/fake-timers/issues/321
8209
+ const currentTime = new Date().getTime();
8210
+
8211
+ // adjusted time is calculated in case your machine time is wrong
8212
+ const adjustedCurrentTime = currentTime - shiftInTime;
8213
+
8214
+ const timeLeft = (permissionTokenExpiryFromServer - adjustedCurrentTime) / 1000;
8215
+
8216
+ return {timeLeft, expiryTime: permissionTokenExpiryFromServer, currentTime};
8217
+ }
8218
+
8219
+ /**
8220
+ * Check if there is enough time left till the permission token expires
8221
+ * If not - refresh the permission token
8222
+ *
8223
+ * @param {number} threshold - time in seconds
8224
+ * @param {string} reason - reason for refreshing the permission token
8225
+ * @returns {Promise<void>}
8226
+ */
8227
+ public checkAndRefreshPermissionToken(threshold: number, reason: string): Promise<void> {
8228
+ const timeLeft = this.getPermissionTokenExpiryInfo()?.timeLeft;
8229
+
8230
+ if (timeLeft !== undefined && timeLeft <= threshold) {
8231
+ return this.refreshPermissionToken(reason);
8232
+ }
8233
+
8234
+ return Promise.resolve();
8235
+ }
7255
8236
  }