@webex/plugin-meetings 3.0.0-stream-classes.5 → 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 (233) 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 +67 -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 +15 -10
  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 +37 -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 +2143 -1087
  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 +33 -18
  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 +13 -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 +177 -65
  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 +65 -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 +272 -35
  114. package/dist/types/meeting/muteState.d.ts +2 -8
  115. package/dist/types/meeting/request.d.ts +2 -0
  116. package/dist/types/meeting/util.d.ts +16 -0
  117. package/dist/types/meeting-info/index.d.ts +7 -0
  118. package/dist/types/meeting-info/meeting-info-v2.d.ts +1 -0
  119. package/dist/types/meetings/collection.d.ts +9 -0
  120. package/dist/types/meetings/index.d.ts +42 -14
  121. package/dist/types/member/index.d.ts +1 -0
  122. package/dist/types/members/types.d.ts +1 -0
  123. package/dist/types/members/util.d.ts +5 -0
  124. package/dist/types/metrics/constants.d.ts +12 -0
  125. package/dist/types/multistream/mediaRequestManager.d.ts +2 -0
  126. package/dist/types/multistream/remoteMediaGroup.d.ts +2 -0
  127. package/dist/types/multistream/remoteMediaManager.d.ts +16 -0
  128. package/dist/types/multistream/sendSlotManager.d.ts +9 -0
  129. package/dist/types/reachability/clusterReachability.d.ts +109 -0
  130. package/dist/types/reachability/index.d.ts +59 -106
  131. package/dist/types/reachability/util.d.ts +8 -0
  132. package/dist/types/reconnection-manager/index.d.ts +10 -0
  133. package/dist/types/roap/index.d.ts +2 -1
  134. package/dist/types/roap/turnDiscovery.d.ts +21 -4
  135. package/dist/types/rtcMetrics/index.d.ts +15 -1
  136. package/dist/types/statsAnalyzer/index.d.ts +28 -11
  137. package/dist/types/statsAnalyzer/mqaUtil.d.ts +28 -4
  138. package/dist/types/webinar/collection.d.ts +16 -0
  139. package/dist/types/webinar/index.d.ts +5 -0
  140. package/dist/webinar/collection.js +44 -0
  141. package/dist/webinar/collection.js.map +1 -0
  142. package/dist/webinar/index.js +69 -0
  143. package/dist/webinar/index.js.map +1 -0
  144. package/package.json +3 -2
  145. package/src/common/errors/no-meeting-info.ts +24 -0
  146. package/src/common/errors/reclaim-host-role-errors.ts +134 -0
  147. package/src/common/errors/webex-errors.ts +19 -2
  148. package/src/common/logs/request.ts +5 -1
  149. package/src/config.ts +1 -1
  150. package/src/constants.ts +70 -6
  151. package/src/index.ts +5 -0
  152. package/src/interceptors/index.ts +3 -0
  153. package/src/interceptors/locusRetry.ts +67 -0
  154. package/src/interpretation/index.ts +18 -1
  155. package/src/locus-info/index.ts +19 -14
  156. package/src/locus-info/mediaSharesUtils.ts +16 -0
  157. package/src/locus-info/parser.ts +40 -21
  158. package/src/media/index.ts +8 -6
  159. package/src/media/properties.ts +17 -2
  160. package/src/mediaQualityMetrics/config.ts +103 -238
  161. package/src/meeting/in-meeting-actions.ts +8 -0
  162. package/src/meeting/index.ts +1471 -533
  163. package/src/meeting/muteState.ts +34 -20
  164. package/src/meeting/request.ts +18 -0
  165. package/src/meeting/util.ts +97 -0
  166. package/src/meeting-info/index.ts +47 -20
  167. package/src/meeting-info/meeting-info-v2.ts +27 -5
  168. package/src/meeting-info/utilv2.ts +1 -1
  169. package/src/meetings/collection.ts +13 -0
  170. package/src/meetings/index.ts +112 -31
  171. package/src/meetings/util.ts +2 -8
  172. package/src/member/index.ts +9 -0
  173. package/src/member/util.ts +14 -0
  174. package/src/members/index.ts +29 -2
  175. package/src/members/types.ts +1 -0
  176. package/src/members/util.ts +15 -1
  177. package/src/metrics/constants.ts +12 -0
  178. package/src/multistream/mediaRequestManager.ts +4 -1
  179. package/src/multistream/remoteMediaGroup.ts +19 -0
  180. package/src/multistream/remoteMediaManager.ts +101 -15
  181. package/src/multistream/sendSlotManager.ts +29 -0
  182. package/src/reachability/clusterReachability.ts +320 -0
  183. package/src/reachability/index.ts +221 -382
  184. package/src/reachability/request.ts +1 -1
  185. package/src/reachability/util.ts +24 -0
  186. package/src/reconnection-manager/index.ts +87 -83
  187. package/src/roap/index.ts +60 -24
  188. package/src/roap/request.ts +3 -16
  189. package/src/roap/turnDiscovery.ts +112 -39
  190. package/src/rtcMetrics/index.ts +71 -5
  191. package/src/statsAnalyzer/index.ts +430 -427
  192. package/src/statsAnalyzer/mqaUtil.ts +317 -168
  193. package/src/webinar/collection.ts +31 -0
  194. package/src/webinar/index.ts +62 -0
  195. package/test/integration/spec/journey.js +22 -22
  196. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  197. package/test/unit/spec/interpretation/index.ts +36 -3
  198. package/test/unit/spec/locus-info/index.js +87 -11
  199. package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
  200. package/test/unit/spec/locus-info/mediaSharesUtils.ts +10 -0
  201. package/test/unit/spec/locus-info/parser.js +54 -13
  202. package/test/unit/spec/media/index.ts +20 -4
  203. package/test/unit/spec/media/properties.ts +2 -2
  204. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  205. package/test/unit/spec/meeting/index.js +4178 -1289
  206. package/test/unit/spec/meeting/muteState.js +219 -67
  207. package/test/unit/spec/meeting/request.js +63 -12
  208. package/test/unit/spec/meeting/utils.js +93 -0
  209. package/test/unit/spec/meeting-info/index.js +180 -61
  210. package/test/unit/spec/meeting-info/meetinginfov2.js +196 -53
  211. package/test/unit/spec/meetings/collection.js +12 -0
  212. package/test/unit/spec/meetings/index.js +617 -204
  213. package/test/unit/spec/meetings/utils.js +35 -12
  214. package/test/unit/spec/member/index.js +8 -7
  215. package/test/unit/spec/member/util.js +32 -0
  216. package/test/unit/spec/members/index.js +130 -17
  217. package/test/unit/spec/members/utils.js +26 -0
  218. package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
  219. package/test/unit/spec/multistream/remoteMediaGroup.ts +80 -1
  220. package/test/unit/spec/multistream/remoteMediaManager.ts +200 -1
  221. package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
  222. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  223. package/test/unit/spec/reachability/index.ts +505 -135
  224. package/test/unit/spec/reachability/util.ts +40 -0
  225. package/test/unit/spec/reconnection-manager/index.js +74 -17
  226. package/test/unit/spec/roap/index.ts +181 -61
  227. package/test/unit/spec/roap/request.ts +27 -3
  228. package/test/unit/spec/roap/turnDiscovery.ts +362 -101
  229. package/test/unit/spec/rtcMetrics/index.ts +57 -3
  230. package/test/unit/spec/stats-analyzer/index.js +1225 -12
  231. package/test/unit/spec/webinar/collection.ts +13 -0
  232. package/test/unit/spec/webinar/index.ts +60 -0
  233. 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;
@@ -546,18 +574,27 @@ export default class Meeting extends StatelessWebexPlugin {
546
574
  meetingState: any;
547
575
  permissionToken: string;
548
576
  permissionTokenPayload: any;
577
+ permissionTokenReceivedLocalTime: number;
549
578
  resourceId: any;
550
579
  resourceUrl: string;
551
580
  selfId: string;
552
581
  state: any;
553
- localAudioStreamMuteStateHandler: (muted: boolean) => void;
554
- localVideoStreamMuteStateHandler: (muted: boolean) => void;
582
+ localAudioStreamMuteStateHandler: () => void;
583
+ localVideoStreamMuteStateHandler: () => void;
555
584
  localOutputTrackChangeHandler: () => void;
556
585
  roles: any[];
557
586
  environment: string;
558
587
  namespace = MEETINGS;
559
588
  allowMediaInLobby: boolean;
589
+ localShareInstanceId: string;
590
+ remoteShareInstanceId: string;
591
+ turnDiscoverySkippedReason: string;
592
+ turnServerUsed: boolean;
593
+ private retriedWithTurnServer: boolean;
560
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;
561
598
 
562
599
  /**
563
600
  * @param {Object} attrs
@@ -592,20 +629,22 @@ export default class Meeting extends StatelessWebexPlugin {
592
629
  */
593
630
  this.id = uuid.v4();
594
631
  /**
595
- * Correlation ID used for network tracking of meeting
632
+ * Call state used for metrics
596
633
  * @instance
597
- * @type {String}
634
+ * @type {CallStateForMetrics}
598
635
  * @readonly
599
636
  * @public
600
637
  * @memberof Meeting
601
638
  */
602
- if (attrs.correlationId) {
639
+ this.callStateForMetrics = attrs.callStateForMetrics || {};
640
+ const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
641
+ if (correlationId) {
603
642
  LoggerProxy.logger.log(
604
- `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}`
605
644
  );
606
- this.correlationId = attrs.correlationId;
645
+ this.callStateForMetrics.correlationId = correlationId;
607
646
  } else {
608
- this.correlationId = this.id;
647
+ this.callStateForMetrics.correlationId = this.id;
609
648
  }
610
649
  /**
611
650
  * @instance
@@ -673,6 +712,14 @@ export default class Meeting extends StatelessWebexPlugin {
673
712
  */
674
713
  // @ts-ignore
675
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});
676
723
  /**
677
724
  * helper class for managing receive slots (for multistream media connections)
678
725
  */
@@ -1069,13 +1116,14 @@ export default class Meeting extends StatelessWebexPlugin {
1069
1116
  */
1070
1117
  this.networkQualityMonitor = null;
1071
1118
  /**
1119
+ * Indicates network status of the webrtc media connection
1072
1120
  * @instance
1073
1121
  * @type {String}
1074
1122
  * @readonly
1075
1123
  * @public
1076
1124
  * @memberof Meeting
1077
1125
  */
1078
- this.networkStatus = null;
1126
+ this.networkStatus = undefined;
1079
1127
  /**
1080
1128
  * Passing only info as we send basic info for meeting added event
1081
1129
  * @instance
@@ -1194,6 +1242,24 @@ export default class Meeting extends StatelessWebexPlugin {
1194
1242
  */
1195
1243
  this.keepAliveTimerId = null;
1196
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
+
1197
1263
  /**
1198
1264
  * The class that helps to control recording functions: start, stop, pause, resume, etc
1199
1265
  * @instance
@@ -1229,12 +1295,12 @@ export default class Meeting extends StatelessWebexPlugin {
1229
1295
  */
1230
1296
  this.remoteMediaManager = null;
1231
1297
 
1232
- this.localAudioStreamMuteStateHandler = (muted: boolean) => {
1233
- this.audio.handleLocalStreamMuteStateChange(this, muted);
1298
+ this.localAudioStreamMuteStateHandler = () => {
1299
+ this.audio.handleLocalStreamMuteStateChange(this);
1234
1300
  };
1235
1301
 
1236
- this.localVideoStreamMuteStateHandler = (muted: boolean) => {
1237
- this.video.handleLocalStreamMuteStateChange(this, muted);
1302
+ this.localVideoStreamMuteStateHandler = () => {
1303
+ this.video.handleLocalStreamMuteStateChange(this);
1238
1304
  };
1239
1305
 
1240
1306
  // The handling of output track changes should be done inside
@@ -1246,6 +1312,60 @@ export default class Meeting extends StatelessWebexPlugin {
1246
1312
  this.updateTranscodedMediaConnection();
1247
1313
  }
1248
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;
1249
1369
  }
1250
1370
 
1251
1371
  /**
@@ -1279,23 +1399,87 @@ export default class Meeting extends StatelessWebexPlugin {
1279
1399
  }
1280
1400
 
1281
1401
  /**
1282
- * Fetches meeting information.
1283
- * @param {Object} options
1284
- * @param {String} [options.password] optional
1285
- * @param {String} [options.captchaCode] optional
1286
- * @public
1287
- * @memberof Meeting
1288
- * @returns {Promise}
1402
+ * Getter - Returns callStateForMetrics.correlationId
1403
+ * @returns {string}
1289
1404
  */
1290
- public async fetchMeetingInfo({
1291
- password = null,
1292
- captchaCode = null,
1293
- extraParams = {},
1294
- }: {
1295
- password?: string;
1296
- captchaCode?: string;
1297
- extraParams?: Record<string, any>;
1298
- }) {
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> {
1299
1483
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1300
1484
  if (this.fetchMeetingInfoTimeoutId) {
1301
1485
  clearTimeout(this.fetchMeetingInfoTimeoutId);
@@ -1303,7 +1487,7 @@ export default class Meeting extends StatelessWebexPlugin {
1303
1487
  }
1304
1488
  if (captchaCode && !this.requiredCaptcha) {
1305
1489
  return Promise.reject(
1306
- new Error('fetchMeetingInfo() called with captchaCode when captcha was not required')
1490
+ new Error(`${caller}() called with captchaCode when captcha was not required`)
1307
1491
  );
1308
1492
  }
1309
1493
  if (
@@ -1312,50 +1496,47 @@ export default class Meeting extends StatelessWebexPlugin {
1312
1496
  this.passwordStatus !== PASSWORD_STATUS.UNKNOWN
1313
1497
  ) {
1314
1498
  return Promise.reject(
1315
- new Error('fetchMeetingInfo() called with password when password was not required')
1499
+ new Error(`${caller}() called with password when password was not required`)
1316
1500
  );
1317
1501
  }
1318
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> {
1319
1521
  try {
1320
1522
  const captchaInfo = captchaCode
1321
1523
  ? {code: captchaCode, id: this.requiredCaptcha.captchaId}
1322
1524
  : null;
1323
1525
 
1324
1526
  const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(
1325
- this.destination,
1326
- this.destinationType,
1527
+ destination,
1528
+ destinationType,
1327
1529
  password,
1328
1530
  captchaInfo,
1329
1531
  // @ts-ignore - config coming from registerPlugin
1330
1532
  this.config.installedOrgID,
1331
1533
  this.locusId,
1332
1534
  extraParams,
1333
- {meetingId: this.id}
1334
- );
1335
-
1336
- this.parseMeetingInfo(info, this.destination);
1337
- this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1338
- this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1339
- this.requiredCaptcha = null;
1340
- if (
1341
- this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
1342
- this.passwordStatus === PASSWORD_STATUS.VERIFIED
1343
- ) {
1344
- this.passwordStatus = PASSWORD_STATUS.VERIFIED;
1345
- } else {
1346
- this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1347
- }
1348
-
1349
- Trigger.trigger(
1350
- this,
1351
- {
1352
- file: 'meetings',
1353
- function: 'fetchMeetingInfo',
1354
- },
1355
- EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
1535
+ {meetingId: this.id, sendCAevents}
1356
1536
  );
1357
1537
 
1358
- this.updateMeetingActions();
1538
+ this.parseMeetingInfo(info?.body, this.destination, info?.errors);
1539
+ this.setMeetingInfo(info?.body, info?.url);
1359
1540
 
1360
1541
  return Promise.resolve();
1361
1542
  } catch (err) {
@@ -1417,19 +1598,113 @@ export default class Meeting extends StatelessWebexPlugin {
1417
1598
  }
1418
1599
  }
1419
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
+
1420
1693
  /**
1421
1694
  * Checks if the supplied password/host key is correct. It returns a promise with information whether the
1422
1695
  * password and captcha code were correct or not.
1423
1696
  * @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
1424
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
1425
1699
  * @public
1426
1700
  * @memberof Meeting
1427
1701
  * @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
1428
1702
  */
1429
- public verifyPassword(password: string, captchaCode: string) {
1703
+ public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
1430
1704
  return this.fetchMeetingInfo({
1431
1705
  password,
1432
1706
  captchaCode,
1707
+ sendCAevents,
1433
1708
  })
1434
1709
  .then(() => {
1435
1710
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
@@ -1760,12 +2035,12 @@ export default class Meeting extends StatelessWebexPlugin {
1760
2035
 
1761
2036
  /**
1762
2037
  * sets the network status on meeting object
1763
- * @param {String} networkStatus
2038
+ * @param {NETWORK_STATUS} networkStatus
1764
2039
  * @private
1765
2040
  * @returns {undefined}
1766
2041
  * @memberof Meeting
1767
2042
  */
1768
- private setNetworkStatus(networkStatus: string) {
2043
+ private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
1769
2044
  if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
1770
2045
  Trigger.trigger(
1771
2046
  this,
@@ -2020,16 +2295,6 @@ export default class Meeting extends StatelessWebexPlugin {
2020
2295
  }
2021
2296
  );
2022
2297
 
2023
- this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2024
- this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2025
- // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2026
- // which means main session is not active for the attendee
2027
- if (error?.statusCode === 403) {
2028
- this.locusInfo.clearMainSessionLocusCache();
2029
- }
2030
- });
2031
- });
2032
-
2033
2298
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
2034
2299
  Trigger.trigger(
2035
2300
  this,
@@ -2153,6 +2418,7 @@ export default class Meeting extends StatelessWebexPlugin {
2153
2418
  if (
2154
2419
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2155
2420
  contentShare.disposition === previousContentShare?.disposition &&
2421
+ contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
2156
2422
  whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
2157
2423
  whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
2158
2424
  whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
@@ -2175,11 +2441,21 @@ export default class Meeting extends StatelessWebexPlugin {
2175
2441
  // LOCAL - check if we started sharing content
2176
2442
  else if (
2177
2443
  this.selfId === contentShare.beneficiaryId &&
2178
- contentShare.disposition === FLOOR_ACTION.GRANTED
2444
+ contentShare.disposition === FLOOR_ACTION.GRANTED &&
2445
+ contentShare.deviceUrlSharing === this.deviceUrl
2179
2446
  ) {
2180
2447
  // CONTENT - sharing content local
2181
2448
  newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2182
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
+ }
2183
2459
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
2184
2460
  // There is no concept of local/remote share for whiteboard
2185
2461
  // It does not matter who requested to share the whiteboard, everyone gets the same view
@@ -2253,6 +2529,8 @@ export default class Meeting extends StatelessWebexPlugin {
2253
2529
  switch (newShareStatus) {
2254
2530
  case SHARE_STATUS.REMOTE_SHARE_ACTIVE: {
2255
2531
  const sendStartedSharingRemote = () => {
2532
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2533
+
2256
2534
  Trigger.trigger(
2257
2535
  this,
2258
2536
  {
@@ -2263,7 +2541,7 @@ export default class Meeting extends StatelessWebexPlugin {
2263
2541
  {
2264
2542
  memberId: contentShare.beneficiaryId,
2265
2543
  url: contentShare.url,
2266
- shareInstanceId: contentShare.shareInstanceId,
2544
+ shareInstanceId: this.remoteShareInstanceId,
2267
2545
  annotationInfo: contentShare.annotation,
2268
2546
  }
2269
2547
  );
@@ -2300,6 +2578,7 @@ export default class Meeting extends StatelessWebexPlugin {
2300
2578
  name: 'client.share.floor-granted.local',
2301
2579
  payload: {
2302
2580
  mediaType: 'share',
2581
+ shareInstanceId: this.localShareInstanceId,
2303
2582
  },
2304
2583
  options: {meetingId: this.id},
2305
2584
  });
@@ -2342,6 +2621,8 @@ export default class Meeting extends StatelessWebexPlugin {
2342
2621
  } else if (newShareStatus === SHARE_STATUS.REMOTE_SHARE_ACTIVE) {
2343
2622
  // if we got here, then some remote participant has stolen
2344
2623
  // the presentation from another remote participant
2624
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2625
+
2345
2626
  Trigger.trigger(
2346
2627
  this,
2347
2628
  {
@@ -2352,7 +2633,7 @@ export default class Meeting extends StatelessWebexPlugin {
2352
2633
  {
2353
2634
  memberId: contentShare.beneficiaryId,
2354
2635
  url: contentShare.url,
2355
- shareInstanceId: contentShare.shareInstanceId,
2636
+ shareInstanceId: this.remoteShareInstanceId,
2356
2637
  annotationInfo: contentShare.annotation,
2357
2638
  }
2358
2639
  );
@@ -2404,6 +2685,7 @@ export default class Meeting extends StatelessWebexPlugin {
2404
2685
  this.locusId = this.locusUrl?.split('/').pop();
2405
2686
  this.recordingController.setLocusUrl(this.locusUrl);
2406
2687
  this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2688
+ this.webinar.locusUrlUpdate(payload);
2407
2689
 
2408
2690
  Trigger.trigger(
2409
2691
  this,
@@ -2433,6 +2715,10 @@ export default class Meeting extends StatelessWebexPlugin {
2433
2715
  this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2434
2716
  this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2435
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
+ );
2436
2722
  });
2437
2723
  }
2438
2724
 
@@ -2473,12 +2759,24 @@ export default class Meeting extends StatelessWebexPlugin {
2473
2759
  );
2474
2760
  }
2475
2761
  });
2476
- this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, () => {
2762
+ this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, ({isInitializing}) => {
2477
2763
  this.updateMeetingActions();
2478
2764
  this.recordingController.setDisplayHints(this.userDisplayHints);
2479
2765
  this.recordingController.setUserPolicy(this.selfUserPolicies);
2480
2766
  this.controlsOptionsManager.setDisplayHints(this.userDisplayHints);
2481
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
+ }
2482
2780
  });
2483
2781
  }
2484
2782
 
@@ -2704,7 +3002,7 @@ export default class Meeting extends StatelessWebexPlugin {
2704
3002
  });
2705
3003
 
2706
3004
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
2707
- this.simultaneousInterpretation.updateSelfInterpretation(payload);
3005
+ const targetChanged = this.simultaneousInterpretation.updateSelfInterpretation(payload);
2708
3006
  Trigger.trigger(
2709
3007
  this,
2710
3008
  {
@@ -2713,6 +3011,9 @@ export default class Meeting extends StatelessWebexPlugin {
2713
3011
  },
2714
3012
  EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
2715
3013
  );
3014
+ if (targetChanged && this.mediaProperties.audioStream) {
3015
+ this.setSendNamedMediaGroup(MediaType.AudioMain);
3016
+ }
2716
3017
  });
2717
3018
 
2718
3019
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
@@ -2723,6 +3024,7 @@ export default class Meeting extends StatelessWebexPlugin {
2723
3024
  this.simultaneousInterpretation.updateCanManageInterpreters(
2724
3025
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
2725
3026
  );
3027
+ this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR));
2726
3028
  Trigger.trigger(
2727
3029
  this,
2728
3030
  {
@@ -2964,30 +3266,40 @@ export default class Meeting extends StatelessWebexPlugin {
2964
3266
  /**
2965
3267
  * Sets the meeting info on the class instance
2966
3268
  * @param {Object} meetingInfo
2967
- * @param {Object} meetingInfo.body
2968
- * @param {String} meetingInfo.body.conversationUrl
2969
- * @param {String} meetingInfo.body.locusUrl
2970
- * @param {String} meetingInfo.body.sipUri
2971
- * @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
2972
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
2973
3282
  * @returns {undefined}
2974
3283
  * @private
2975
3284
  * @memberof Meeting
2976
3285
  */
2977
3286
  parseMeetingInfo(
2978
- meetingInfo:
2979
- | {
2980
- body: {
2981
- conversationUrl: string;
2982
- locusUrl: string;
2983
- sipUri: string;
2984
- owner: object;
2985
- };
2986
- }
2987
- | any,
2988
- 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
2989
3302
  ) {
2990
- const webexMeetingInfo = meetingInfo?.body;
2991
3303
  // We try to use as much info from Locus meeting object, stored in destination
2992
3304
 
2993
3305
  let locusMeetingObject;
@@ -2997,40 +3309,31 @@ export default class Meeting extends StatelessWebexPlugin {
2997
3309
  }
2998
3310
 
2999
3311
  // MeetingInfo will be undefined for 1:1 calls
3000
- if (
3001
- locusMeetingObject ||
3002
- (webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))
3003
- ) {
3312
+ if (locusMeetingObject || (meetingInfo && !(errors?.length > 0))) {
3004
3313
  this.conversationUrl =
3005
- locusMeetingObject?.conversationUrl ||
3006
- webexMeetingInfo?.conversationUrl ||
3007
- this.conversationUrl;
3008
- this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
3314
+ locusMeetingObject?.conversationUrl || meetingInfo?.conversationUrl || this.conversationUrl;
3315
+ this.locusUrl = locusMeetingObject?.url || meetingInfo?.locusUrl || this.locusUrl;
3009
3316
  // @ts-ignore - config coming from registerPlugin
3010
3317
  this.setSipUri(
3011
3318
  // @ts-ignore
3012
3319
  this.config.experimental.enableUnifiedMeetings
3013
- ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
3014
- : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
3320
+ ? locusMeetingObject?.info.sipUri || meetingInfo?.sipUrl
3321
+ : locusMeetingObject?.info.sipUri || meetingInfo?.sipMeetingUri || this.sipUri
3015
3322
  );
3016
3323
  // @ts-ignore - config coming from registerPlugin
3017
3324
  if (this.config.experimental.enableUnifiedMeetings) {
3018
- this.meetingNumber =
3019
- locusMeetingObject?.info.webExMeetingId || webexMeetingInfo?.meetingNumber;
3020
- this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
3325
+ this.meetingNumber = locusMeetingObject?.info.webExMeetingId || meetingInfo?.meetingNumber;
3326
+ this.meetingJoinUrl = meetingInfo?.meetingJoinUrl;
3021
3327
  }
3022
3328
  this.owner =
3023
- locusMeetingObject?.info.owner ||
3024
- webexMeetingInfo?.owner ||
3025
- webexMeetingInfo?.hostId ||
3026
- this.owner;
3027
- this.permissionToken = webexMeetingInfo?.permissionToken;
3028
- this.setPermissionTokenPayload(webexMeetingInfo?.permissionToken);
3329
+ locusMeetingObject?.info.owner || meetingInfo?.owner || meetingInfo?.hostId || this.owner;
3330
+ this.permissionToken = meetingInfo?.permissionToken;
3331
+ this.setPermissionTokenPayload(meetingInfo?.permissionToken);
3029
3332
  this.setSelfUserPolicies();
3030
3333
  // Need to populate environment when sending CA event
3031
- this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
3334
+ this.environment = locusMeetingObject?.info.channel || meetingInfo?.channel;
3032
3335
  }
3033
- MeetingUtil.parseInterpretationInfo(this, webexMeetingInfo);
3336
+ MeetingUtil.parseInterpretationInfo(this, meetingInfo);
3034
3337
  }
3035
3338
 
3036
3339
  /**
@@ -3082,6 +3385,11 @@ export default class Meeting extends StatelessWebexPlugin {
3082
3385
  }) &&
3083
3386
  this.meetingInfo?.video?.supportHDV) ||
3084
3387
  !this.arePolicyRestrictionsSupported(),
3388
+ enforceVirtualBackground:
3389
+ ControlsOptionsUtil.hasPolicies({
3390
+ requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
3391
+ policies: this.selfUserPolicies,
3392
+ }) && this.arePolicyRestrictionsSupported(),
3085
3393
  supportHQV:
3086
3394
  (ControlsOptionsUtil.hasPolicies({
3087
3395
  requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
@@ -3235,6 +3543,10 @@ export default class Meeting extends StatelessWebexPlugin {
3235
3543
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
3236
3544
  policies: this.selfUserPolicies,
3237
3545
  }),
3546
+ canChat: ControlsOptionsUtil.hasPolicies({
3547
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
3548
+ policies: this.selfUserPolicies,
3549
+ }),
3238
3550
  canShareApplication:
3239
3551
  (ControlsOptionsUtil.hasHints({
3240
3552
  requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
@@ -3294,6 +3606,7 @@ export default class Meeting extends StatelessWebexPlugin {
3294
3606
  */
3295
3607
  setSelfUserPolicies() {
3296
3608
  this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
3609
+ this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
3297
3610
  }
3298
3611
 
3299
3612
  /**
@@ -3304,6 +3617,7 @@ export default class Meeting extends StatelessWebexPlugin {
3304
3617
  */
3305
3618
  public setPermissionTokenPayload(permissionToken: string) {
3306
3619
  this.permissionTokenPayload = jwt.decode(permissionToken);
3620
+ this.permissionTokenReceivedLocalTime = new Date().getTime();
3307
3621
  }
3308
3622
 
3309
3623
  /**
@@ -3397,8 +3711,7 @@ export default class Meeting extends StatelessWebexPlugin {
3397
3711
  * @memberof Meeting
3398
3712
  */
3399
3713
  closeRemoteStreams() {
3400
- const {remoteAudioStream, remoteVideoStream, remoteShareStream, shareAudioStream} =
3401
- this.mediaProperties;
3714
+ const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
3402
3715
 
3403
3716
  /**
3404
3717
  * Triggers an event to the developer
@@ -3439,7 +3752,6 @@ export default class Meeting extends StatelessWebexPlugin {
3439
3752
  stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
3440
3753
  stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
3441
3754
  stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
3442
- stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
3443
3755
  ]);
3444
3756
  }
3445
3757
 
@@ -3453,7 +3765,14 @@ export default class Meeting extends StatelessWebexPlugin {
3453
3765
  private async setLocalAudioStream(localStream?: LocalMicrophoneStream) {
3454
3766
  const oldStream = this.mediaProperties.audioStream;
3455
3767
 
3456
- 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
+ );
3457
3776
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3458
3777
 
3459
3778
  // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
@@ -3461,7 +3780,14 @@ export default class Meeting extends StatelessWebexPlugin {
3461
3780
 
3462
3781
  this.audio.handleLocalStreamChange(this);
3463
3782
 
3464
- 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
+ );
3465
3791
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3466
3792
 
3467
3793
  if (!this.isMultistream || !localStream) {
@@ -3481,7 +3807,14 @@ export default class Meeting extends StatelessWebexPlugin {
3481
3807
  private async setLocalVideoStream(localStream?: LocalCameraStream) {
3482
3808
  const oldStream = this.mediaProperties.videoStream;
3483
3809
 
3484
- 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
+ );
3485
3818
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3486
3819
 
3487
3820
  // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
@@ -3489,7 +3822,14 @@ export default class Meeting extends StatelessWebexPlugin {
3489
3822
 
3490
3823
  this.video.handleLocalStreamChange(this);
3491
3824
 
3492
- 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
+ );
3493
3833
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3494
3834
 
3495
3835
  if (!this.isMultistream || !localStream) {
@@ -3510,11 +3850,19 @@ export default class Meeting extends StatelessWebexPlugin {
3510
3850
  private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3511
3851
  const oldStream = this.mediaProperties.shareVideoStream;
3512
3852
 
3853
+ oldStream?.off(
3854
+ LocalStreamEventNames.SystemMuteStateChange,
3855
+ this.handleShareVideoStreamMuteStateChange
3856
+ );
3513
3857
  oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3514
3858
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3515
3859
 
3516
3860
  this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3517
3861
 
3862
+ localDisplayStream?.on(
3863
+ LocalStreamEventNames.SystemMuteStateChange,
3864
+ this.handleShareVideoStreamMuteStateChange
3865
+ );
3518
3866
  localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3519
3867
  localDisplayStream?.on(
3520
3868
  LocalStreamEventNames.OutputTrackChange,
@@ -3570,7 +3918,7 @@ export default class Meeting extends StatelessWebexPlugin {
3570
3918
  functionName: string;
3571
3919
  isPublished: boolean;
3572
3920
  mediaType: MediaType;
3573
- stream: MediaStream;
3921
+ stream: LocalStream;
3574
3922
  }) {
3575
3923
  const {functionName, isPublished, mediaType, stream} = options;
3576
3924
  Trigger.trigger(
@@ -3598,18 +3946,37 @@ export default class Meeting extends StatelessWebexPlugin {
3598
3946
  public cleanupLocalStreams() {
3599
3947
  const {audioStream, videoStream, shareAudioStream, shareVideoStream} = this.mediaProperties;
3600
3948
 
3601
- 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
+ );
3602
3957
  audioStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3603
3958
 
3604
- 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
+ );
3605
3967
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3606
3968
 
3607
- shareAudioStream?.off(StreamEventNames.MuteStateChange, this.handleShareAudioStreamEnded);
3969
+ shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3608
3970
  shareAudioStream?.off(
3609
3971
  LocalStreamEventNames.OutputTrackChange,
3610
3972
  this.localOutputTrackChangeHandler
3611
3973
  );
3612
- shareVideoStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamEnded);
3974
+
3975
+ shareVideoStream?.off(
3976
+ LocalStreamEventNames.SystemMuteStateChange,
3977
+ this.handleShareVideoStreamMuteStateChange
3978
+ );
3979
+ shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3613
3980
  shareVideoStream?.off(
3614
3981
  LocalStreamEventNames.OutputTrackChange,
3615
3982
  this.localOutputTrackChangeHandler
@@ -3723,6 +4090,7 @@ export default class Meeting extends StatelessWebexPlugin {
3723
4090
  this.receiveSlotManager.reset();
3724
4091
  this.mediaProperties.webrtcMediaConnection.close();
3725
4092
  this.sendSlotManager.reset();
4093
+ this.setNetworkStatus(undefined);
3726
4094
  }
3727
4095
 
3728
4096
  this.audio = null;
@@ -3744,18 +4112,31 @@ export default class Meeting extends StatelessWebexPlugin {
3744
4112
  if (this.config.reconnection.detection) {
3745
4113
  // @ts-ignore
3746
4114
  this.webex.internal.mercury.off(ONLINE);
4115
+ // @ts-ignore
4116
+ this.webex.internal.mercury.off(OFFLINE);
3747
4117
  }
3748
4118
  }
3749
4119
 
3750
4120
  /**
3751
- * Convenience method to set the correlation id for the Meeting
3752
- * @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
3753
4123
  * @returns {undefined}
3754
- * @private
4124
+ * @public
3755
4125
  * @memberof Meeting
3756
4126
  */
3757
- private setCorrelationId(id: string) {
3758
- this.correlationId = id;
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
4136
+ * @memberof Meeting
4137
+ */
4138
+ public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
4139
+ this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
3759
4140
  }
3760
4141
 
3761
4142
  /**
@@ -3992,6 +4373,14 @@ export default class Meeting extends StatelessWebexPlugin {
3992
4373
  ) {
3993
4374
  const {mediaOptions, joinOptions} = options;
3994
4375
 
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
+
3995
4384
  return this.join(joinOptions)
3996
4385
  .then((joinResponse) =>
3997
4386
  this.addMedia(mediaOptions).then((mediaResponse) => ({
@@ -4073,6 +4462,8 @@ export default class Meeting extends StatelessWebexPlugin {
4073
4462
 
4074
4463
  return this.reconnectionManager
4075
4464
  .reconnect(options)
4465
+ .then(() => this.waitForRemoteSDPAnswer())
4466
+ .then(() => this.waitForMediaConnectionConnected())
4076
4467
  .then(() => {
4077
4468
  Trigger.trigger(
4078
4469
  this,
@@ -4083,10 +4474,22 @@ export default class Meeting extends StatelessWebexPlugin {
4083
4474
  EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
4084
4475
  );
4085
4476
  LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
4086
- })
4087
- .catch((error) => {
4088
- Trigger.trigger(
4089
- this,
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);
4489
+ })
4490
+ .catch((error) => {
4491
+ Trigger.trigger(
4492
+ this,
4090
4493
  {
4091
4494
  file: 'meeting/index',
4092
4495
  function: 'reconnect',
@@ -4338,7 +4741,7 @@ export default class Meeting extends StatelessWebexPlugin {
4338
4741
  * if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
4339
4742
  * Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
4340
4743
  */
4341
- public join(options: any = {}) {
4744
+ public async join(options: any = {}) {
4342
4745
  // @ts-ignore - fix type
4343
4746
  if (!this.webex.meetings.registered) {
4344
4747
  const errorMessage = 'Meeting:index#join --> Device not registered';
@@ -4392,27 +4795,14 @@ export default class Meeting extends StatelessWebexPlugin {
4392
4795
  // @ts-ignore
4393
4796
  this.webex.internal.newMetrics.submitClientEvent({
4394
4797
  name: 'client.call.initiated',
4395
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
4798
+ payload: {
4799
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
4800
+ isRoapCallEnabled: true,
4801
+ pstnAudioType: options?.pstnAudioType,
4802
+ },
4396
4803
  options: {meetingId: this.id},
4397
4804
  });
4398
4805
 
4399
- if (!isEmpty(this.meetingInfo)) {
4400
- // @ts-ignore
4401
- this.webex.internal.newMetrics.submitClientEvent({
4402
- name: 'client.meetinginfo.request',
4403
- options: {meetingId: this.id},
4404
- });
4405
-
4406
- // @ts-ignore
4407
- this.webex.internal.newMetrics.submitClientEvent({
4408
- name: 'client.meetinginfo.response',
4409
- payload: {
4410
- identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
4411
- },
4412
- options: {meetingId: this.id},
4413
- });
4414
- }
4415
-
4416
4806
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4417
4807
 
4418
4808
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -4466,44 +4856,55 @@ export default class Meeting extends StatelessWebexPlugin {
4466
4856
 
4467
4857
  this.isMultistream = !!options.enableMultistream;
4468
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
+
4469
4896
  return MeetingUtil.joinMeetingOptions(this, options)
4470
4897
  .then((join) => {
4471
4898
  this.meetingFiniteStateMachine.join();
4472
4899
  LoggerProxy.logger.log('Meeting:index#join --> Success');
4473
4900
 
4474
- return join;
4475
- })
4476
- .then((join) => {
4477
- joinSuccess(join);
4478
- this.deferJoin = undefined;
4479
4901
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
4480
4902
  correlation_id: this.correlationId,
4481
4903
  });
4482
4904
 
4483
- return join;
4484
- })
4485
- .then(async (join) => {
4486
- // @ts-ignore - config coming from registerPlugin
4487
- if (this.config.enableAutomaticLLM) {
4488
- await this.updateLLMConnection();
4489
- }
4905
+ joinSuccess(join);
4490
4906
 
4491
- return join;
4492
- })
4493
- .then(async (join) => {
4494
- if (isBrowser) {
4495
- // @ts-ignore - config coming from registerPlugin
4496
- if (this.config.receiveTranscription || options.receiveTranscription) {
4497
- if (this.isTranscriptionSupported()) {
4498
- await this.receiveTranscription();
4499
- LoggerProxy.logger.info('Meeting:index#join --> enabled to recieve transcription!');
4500
- }
4501
- }
4502
- } else {
4503
- LoggerProxy.logger.error(
4504
- 'Meeting:index#join --> Receving transcription is not supported on this platform'
4505
- );
4506
- }
4907
+ this.deferJoin = undefined;
4507
4908
 
4508
4909
  return join;
4509
4910
  })
@@ -4539,9 +4940,59 @@ export default class Meeting extends StatelessWebexPlugin {
4539
4940
  );
4540
4941
 
4541
4942
  joinFailed(error);
4943
+
4542
4944
  this.deferJoin = undefined;
4543
4945
 
4544
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;
4545
4996
  });
4546
4997
  }
4547
4998
 
@@ -4921,7 +5372,74 @@ export default class Meeting extends StatelessWebexPlugin {
4921
5372
  }
4922
5373
  };
4923
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
+
4924
5440
  setupMediaConnectionListeners = () => {
5441
+ this.setupSdpListeners();
5442
+
4925
5443
  this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4926
5444
  this.isRoapInProgress = true;
4927
5445
  });
@@ -4939,12 +5457,6 @@ export default class Meeting extends StatelessWebexPlugin {
4939
5457
 
4940
5458
  switch (event.roapMessage.messageType) {
4941
5459
  case 'OK':
4942
- // @ts-ignore
4943
- this.webex.internal.newMetrics.submitClientEvent({
4944
- name: 'client.media-engine.remote-sdp-received',
4945
- options: {meetingId: this.id},
4946
- });
4947
-
4948
5460
  logRequest(
4949
5461
  this.roap.sendRoapOK({
4950
5462
  seq: event.roapMessage.seq,
@@ -4958,33 +5470,32 @@ export default class Meeting extends StatelessWebexPlugin {
4958
5470
  break;
4959
5471
 
4960
5472
  case 'OFFER':
4961
- // @ts-ignore
4962
- this.webex.internal.newMetrics.submitClientEvent({
4963
- name: 'client.media-engine.local-sdp-generated',
4964
- options: {meetingId: this.id},
4965
- });
4966
-
4967
5473
  logRequest(
4968
- this.roap.sendRoapMediaRequest({
4969
- sdp: event.roapMessage.sdp,
4970
- seq: event.roapMessage.seq,
4971
- tieBreaker: event.roapMessage.tieBreaker,
4972
- meeting: this, // or can pass meeting ID
4973
- reconnect: this.reconnectionManager.isReconnectInProgress(),
4974
- }),
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
+ }),
4975
5488
  {
4976
5489
  logText: `${LOG_HEADER} Roap Offer`,
4977
5490
  }
4978
- );
5491
+ ).catch(() => {
5492
+ this.deferSDPAnswer.reject();
5493
+ clearTimeout(this.sdpResponseTimer);
5494
+ this.sdpResponseTimer = undefined;
5495
+ });
4979
5496
  break;
4980
5497
 
4981
5498
  case 'ANSWER':
4982
- // @ts-ignore
4983
- this.webex.internal.newMetrics.submitClientEvent({
4984
- name: 'client.media-engine.remote-sdp-received',
4985
- options: {meetingId: this.id},
4986
- });
4987
-
4988
5499
  logRequest(
4989
5500
  this.roap.sendRoapAnswer({
4990
5501
  sdp: event.roapMessage.sdp,
@@ -5097,70 +5608,71 @@ export default class Meeting extends StatelessWebexPlugin {
5097
5608
 
5098
5609
  this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5099
5610
  const connectionFailed = () => {
5100
- // we know the media connection failed and browser will not attempt to recover it any more
5101
- // so reset the timer as it's not needed anymore, we want to reconnect immediately
5102
- this.reconnectionManager.resetReconnectionTimer();
5103
-
5104
- this.reconnect({networkDisconnect: true});
5105
- // @ts-ignore
5106
- this.webex.internal.newMetrics.submitClientEvent({
5107
- name: 'client.ice.end',
5108
- payload: {
5109
- canProceed: false,
5110
- icePhase: 'IN_MEETING',
5111
- errors: [
5112
- // @ts-ignore
5113
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5114
- {
5115
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
5116
- }
5117
- ),
5118
- ],
5119
- },
5120
- options: {
5121
- meetingId: this.id,
5122
- },
5123
- });
5124
-
5125
- this.uploadLogs({
5126
- file: 'peer-connection-manager/index',
5127
- function: 'connectionFailed',
5128
- });
5129
-
5130
5611
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5131
5612
  correlation_id: this.correlationId,
5132
5613
  locus_id: this.locusId,
5614
+ networkStatus: this.networkStatus,
5615
+ hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
5133
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
+ }
5134
5630
  };
5135
5631
 
5136
5632
  LoggerProxy.logger.info(
5137
5633
  `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
5138
5634
  );
5635
+
5636
+ // @ts-ignore
5637
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5638
+
5139
5639
  switch (event.state) {
5140
5640
  case ConnectionState.Connecting:
5141
- // @ts-ignore
5142
- this.webex.internal.newMetrics.submitClientEvent({
5143
- name: 'client.ice.start',
5144
- options: {
5145
- meetingId: this.id,
5146
- },
5147
- });
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
+ }
5148
5651
  break;
5149
5652
  case ConnectionState.Connected:
5150
- // @ts-ignore
5151
- this.webex.internal.newMetrics.submitClientEvent({
5152
- name: 'client.ice.end',
5153
- options: {
5154
- meetingId: this.id,
5155
- },
5156
- });
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
+ }
5157
5667
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5158
5668
  correlation_id: this.correlationId,
5159
5669
  locus_id: this.locusId,
5670
+ latency: cdl.getICESetupTime(),
5160
5671
  });
5161
5672
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
5162
5673
  this.reconnectionManager.iceReconnected();
5163
5674
  this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
5675
+ this.hasMediaConnectionConnectedAtLeastOnce = true;
5164
5676
  break;
5165
5677
  case ConnectionState.Disconnected:
5166
5678
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
@@ -5281,7 +5793,10 @@ export default class Meeting extends StatelessWebexPlugin {
5281
5793
  // @ts-ignore
5282
5794
  this.webex.internal.newMetrics.submitClientEvent({
5283
5795
  name: 'client.media.tx.start',
5284
- payload: {mediaType: data.type},
5796
+ payload: {
5797
+ mediaType: data.type,
5798
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5799
+ },
5285
5800
  options: {
5286
5801
  meetingId: this.id,
5287
5802
  },
@@ -5291,7 +5806,10 @@ export default class Meeting extends StatelessWebexPlugin {
5291
5806
  // @ts-ignore
5292
5807
  this.webex.internal.newMetrics.submitClientEvent({
5293
5808
  name: 'client.media.tx.stop',
5294
- payload: {mediaType: data.type},
5809
+ payload: {
5810
+ mediaType: data.type,
5811
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5812
+ },
5295
5813
  options: {
5296
5814
  meetingId: this.id,
5297
5815
  },
@@ -5310,7 +5828,10 @@ export default class Meeting extends StatelessWebexPlugin {
5310
5828
  // @ts-ignore
5311
5829
  this.webex.internal.newMetrics.submitClientEvent({
5312
5830
  name: 'client.media.rx.start',
5313
- payload: {mediaType: data.type},
5831
+ payload: {
5832
+ mediaType: data.type,
5833
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5834
+ },
5314
5835
  options: {
5315
5836
  meetingId: this.id,
5316
5837
  },
@@ -5320,7 +5841,10 @@ export default class Meeting extends StatelessWebexPlugin {
5320
5841
  // @ts-ignore
5321
5842
  this.webex.internal.newMetrics.submitClientEvent({
5322
5843
  name: 'client.media.rx.stop',
5323
- payload: {mediaType: data.type},
5844
+ payload: {
5845
+ mediaType: data.type,
5846
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5847
+ },
5324
5848
  options: {
5325
5849
  meetingId: this.id,
5326
5850
  },
@@ -5373,14 +5897,15 @@ export default class Meeting extends StatelessWebexPlugin {
5373
5897
  this.mediaProperties.mediaDirection.receiveShare,
5374
5898
  ];
5375
5899
 
5376
- this.sendSlotManager.createSlot(mc, MediaType.VideoMain, audioEnabled);
5377
- this.sendSlotManager.createSlot(mc, MediaType.AudioMain, videoEnabled);
5900
+ this.sendSlotManager.createSlot(mc, MediaType.VideoMain, videoEnabled);
5901
+ this.sendSlotManager.createSlot(mc, MediaType.AudioMain, audioEnabled);
5378
5902
  this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
5379
5903
  this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
5380
5904
  }
5381
5905
 
5382
5906
  // publish the streams
5383
5907
  if (this.mediaProperties.audioStream) {
5908
+ this.setSendNamedMediaGroup(MediaType.AudioMain);
5384
5909
  await this.publishStream(MediaType.AudioMain, this.mediaProperties.audioStream);
5385
5910
  }
5386
5911
  if (this.mediaProperties.videoStream) {
@@ -5397,50 +5922,469 @@ export default class Meeting extends StatelessWebexPlugin {
5397
5922
  }
5398
5923
 
5399
5924
  /**
5400
- * Listens for an event emitted by eventEmitter and emits it from the meeting object
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
5401
6341
  *
5402
6342
  * @private
5403
- * @param {*} eventEmitter object from which to forward the event
5404
- * @param {*} eventTypeToForward which event type to listen on and to forward
5405
- * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
5406
- * @returns {void}
6343
+ * @returns {Promise<void>}
5407
6344
  */
5408
- forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
5409
- eventEmitter.on(eventTypeToForward, (data) =>
5410
- Trigger.trigger(
5411
- this,
5412
- {
5413
- file: 'meetings',
5414
- function: 'addMedia',
5415
- },
5416
- meetingEventType,
5417
- data
5418
- )
5419
- );
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
+ }
5420
6366
  }
5421
6367
 
5422
6368
  /**
5423
6369
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
5424
6370
  *
5425
6371
  * @param {AddMediaOptions} options
5426
- * @returns {Promise}
6372
+ * @returns {Promise<void>}
5427
6373
  * @public
5428
6374
  * @memberof Meeting
5429
6375
  */
5430
- addMedia(options: AddMediaOptions = {}) {
6376
+ async addMedia(options: AddMediaOptions = {}): Promise<void> {
6377
+ this.retriedWithTurnServer = false;
6378
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
5431
6379
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5432
-
5433
- let turnDiscoverySkippedReason;
5434
- let turnServerUsed = false;
5435
-
5436
6380
  LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5437
6381
 
5438
- if (this.meetingState !== FULL_STATE.ACTIVE) {
5439
- return Promise.reject(new MeetingNotActiveError());
6382
+ if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
6383
+ throw new MeetingNotActiveError();
5440
6384
  }
5441
6385
 
5442
6386
  if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
5443
- return Promise.reject(new UserNotJoinedError());
6387
+ throw new UserNotJoinedError();
5444
6388
  }
5445
6389
 
5446
6390
  const {
@@ -5459,7 +6403,7 @@ export default class Meeting extends StatelessWebexPlugin {
5459
6403
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
5460
6404
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
5461
6405
  if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
5462
- return Promise.reject(new UserInLobbyError());
6406
+ throw new UserInLobbyError();
5463
6407
  }
5464
6408
 
5465
6409
  // @ts-ignore
@@ -5519,240 +6463,100 @@ export default class Meeting extends StatelessWebexPlugin {
5519
6463
 
5520
6464
  this.audio = createMuteState(AUDIO, this, audioEnabled);
5521
6465
  this.video = createMuteState(VIDEO, this, videoEnabled);
5522
- const promises = [];
5523
-
5524
- // setup all the references to local streams in this.mediaProperties before creating media connection
5525
- // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5526
- if (localStreams?.microphone) {
5527
- promises.push(this.setLocalAudioStream(localStreams.microphone));
5528
- }
5529
- if (localStreams?.camera) {
5530
- promises.push(this.setLocalVideoStream(localStreams.camera));
5531
- }
5532
- if (localStreams?.screenShare?.video) {
5533
- promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5534
- }
5535
- if (localStreams?.screenShare?.audio) {
5536
- promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5537
- }
5538
-
5539
- return Promise.all(promises)
5540
- .then(() => this.roap.doTurnDiscovery(this, false))
5541
- .then(async (turnDiscoveryObject) => {
5542
- ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5543
- turnServerUsed = !turnDiscoverySkippedReason;
5544
-
5545
- const {turnServerInfo} = turnDiscoveryObject;
5546
6466
 
5547
- const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5548
-
5549
- if (this.isMultistream) {
5550
- this.remoteMediaManager = new RemoteMediaManager(
5551
- this.receiveSlotManager,
5552
- this.mediaRequestManagers,
5553
- remoteMediaManagerConfig
5554
- );
5555
-
5556
- this.forwardEvent(
5557
- this.remoteMediaManager,
5558
- RemoteMediaManagerEvent.AudioCreated,
5559
- EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
5560
- );
5561
- this.forwardEvent(
5562
- this.remoteMediaManager,
5563
- RemoteMediaManagerEvent.ScreenShareAudioCreated,
5564
- EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
5565
- );
5566
- this.forwardEvent(
5567
- this.remoteMediaManager,
5568
- RemoteMediaManagerEvent.VideoLayoutChanged,
5569
- EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5570
- );
6467
+ try {
6468
+ await this.setUpLocalStreamReferences(localStreams);
5571
6469
 
5572
- await this.remoteMediaManager.start();
5573
- }
6470
+ this.setMercuryListener();
5574
6471
 
5575
- await mc.initiateOffer();
5576
- })
5577
- .then(() => {
5578
- this.setMercuryListener();
5579
- })
5580
- .then(
5581
- () =>
5582
- getDevices()
5583
- .then((devices) => {
5584
- MeetingUtil.handleDeviceLogging(devices);
5585
- })
5586
- .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
5587
- )
5588
- .then(() => {
5589
- this.handleMediaLogging(this.mediaProperties);
5590
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6472
+ this.createStatsAnalyzer();
5591
6473
 
5592
- // @ts-ignore - config coming from registerPlugin
5593
- if (this.config.stats.enableStatsAnalyzer) {
5594
- // @ts-ignore - config coming from registerPlugin
5595
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5596
- this.statsAnalyzer = new StatsAnalyzer(
5597
- // @ts-ignore - config coming from registerPlugin
5598
- this.config.stats,
5599
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5600
- this.networkQualityMonitor
5601
- );
5602
- this.setupStatsAnalyzerEventHandlers();
5603
- this.networkQualityMonitor.on(
5604
- EVENT_TRIGGERS.NETWORK_QUALITY,
5605
- this.sendNetworkQualityEvent.bind(this)
5606
- );
5607
- }
5608
- })
5609
- .catch((error) => {
5610
- LoggerProxy.logger.error(
5611
- `${LOG_HEADER} Error adding media , setting up peerconnection, `,
5612
- error
5613
- );
6474
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
5614
6475
 
5615
- throw error;
5616
- })
5617
- .then(
5618
- () =>
5619
- new Promise<void>((resolve, reject) => {
5620
- let timerCount = 0;
5621
-
5622
- // eslint-disable-next-line func-names
5623
- // eslint-disable-next-line prefer-arrow-callback
5624
- if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5625
- resolve();
5626
- }
5627
- const joiningTimer = setInterval(() => {
5628
- timerCount += 1;
5629
- if (this.meetingState === FULL_STATE.ACTIVE) {
5630
- clearInterval(joiningTimer);
5631
- resolve();
5632
- }
6476
+ await Meeting.handleDeviceLogging();
5633
6477
 
5634
- if (timerCount === 4) {
5635
- clearInterval(joiningTimer);
5636
- reject(new Error('Meeting is still not active '));
5637
- }
5638
- }, 1000);
5639
- })
5640
- )
5641
- .then(() =>
5642
- this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5643
- // @ts-ignore
5644
- this.webex.internal.newMetrics.submitClientEvent({
5645
- name: 'client.ice.end',
5646
- payload: {
5647
- canProceed: false,
5648
- icePhase: 'JOIN_MEETING_FINAL',
5649
- errors: [
5650
- // @ts-ignore
5651
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5652
- {
5653
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
5654
- }
5655
- ),
5656
- ],
5657
- },
5658
- options: {
5659
- meetingId: this.id,
5660
- },
5661
- });
5662
- throw new Error(
5663
- `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5664
- );
5665
- })
5666
- )
5667
- .then(() => {
5668
- if (this.mediaProperties.hasLocalShareStream()) {
5669
- return this.enqueueScreenShareFloorRequest();
5670
- }
6478
+ if (this.mediaProperties.hasLocalShareStream()) {
6479
+ await this.enqueueScreenShareFloorRequest();
6480
+ }
5671
6481
 
5672
- return Promise.resolve();
5673
- })
5674
- .then(() => this.mediaProperties.getCurrentConnectionType())
5675
- .then((connectionType) => {
5676
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
5677
- correlation_id: this.correlationId,
5678
- locus_id: this.locusUrl.split('/').pop(),
5679
- connectionType,
5680
- isMultistream: this.isMultistream,
5681
- });
5682
- // @ts-ignore
5683
- this.webex.internal.newMetrics.submitClientEvent({
5684
- name: 'client.media-engine.ready',
5685
- options: {
5686
- meetingId: this.id,
5687
- },
5688
- });
5689
- LoggerProxy.logger.info(
5690
- `${LOG_HEADER} successfully established media connection, type=${connectionType}`
5691
- );
6482
+ const connectionType = await this.mediaProperties.getCurrentConnectionType();
6483
+ // @ts-ignore
6484
+ const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5692
6485
 
5693
- // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
5694
- this.remoteMediaManager?.logAllReceiveSlots();
5695
- })
5696
- .catch((error) => {
5697
- LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
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
+ );
5698
6504
 
5699
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5700
- correlation_id: this.correlationId,
5701
- locus_id: this.locusUrl.split('/').pop(),
5702
- reason: error.message,
5703
- stack: error.stack,
5704
- code: error.code,
5705
- turnDiscoverySkippedReason,
5706
- turnServerUsed,
5707
- isMultistream: this.isMultistream,
5708
- signalingState:
5709
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5710
- ?.signalingState ||
5711
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5712
- 'unknown',
5713
- connectionState:
5714
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5715
- ?.connectionState ||
5716
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5717
- 'unknown',
5718
- iceConnectionState:
5719
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5720
- ?.iceConnectionState ||
5721
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5722
- 'unknown',
5723
- });
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);
5724
6509
 
5725
- // Clean up stats analyzer, peer connection, and turn off listeners
5726
- const stopStatsAnalyzer = this.statsAnalyzer
5727
- ? this.statsAnalyzer.stopAnalyzer()
5728
- : Promise.resolve();
6510
+ // @ts-ignore
6511
+ const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
5729
6512
 
5730
- return stopStatsAnalyzer.then(() => {
5731
- this.statsAnalyzer = null;
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
+ });
5732
6540
 
5733
- if (this.mediaProperties.webrtcMediaConnection) {
5734
- this.closePeerConnections();
5735
- this.unsetPeerConnections();
5736
- }
6541
+ await this.cleanUpOnAddMediaFailure();
5737
6542
 
5738
- // Upload logs on error while adding media
5739
- Trigger.trigger(
5740
- this,
5741
- {
5742
- file: 'meeting/index',
5743
- function: 'addMedia',
5744
- },
5745
- EVENTS.REQUEST_UPLOAD_LOGS,
5746
- this
5747
- );
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
+ );
5748
6553
 
5749
- if (error instanceof Errors.SdpError) {
5750
- this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5751
- }
6554
+ if (error instanceof Errors.SdpError) {
6555
+ this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
6556
+ }
5752
6557
 
5753
- throw error;
5754
- });
5755
- });
6558
+ throw error;
6559
+ }
5756
6560
  }
5757
6561
 
5758
6562
  /**
@@ -5766,6 +6570,24 @@ export default class Meeting extends StatelessWebexPlugin {
5766
6570
  return !this.isRoapInProgress;
5767
6571
  }
5768
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
+
5769
6591
  /**
5770
6592
  * Enqueues a media update operation.
5771
6593
  * @param {String} mediaUpdateType one of MEDIA_UPDATE_TYPE values
@@ -6231,17 +7053,13 @@ export default class Meeting extends StatelessWebexPlugin {
6231
7053
  .catch((error) => {
6232
7054
  LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
6233
7055
 
6234
- Metrics.sendBehavioralMetric(
6235
- // @ts-ignore - check if STOP_WHITEBOARD_SHARE_FAILURE exists
6236
- BEHAVIORAL_METRICS.STOP_WHITEBOARD_SHARE_FAILURE,
6237
- {
6238
- correlation_id: this.correlationId,
6239
- locus_id: this.locusUrl.split('/').pop(),
6240
- reason: error.message,
6241
- stack: error.stack,
6242
- board: {channelUrl},
6243
- }
6244
- );
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
+ });
6245
7063
 
6246
7064
  return Promise.reject(error);
6247
7065
  })
@@ -6278,11 +7096,14 @@ export default class Meeting extends StatelessWebexPlugin {
6278
7096
  if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
6279
7097
  // @ts-ignore
6280
7098
  this.webex.internal.newMetrics.submitClientEvent({
6281
- name: 'client.share.initiated',
7099
+ name: 'client.share.floor-grant.request',
6282
7100
  payload: {
6283
7101
  mediaType: 'share',
7102
+ shareInstanceId: this.localShareInstanceId,
7103
+ },
7104
+ options: {
7105
+ meetingId: this.id,
6284
7106
  },
6285
- options: {meetingId: this.id},
6286
7107
  });
6287
7108
 
6288
7109
  return this.meetingRequest
@@ -6292,10 +7113,16 @@ export default class Meeting extends StatelessWebexPlugin {
6292
7113
  deviceUrl: this.deviceUrl,
6293
7114
  uri: content.url,
6294
7115
  resourceUrl: this.resourceUrl,
7116
+ shareInstanceId: this.localShareInstanceId,
6295
7117
  })
6296
7118
  .then(() => {
6297
7119
  this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
6298
7120
 
7121
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
7122
+ correlation_id: this.correlationId,
7123
+ locus_id: this.locusUrl.split('/').pop(),
7124
+ });
7125
+
6299
7126
  return Promise.resolve();
6300
7127
  })
6301
7128
  .catch((error) => {
@@ -6308,6 +7135,19 @@ export default class Meeting extends StatelessWebexPlugin {
6308
7135
  stack: error.stack,
6309
7136
  });
6310
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
+
6311
7151
  this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
6312
7152
 
6313
7153
  return Promise.reject(error);
@@ -6359,6 +7199,7 @@ export default class Meeting extends StatelessWebexPlugin {
6359
7199
  name: 'client.share.stopped',
6360
7200
  payload: {
6361
7201
  mediaType: 'share',
7202
+ shareInstanceId: this.localShareInstanceId,
6362
7203
  },
6363
7204
  options: {meetingId: this.id},
6364
7205
  });
@@ -6375,6 +7216,7 @@ export default class Meeting extends StatelessWebexPlugin {
6375
7216
  deviceUrl: this.deviceUrl,
6376
7217
  uri: content.url,
6377
7218
  resourceUrl: this.resourceUrl,
7219
+ shareInstanceId: this.localShareInstanceId,
6378
7220
  })
6379
7221
  .catch((error) => {
6380
7222
  LoggerProxy.logger.error('Meeting:index#releaseScreenShareFloor --> Error ', error);
@@ -6579,7 +7421,7 @@ export default class Meeting extends StatelessWebexPlugin {
6579
7421
  if (layoutType) {
6580
7422
  if (!LAYOUT_TYPES.includes(layoutType)) {
6581
7423
  this.rejectWithErrorLog(
6582
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType recieved.'
7424
+ 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
6583
7425
  );
6584
7426
  }
6585
7427
 
@@ -6717,6 +7559,23 @@ export default class Meeting extends StatelessWebexPlugin {
6717
7559
  }
6718
7560
  };
6719
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
+
6720
7579
  /**
6721
7580
  * Functionality for when a share video is ended.
6722
7581
  * @private
@@ -7083,10 +7942,12 @@ export default class Meeting extends StatelessWebexPlugin {
7083
7942
  .update({
7084
7943
  // TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
7085
7944
  localTracks: {
7086
- audio: this.mediaProperties.audioStream?.outputTrack || null,
7087
- video: this.mediaProperties.videoStream?.outputTrack || null,
7088
- screenShareVideo: this.mediaProperties.shareVideoStream?.outputTrack || null,
7089
- 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,
7090
7951
  },
7091
7952
  direction: {
7092
7953
  audio: Media.getDirection(
@@ -7124,6 +7985,33 @@ export default class Meeting extends StatelessWebexPlugin {
7124
7985
  });
7125
7986
  }
7126
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
+
7127
8015
  /**
7128
8016
  * Publishes a stream.
7129
8017
  *
@@ -7220,6 +8108,23 @@ export default class Meeting extends StatelessWebexPlugin {
7220
8108
  }
7221
8109
 
7222
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
+ });
7223
8128
  // we're sending the http request to Locus to request the screen share floor
7224
8129
  // only after the SDP update, because that's how it's always been done for transcoded meetings
7225
8130
  // and also if sharing from the start, we need confluence to have been created
@@ -7268,6 +8173,12 @@ export default class Meeting extends StatelessWebexPlugin {
7268
8173
  if (!this.mediaProperties.hasLocalShareStream()) {
7269
8174
  try {
7270
8175
  this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
8176
+
8177
+ this.statsAnalyzer.updateMediaStatus({
8178
+ expected: {
8179
+ sendShare: false,
8180
+ },
8181
+ });
7271
8182
  } catch (e) {
7272
8183
  // nothing to do here, error is logged already inside releaseScreenShareFloor()
7273
8184
  }
@@ -7275,24 +8186,51 @@ export default class Meeting extends StatelessWebexPlugin {
7275
8186
  }
7276
8187
 
7277
8188
  /**
7278
- * Gets the time left in seconds till the permission token expires
8189
+ * Gets permission token expiry information including timeLeft, expiryTime, currentTime
7279
8190
  * (from the time the function has been fired)
7280
8191
  *
7281
- * @returns {number} time left in seconds
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
7282
8196
  */
7283
- public getPermissionTokenTimeLeftInSec(): number | undefined {
8197
+ public getPermissionTokenExpiryInfo() {
7284
8198
  if (!this.permissionTokenPayload) {
7285
8199
  return undefined;
7286
8200
  }
7287
8201
 
7288
- const permissionTokenExpValue = Number(this.permissionTokenPayload.exp);
8202
+ const permissionTokenExpiryFromServer = Number(this.permissionTokenPayload.exp);
8203
+ const permissionTokenIssuedTimeFromServer = Number(this.permissionTokenPayload.iat);
8204
+
8205
+ const shiftInTime = this.permissionTokenReceivedLocalTime - permissionTokenIssuedTimeFromServer;
7289
8206
 
7290
8207
  // using new Date instead of Date.now() to allow for accurate unit testing
7291
8208
  // https://github.com/sinonjs/fake-timers/issues/321
7292
- const now = new Date().getTime();
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
+ }
7293
8218
 
7294
- // substract current time from the permissionTokenExp
7295
- // (permissionTokenExp is a epoch timestamp, not a time to live duration)
7296
- return (permissionTokenExpValue - now) / 1000;
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();
7297
8235
  }
7298
8236
  }