@webex/plugin-meetings 3.7.0-next.6 → 3.7.0-next.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/annotation/index.js +17 -0
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/join-forbidden-error.js +52 -0
  6. package/dist/common/errors/join-forbidden-error.js.map +1 -0
  7. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  8. package/dist/common/errors/join-webinar-error.js.map +1 -0
  9. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  10. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  11. package/dist/config.js +1 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/constants.js +46 -5
  14. package/dist/constants.js.map +1 -1
  15. package/dist/index.js +16 -11
  16. package/dist/index.js.map +1 -1
  17. package/dist/interpretation/index.js +1 -1
  18. package/dist/interpretation/siLanguage.js +1 -1
  19. package/dist/locus-info/index.js +14 -3
  20. package/dist/locus-info/index.js.map +1 -1
  21. package/dist/locus-info/selfUtils.js +35 -17
  22. package/dist/locus-info/selfUtils.js.map +1 -1
  23. package/dist/meeting/brbState.js +167 -0
  24. package/dist/meeting/brbState.js.map +1 -0
  25. package/dist/meeting/in-meeting-actions.js +2 -0
  26. package/dist/meeting/in-meeting-actions.js.map +1 -1
  27. package/dist/meeting/index.js +774 -649
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/locusMediaRequest.js +9 -0
  30. package/dist/meeting/locusMediaRequest.js.map +1 -1
  31. package/dist/meeting/muteState.js +1 -6
  32. package/dist/meeting/muteState.js.map +1 -1
  33. package/dist/meeting/request.js +30 -0
  34. package/dist/meeting/request.js.map +1 -1
  35. package/dist/meeting/request.type.js.map +1 -1
  36. package/dist/meeting/util.js +16 -16
  37. package/dist/meeting/util.js.map +1 -1
  38. package/dist/meeting-info/meeting-info-v2.js +96 -33
  39. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  40. package/dist/meeting-info/utilv2.js +1 -1
  41. package/dist/meeting-info/utilv2.js.map +1 -1
  42. package/dist/meetings/index.js +107 -55
  43. package/dist/meetings/index.js.map +1 -1
  44. package/dist/meetings/meetings.types.js +2 -0
  45. package/dist/meetings/meetings.types.js.map +1 -1
  46. package/dist/meetings/util.js +1 -1
  47. package/dist/meetings/util.js.map +1 -1
  48. package/dist/member/index.js +9 -0
  49. package/dist/member/index.js.map +1 -1
  50. package/dist/member/types.js.map +1 -1
  51. package/dist/member/util.js +39 -28
  52. package/dist/member/util.js.map +1 -1
  53. package/dist/metrics/constants.js +3 -2
  54. package/dist/metrics/constants.js.map +1 -1
  55. package/dist/multistream/remoteMedia.js +30 -15
  56. package/dist/multistream/remoteMedia.js.map +1 -1
  57. package/dist/multistream/sendSlotManager.js +24 -0
  58. package/dist/multistream/sendSlotManager.js.map +1 -1
  59. package/dist/reachability/index.js +31 -3
  60. package/dist/reachability/index.js.map +1 -1
  61. package/dist/roap/index.js +10 -8
  62. package/dist/roap/index.js.map +1 -1
  63. package/dist/types/annotation/index.d.ts +5 -0
  64. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  65. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  66. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  67. package/dist/types/constants.d.ts +38 -1
  68. package/dist/types/index.d.ts +3 -3
  69. package/dist/types/locus-info/index.d.ts +2 -1
  70. package/dist/types/meeting/brbState.d.ts +54 -0
  71. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  72. package/dist/types/meeting/index.d.ts +21 -12
  73. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  74. package/dist/types/meeting/request.d.ts +12 -1
  75. package/dist/types/meeting/request.type.d.ts +6 -0
  76. package/dist/types/meeting/util.d.ts +1 -1
  77. package/dist/types/meeting-info/meeting-info-v2.d.ts +27 -4
  78. package/dist/types/meetings/index.d.ts +19 -1
  79. package/dist/types/meetings/meetings.types.d.ts +8 -0
  80. package/dist/types/member/index.d.ts +1 -0
  81. package/dist/types/member/types.d.ts +7 -0
  82. package/dist/types/metrics/constants.d.ts +2 -1
  83. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  84. package/dist/types/reachability/index.d.ts +9 -1
  85. package/dist/webinar/index.js +354 -3
  86. package/dist/webinar/index.js.map +1 -1
  87. package/package.json +23 -22
  88. package/src/annotation/index.ts +16 -0
  89. package/src/common/errors/join-forbidden-error.ts +26 -0
  90. package/src/common/errors/join-webinar-error.ts +24 -0
  91. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  92. package/src/config.ts +1 -1
  93. package/src/constants.ts +43 -3
  94. package/src/index.ts +5 -3
  95. package/src/locus-info/index.ts +20 -3
  96. package/src/locus-info/selfUtils.ts +24 -6
  97. package/src/meeting/brbState.ts +169 -0
  98. package/src/meeting/in-meeting-actions.ts +4 -0
  99. package/src/meeting/index.ts +256 -82
  100. package/src/meeting/locusMediaRequest.ts +7 -0
  101. package/src/meeting/muteState.ts +1 -6
  102. package/src/meeting/request.ts +26 -1
  103. package/src/meeting/request.type.ts +7 -0
  104. package/src/meeting/util.ts +8 -10
  105. package/src/meeting-info/meeting-info-v2.ts +74 -11
  106. package/src/meeting-info/utilv2.ts +3 -1
  107. package/src/meetings/index.ts +79 -20
  108. package/src/meetings/meetings.types.ts +10 -0
  109. package/src/meetings/util.ts +2 -1
  110. package/src/member/index.ts +9 -0
  111. package/src/member/types.ts +8 -0
  112. package/src/member/util.ts +34 -24
  113. package/src/metrics/constants.ts +2 -1
  114. package/src/multistream/remoteMedia.ts +28 -15
  115. package/src/multistream/sendSlotManager.ts +31 -0
  116. package/src/reachability/index.ts +29 -1
  117. package/src/roap/index.ts +10 -8
  118. package/src/webinar/index.ts +197 -3
  119. package/test/unit/spec/annotation/index.ts +46 -1
  120. package/test/unit/spec/locus-info/index.js +292 -60
  121. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  122. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  123. package/test/unit/spec/meeting/brbState.ts +114 -0
  124. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  125. package/test/unit/spec/meeting/index.js +733 -106
  126. package/test/unit/spec/meeting/muteState.js +0 -24
  127. package/test/unit/spec/meeting/utils.js +22 -19
  128. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  129. package/test/unit/spec/meeting-info/utilv2.js +17 -0
  130. package/test/unit/spec/meetings/index.js +159 -18
  131. package/test/unit/spec/meetings/utils.js +10 -0
  132. package/test/unit/spec/member/util.js +52 -11
  133. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  134. package/test/unit/spec/reachability/index.ts +120 -10
  135. package/test/unit/spec/roap/index.ts +47 -0
  136. package/test/unit/spec/webinar/index.ts +457 -0
  137. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  138. package/src/common/errors/webinar-registration-error.ts +0 -27
@@ -31,7 +31,6 @@ import {
31
31
  } from '@webex/internal-media-core';
32
32
 
33
33
  import {
34
- getDevices,
35
34
  LocalStream,
36
35
  LocalCameraStream,
37
36
  LocalDisplayStream,
@@ -122,6 +121,9 @@ import {
122
121
  MEETING_PERMISSION_TOKEN_REFRESH_REASON,
123
122
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
124
123
  NAMED_MEDIA_GROUP_TYPE_AUDIO,
124
+ WEBINAR_ERROR_WEBCAST,
125
+ WEBINAR_ERROR_REGISTRATIONID,
126
+ JOIN_BEFORE_HOST,
125
127
  } from '../constants';
126
128
  import BEHAVIORAL_METRICS from '../metrics/constants';
127
129
  import ParameterError from '../common/errors/parameter';
@@ -129,7 +131,8 @@ import {
129
131
  MeetingInfoV2PasswordError,
130
132
  MeetingInfoV2CaptchaError,
131
133
  MeetingInfoV2PolicyError,
132
- MeetingInfoV2WebinarRegistrationError,
134
+ MeetingInfoV2JoinWebinarError,
135
+ MeetingInfoV2JoinForbiddenError,
133
136
  } from '../meeting-info/meeting-info-v2';
134
137
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
135
138
  import SendSlotManager from '../multistream/sendSlotManager';
@@ -158,7 +161,11 @@ import ControlsOptionsManager from '../controls-options-manager';
158
161
  import PermissionError from '../common/errors/permission';
159
162
  import {LocusMediaRequest} from './locusMediaRequest';
160
163
  import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
161
- import WebinarRegistrationError from '../common/errors/webinar-registration-error';
164
+ import JoinWebinarError from '../common/errors/join-webinar-error';
165
+ import Member from '../member';
166
+ import {BrbState, createBrbState} from './brbState';
167
+ import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
168
+ import JoinForbiddenError from '../common/errors/join-forbidden-error';
162
169
 
163
170
  // default callback so we don't call an undefined function, but in practice it should never be used
164
171
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -643,6 +650,7 @@ export default class Meeting extends StatelessWebexPlugin {
643
650
  turnServerUsed: boolean;
644
651
  areVoiceaEventsSetup = false;
645
652
  isMoveToInProgress = false;
653
+ brbState: BrbState;
646
654
 
647
655
  voiceaListenerCallbacks: object = {
648
656
  [VOICEAEVENTS.VOICEA_ANNOUNCEMENT]: (payload: Transcription['languageOptions']) => {
@@ -848,7 +856,7 @@ export default class Meeting extends StatelessWebexPlugin {
848
856
  * @memberof Meeting
849
857
  */
850
858
  // @ts-ignore
851
- this.webinar = new Webinar({}, {parent: this.webex});
859
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
852
860
  /**
853
861
  * helper class for managing receive slots (for multistream media connections)
854
862
  */
@@ -1767,15 +1775,34 @@ export default class Meeting extends StatelessWebexPlugin {
1767
1775
  this.meetingInfo = err.meetingInfo;
1768
1776
  }
1769
1777
  throw new PermissionError();
1770
- } else if (err instanceof MeetingInfoV2WebinarRegistrationError) {
1778
+ } else if (err instanceof MeetingInfoV2JoinWebinarError) {
1771
1779
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION;
1780
+ if (WEBINAR_ERROR_WEBCAST.includes(err.wbxAppApiCode)) {
1781
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST;
1782
+ } else if (WEBINAR_ERROR_REGISTRATIONID.includes(err.wbxAppApiCode)) {
1783
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID;
1784
+ }
1785
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1786
+
1787
+ if (err.meetingInfo) {
1788
+ this.meetingInfo = err.meetingInfo;
1789
+ }
1790
+
1791
+ throw new JoinWebinarError();
1792
+ } else if (err instanceof MeetingInfoV2JoinForbiddenError) {
1793
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.JOIN_FORBIDDEN;
1772
1794
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1773
1795
 
1774
1796
  if (err.meetingInfo) {
1775
1797
  this.meetingInfo = err.meetingInfo;
1776
1798
  }
1777
1799
 
1778
- throw new WebinarRegistrationError();
1800
+ // Handle the case where user hasn't reached Join Before Host (JBH) time (error code 403003)
1801
+ if (JOIN_BEFORE_HOST === err.wbxAppApiCode) {
1802
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH;
1803
+ }
1804
+
1805
+ throw new JoinForbiddenError(this.meetingInfoFailureReason, err);
1779
1806
  } else if (err instanceof MeetingInfoV2PasswordError) {
1780
1807
  LoggerProxy.logger.info(
1781
1808
  // @ts-ignore
@@ -2734,6 +2761,7 @@ export default class Meeting extends StatelessWebexPlugin {
2734
2761
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2735
2762
 
2736
2763
  if (
2764
+ !payload.forceUpdate &&
2737
2765
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2738
2766
  contentShare.disposition === previousContentShare?.disposition &&
2739
2767
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2780,7 +2808,11 @@ export default class Meeting extends StatelessWebexPlugin {
2780
2808
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2781
2809
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2782
2810
  // WHITEBOARD - sharing whiteboard
2783
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2811
+ // Webinar attendee should receive whiteboard as remote share
2812
+ newShareStatus =
2813
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2814
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2815
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2784
2816
  }
2785
2817
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2786
2818
  else if (
@@ -2795,6 +2827,7 @@ export default class Meeting extends StatelessWebexPlugin {
2795
2827
  LoggerProxy.logger.info(
2796
2828
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2797
2829
  );
2830
+
2798
2831
  if (newShareStatus !== this.shareStatus) {
2799
2832
  const oldShareStatus = this.shareStatus;
2800
2833
 
@@ -3052,7 +3085,20 @@ export default class Meeting extends StatelessWebexPlugin {
3052
3085
  */
3053
3086
  private setUpLocusResourcesListener() {
3054
3087
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3055
- this.webinar.updateWebcastUrl(payload);
3088
+ if (payload) {
3089
+ this.webinar.updateWebcastUrl(payload);
3090
+ Trigger.trigger(
3091
+ this,
3092
+ {
3093
+ file: 'meeting/index',
3094
+ function: 'setUpLocusInfoMeetingInfoListener',
3095
+ },
3096
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3097
+ {
3098
+ payload,
3099
+ }
3100
+ );
3101
+ }
3056
3102
  });
3057
3103
  }
3058
3104
 
@@ -3362,6 +3408,21 @@ export default class Meeting extends StatelessWebexPlugin {
3362
3408
  }
3363
3409
  });
3364
3410
 
3411
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
3412
+ this.brbState?.handleServerBrbUpdate(payload?.brb?.enabled);
3413
+ Trigger.trigger(
3414
+ this,
3415
+ {
3416
+ file: 'meeting/index',
3417
+ function: 'setUpLocusInfoSelfListener',
3418
+ },
3419
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
3420
+ {
3421
+ payload,
3422
+ }
3423
+ );
3424
+ });
3425
+
3365
3426
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3366
3427
  const isModeratorOrCohost =
3367
3428
  payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3371,6 +3432,7 @@ export default class Meeting extends StatelessWebexPlugin {
3371
3432
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3372
3433
  );
3373
3434
  this.webinar.updateRoleChanged(payload);
3435
+
3374
3436
  Trigger.trigger(
3375
3437
  this,
3376
3438
  {
@@ -3565,6 +3627,35 @@ export default class Meeting extends StatelessWebexPlugin {
3565
3627
  return this.members.admitMembers(memberIds, locusUrls);
3566
3628
  }
3567
3629
 
3630
+ /**
3631
+ * Manages be right back status updates for the current participant.
3632
+ *
3633
+ * @param {boolean} enabled - Indicates whether the user enabled brb or not.
3634
+ * @returns {Promise<void>} resolves when the brb status is updated or does nothing if not in a multistream meeting.
3635
+ * @throws {Error} - Throws an error if the request fails.
3636
+ */
3637
+ public async beRightBack(enabled: boolean): Promise<void> {
3638
+ if (!this.isMultistream) {
3639
+ const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
3640
+ const error = new Error(errorMessage);
3641
+
3642
+ LoggerProxy.logger.error(error);
3643
+
3644
+ return Promise.reject(error);
3645
+ }
3646
+
3647
+ if (!this.mediaProperties.webrtcMediaConnection) {
3648
+ const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
3649
+ const error = new Error(errorMessage);
3650
+
3651
+ LoggerProxy.logger.error(error);
3652
+
3653
+ return Promise.reject(error);
3654
+ }
3655
+
3656
+ return this.brbState.enable(enabled, this.sendSlotManager);
3657
+ }
3658
+
3568
3659
  /**
3569
3660
  * Remove the member from the meeting, boot them
3570
3661
  * @param {String} memberId
@@ -3804,6 +3895,7 @@ export default class Meeting extends StatelessWebexPlugin {
3804
3895
  this.userDisplayHints
3805
3896
  ),
3806
3897
  canManageBreakout: MeetingUtil.canManageBreakout(this.userDisplayHints),
3898
+ canStartBreakout: MeetingUtil.canStartBreakout(this.userDisplayHints),
3807
3899
  canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
3808
3900
  this.userDisplayHints,
3809
3901
  this.selfUserPolicies
@@ -4099,10 +4191,11 @@ export default class Meeting extends StatelessWebexPlugin {
4099
4191
  */
4100
4192
  private setLogUploadTimer() {
4101
4193
  // start with short timeouts and increase them later on so in case users have very long multi-hour meetings we don't get too fragmented logs
4102
- const LOG_UPLOAD_INTERVALS = [0.1, 1, 15, 15, 30, 30, 30, 60];
4194
+ const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
4103
4195
 
4104
4196
  const delay =
4105
4197
  1000 *
4198
+ 60 *
4106
4199
  // @ts-ignore - config coming from registerPlugin
4107
4200
  this.config.logUploadIntervalMultiplicationFactor *
4108
4201
  LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
@@ -4541,11 +4634,12 @@ export default class Meeting extends StatelessWebexPlugin {
4541
4634
  * Close the peer connections and remove them from the class.
4542
4635
  * Cleanup any media connection related things.
4543
4636
  *
4637
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4544
4638
  * @returns {Promise}
4545
4639
  * @public
4546
4640
  * @memberof Meeting
4547
4641
  */
4548
- public closePeerConnections() {
4642
+ public closePeerConnections(resetMuteStates = true) {
4549
4643
  if (this.mediaProperties.webrtcMediaConnection) {
4550
4644
  if (this.remoteMediaManager) {
4551
4645
  this.remoteMediaManager.stop();
@@ -4558,12 +4652,15 @@ export default class Meeting extends StatelessWebexPlugin {
4558
4652
 
4559
4653
  this.receiveSlotManager.reset();
4560
4654
  this.mediaProperties.webrtcMediaConnection.close();
4655
+ this.mediaProperties.unsetPeerConnection();
4561
4656
  this.sendSlotManager.reset();
4562
4657
  this.setNetworkStatus(undefined);
4563
4658
  }
4564
4659
 
4565
- this.audio = null;
4566
- this.video = null;
4660
+ if (resetMuteStates) {
4661
+ this.audio = null;
4662
+ this.video = null;
4663
+ }
4567
4664
 
4568
4665
  return Promise.resolve();
4569
4666
  }
@@ -4823,7 +4920,7 @@ export default class Meeting extends StatelessWebexPlugin {
4823
4920
  * @param {Object} options - options to join with media
4824
4921
  * @param {JoinOptions} [options.joinOptions] - see #join()
4825
4922
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4826
- * @returns {Promise} -- {join: see join(), media: see addMedia()}
4923
+ * @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
4827
4924
  * @public
4828
4925
  * @memberof Meeting
4829
4926
  * @example
@@ -4913,6 +5010,7 @@ export default class Meeting extends StatelessWebexPlugin {
4913
5010
  return {
4914
5011
  join: joinResponse,
4915
5012
  media: mediaResponse,
5013
+ multistreamEnabled: this.isMultistream,
4916
5014
  };
4917
5015
  } catch (error) {
4918
5016
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -4921,7 +5019,17 @@ export default class Meeting extends StatelessWebexPlugin {
4921
5019
 
4922
5020
  this.roap.abortTurnDiscovery();
4923
5021
 
4924
- if (joined && isRetry) {
5022
+ // if this was the first attempt, let's do a retry
5023
+ let shouldRetry = !isRetry;
5024
+
5025
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
5026
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
5027
+ // so there is no point doing a retry
5028
+ shouldRetry = false;
5029
+ }
5030
+
5031
+ // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
5032
+ if (joined && (isRetry || !shouldRetry)) {
4925
5033
  try {
4926
5034
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4927
5035
  } catch (e) {
@@ -4945,15 +5053,6 @@ export default class Meeting extends StatelessWebexPlugin {
4945
5053
  }
4946
5054
  );
4947
5055
 
4948
- // if this was the first attempt, let's do a retry
4949
- let shouldRetry = !isRetry;
4950
-
4951
- if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
4952
- // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
4953
- // so there is no point doing a retry
4954
- shouldRetry = false;
4955
- }
4956
-
4957
5056
  if (shouldRetry) {
4958
5057
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4959
5058
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5209,7 +5308,16 @@ export default class Meeting extends StatelessWebexPlugin {
5209
5308
  (this.config.receiveReactions || options.receiveReactions) &&
5210
5309
  this.isReactionsSupported()
5211
5310
  ) {
5212
- const {name} = this.members.membersCollection.get(e.data.sender.participantId);
5311
+ const member = this.members.membersCollection.get(e.data.sender.participantId);
5312
+ if (!member) {
5313
+ // @ts-ignore -- fix type
5314
+ LoggerProxy.logger.warn(
5315
+ `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
5316
+ );
5317
+ break;
5318
+ }
5319
+
5320
+ const {name} = member;
5213
5321
  const processedReaction: ProcessedReaction = {
5214
5322
  reaction: e.data.reaction,
5215
5323
  sender: {
@@ -5263,6 +5371,9 @@ export default class Meeting extends StatelessWebexPlugin {
5263
5371
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5264
5372
  );
5265
5373
 
5374
+ // @ts-ignore
5375
+ this.webex.internal.voicea.deregisterEvents();
5376
+
5266
5377
  this.areVoiceaEventsSetup = false;
5267
5378
  this.triggerStopReceivingTranscriptionEvent();
5268
5379
  }
@@ -5373,16 +5484,19 @@ export default class Meeting extends StatelessWebexPlugin {
5373
5484
  this.meetingFiniteStateMachine.reset();
5374
5485
  }
5375
5486
 
5376
- // @ts-ignore
5377
- this.webex.internal.newMetrics.submitClientEvent({
5378
- name: 'client.call.initiated',
5379
- payload: {
5380
- trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5381
- isRoapCallEnabled: true,
5382
- pstnAudioType: options?.pstnAudioType,
5383
- },
5384
- options: {meetingId: this.id},
5385
- });
5487
+ // send client.call.initiated unless told not to
5488
+ if (options.sendCallInitiated !== false) {
5489
+ // @ts-ignore
5490
+ this.webex.internal.newMetrics.submitClientEvent({
5491
+ name: 'client.call.initiated',
5492
+ payload: {
5493
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5494
+ isRoapCallEnabled: true,
5495
+ pstnAudioType: options?.pstnAudioType,
5496
+ },
5497
+ options: {meetingId: this.id},
5498
+ });
5499
+ }
5386
5500
 
5387
5501
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
5388
5502
 
@@ -5570,17 +5684,23 @@ export default class Meeting extends StatelessWebexPlugin {
5570
5684
  */
5571
5685
  async updateLLMConnection() {
5572
5686
  // @ts-ignore - Fix type
5573
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5687
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5574
5688
 
5575
5689
  const isJoined = this.isJoined();
5576
5690
 
5691
+ // webinar panelist should use new data channel in practice session
5692
+ const dataChannelUrl =
5693
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5694
+ ? practiceSessionDatachannelUrl
5695
+ : datachannelUrl;
5696
+
5577
5697
  // @ts-ignore - Fix type
5578
5698
  if (this.webex.internal.llm.isConnected()) {
5579
5699
  if (
5580
5700
  // @ts-ignore - Fix type
5581
5701
  url === this.webex.internal.llm.getLocusUrl() &&
5582
5702
  // @ts-ignore - Fix type
5583
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5703
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5584
5704
  isJoined
5585
5705
  ) {
5586
5706
  return undefined;
@@ -5597,7 +5717,7 @@ export default class Meeting extends StatelessWebexPlugin {
5597
5717
 
5598
5718
  // @ts-ignore - Fix type
5599
5719
  return this.webex.internal.llm
5600
- .registerAndConnect(url, datachannelUrl)
5720
+ .registerAndConnect(url, dataChannelUrl)
5601
5721
  .then((registerAndConnectResult) => {
5602
5722
  // @ts-ignore - Fix type
5603
5723
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5967,8 +6087,16 @@ export default class Meeting extends StatelessWebexPlugin {
5967
6087
  * @returns {undefined}
5968
6088
  */
5969
6089
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5970
- const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5971
-
6090
+ const mediaServer =
6091
+ roapMessage.messageType === 'ANSWER'
6092
+ ? MeetingsUtil.getMediaServer(roapMessage.sdp)
6093
+ : undefined;
6094
+
6095
+ if (this.isMultistream && mediaServer && mediaServer !== 'homer') {
6096
+ throw new MultistreamNotSupportedError(
6097
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6098
+ );
6099
+ }
5972
6100
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5973
6101
 
5974
6102
  if (mediaServer) {
@@ -6091,16 +6219,20 @@ export default class Meeting extends StatelessWebexPlugin {
6091
6219
  logText: `${LOG_HEADER} Roap Offer`,
6092
6220
  }
6093
6221
  ).catch((error) => {
6222
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6223
+
6094
6224
  // @ts-ignore
6095
6225
  this.webex.internal.newMetrics.submitClientEvent({
6096
6226
  name: 'client.media-engine.remote-sdp-received',
6097
6227
  payload: {
6098
- canProceed: false,
6228
+ canProceed: multistreamNotSupported,
6099
6229
  errors: [
6100
6230
  // @ts-ignore
6101
6231
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6102
6232
  {
6103
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6233
+ clientErrorCode: multistreamNotSupported
6234
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6235
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6104
6236
  }
6105
6237
  ),
6106
6238
  ],
@@ -6108,7 +6240,7 @@ export default class Meeting extends StatelessWebexPlugin {
6108
6240
  options: {meetingId: this.id, rawError: error},
6109
6241
  });
6110
6242
 
6111
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6243
+ this.deferSDPAnswer.reject(error);
6112
6244
  clearTimeout(this.sdpResponseTimer);
6113
6245
  this.sdpResponseTimer = undefined;
6114
6246
  });
@@ -6436,6 +6568,14 @@ export default class Meeting extends StatelessWebexPlugin {
6436
6568
  this.webex.meetings.geoHintInfo?.clientAddress ||
6437
6569
  options.data.intervalMetadata.peerReflexiveIP ||
6438
6570
  MQA_STATS.DEFAULT_IP;
6571
+
6572
+ const {members} = this.getMembers().membersCollection;
6573
+
6574
+ // Count members that are in the meeting
6575
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6576
+ (member: Member) => member.isInMeeting
6577
+ ).length;
6578
+
6439
6579
  // @ts-ignore
6440
6580
  this.webex.internal.newMetrics.submitMQE({
6441
6581
  name: 'client.mediaquality.event',
@@ -6567,6 +6707,9 @@ export default class Meeting extends StatelessWebexPlugin {
6567
6707
  new RtcMetrics(this.webex, {meetingId: this.id}, this.correlationId)
6568
6708
  : undefined;
6569
6709
 
6710
+ // ongoing reachability checks slow down new media connections especially on Firefox, so we stop them
6711
+ this.getWebexObject().meetings.reachability.stopReachability();
6712
+
6570
6713
  const mc = Media.createMediaConnection(
6571
6714
  this.isMultistream,
6572
6715
  this.getMediaConnectionDebugId(),
@@ -6767,32 +6910,6 @@ export default class Meeting extends StatelessWebexPlugin {
6767
6910
  }
6768
6911
  }
6769
6912
 
6770
- /**
6771
- * Handles device logging
6772
- *
6773
- * @private
6774
- * @static
6775
- * @param {boolean} isAudioEnabled
6776
- * @param {boolean} isVideoEnabled
6777
- * @returns {Promise<void>}
6778
- */
6779
-
6780
- private static async handleDeviceLogging(isAudioEnabled, isVideoEnabled): Promise<void> {
6781
- try {
6782
- let devices = [];
6783
- if (isVideoEnabled && isAudioEnabled) {
6784
- devices = await getDevices();
6785
- } else if (isVideoEnabled) {
6786
- devices = await getDevices(Media.DeviceKind.VIDEO_INPUT);
6787
- } else if (isAudioEnabled) {
6788
- devices = await getDevices(Media.DeviceKind.AUDIO_INPUT);
6789
- }
6790
- MeetingUtil.handleDeviceLogging(devices);
6791
- } catch {
6792
- // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
6793
- }
6794
- }
6795
-
6796
6913
  /**
6797
6914
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6798
6915
  * once the remote sdp answer has been received.
@@ -7016,7 +7133,9 @@ export default class Meeting extends StatelessWebexPlugin {
7016
7133
 
7017
7134
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
7018
7135
 
7019
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7136
+ LoggerProxy.logger.info(
7137
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7138
+ );
7020
7139
 
7021
7140
  if (this.isMultistream) {
7022
7141
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7094,6 +7213,33 @@ export default class Meeting extends StatelessWebexPlugin {
7094
7213
  }
7095
7214
  }
7096
7215
 
7216
+ /**
7217
+ * Cleans up stats analyzer, peer connection and other things before
7218
+ * we can create a new transcoded media connection
7219
+ *
7220
+ * @private
7221
+ * @returns {Promise<void>}
7222
+ */
7223
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7224
+ if (this.statsAnalyzer) {
7225
+ await this.statsAnalyzer.stopAnalyzer();
7226
+ }
7227
+ this.statsAnalyzer = null;
7228
+
7229
+ this.isMultistream = false;
7230
+
7231
+ if (this.mediaProperties.webrtcMediaConnection) {
7232
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7233
+ this.closePeerConnections(false);
7234
+
7235
+ this.mediaProperties.unsetPeerConnection();
7236
+ }
7237
+
7238
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7239
+
7240
+ this.createStatsAnalyzer();
7241
+ }
7242
+
7097
7243
  /**
7098
7244
  * Sends stats report, closes peer connection and cleans up any media connection
7099
7245
  * related things before trying to establish media connection again with turn server
@@ -7280,6 +7426,7 @@ export default class Meeting extends StatelessWebexPlugin {
7280
7426
 
7281
7427
  this.audio = createMuteState(AUDIO, this, audioEnabled);
7282
7428
  this.video = createMuteState(VIDEO, this, videoEnabled);
7429
+ this.brbState = createBrbState(this, false);
7283
7430
 
7284
7431
  try {
7285
7432
  await this.setUpLocalStreamReferences(localStreams);
@@ -7288,19 +7435,33 @@ export default class Meeting extends StatelessWebexPlugin {
7288
7435
 
7289
7436
  this.createStatsAnalyzer();
7290
7437
 
7291
- await this.establishMediaConnection(
7292
- remoteMediaManagerConfig,
7293
- bundlePolicy,
7294
- forceTurnDiscovery,
7295
- turnServerInfo
7296
- );
7438
+ try {
7439
+ await this.establishMediaConnection(
7440
+ remoteMediaManagerConfig,
7441
+ bundlePolicy,
7442
+ forceTurnDiscovery,
7443
+ turnServerInfo
7444
+ );
7445
+ } catch (error) {
7446
+ if (error instanceof MultistreamNotSupportedError) {
7447
+ LoggerProxy.logger.warn(
7448
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7449
+ );
7297
7450
 
7298
- if (audioEnabled || videoEnabled) {
7299
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7300
- } else {
7301
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7302
- }
7451
+ await this.downgradeFromMultistreamToTranscoded();
7303
7452
 
7453
+ // Establish new media connection with forced TURN discovery
7454
+ // We need to do TURN discovery again, because backend will be creating a new confluence, so it might land on a different node or cluster
7455
+ await this.establishMediaConnection(
7456
+ remoteMediaManagerConfig,
7457
+ bundlePolicy,
7458
+ true,
7459
+ undefined
7460
+ );
7461
+ } else {
7462
+ throw error;
7463
+ }
7464
+ }
7304
7465
  if (this.mediaProperties.hasLocalShareStream()) {
7305
7466
  await this.enqueueScreenShareFloorRequest();
7306
7467
  }
@@ -8270,7 +8431,7 @@ export default class Meeting extends StatelessWebexPlugin {
8270
8431
  if (layoutType) {
8271
8432
  if (!LAYOUT_TYPES.includes(layoutType)) {
8272
8433
  return this.rejectWithErrorLog(
8273
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8434
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8274
8435
  );
8275
8436
  }
8276
8437
 
@@ -8426,6 +8587,12 @@ export default class Meeting extends StatelessWebexPlugin {
8426
8587
  correlationId: this.correlationId,
8427
8588
  muted,
8428
8589
  encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
8590
+ // TypeScript 4 does not recognize the `displaySurface` property. Instead of upgrading the
8591
+ // SDK to TypeScript 5, which may affect other packages, use bracket notation for now, since
8592
+ // all we're doing here is adding metrics.
8593
+ // eslint-disable-next-line dot-notation
8594
+ displaySurface: this.mediaProperties?.shareVideoStream?.getSettings()['displaySurface'],
8595
+ isMultistream: this.isMultistream,
8429
8596
  });
8430
8597
  };
8431
8598
 
@@ -8628,6 +8795,11 @@ export default class Meeting extends StatelessWebexPlugin {
8628
8795
  this.stopTranscription();
8629
8796
  this.transcription = undefined;
8630
8797
  }
8798
+
8799
+ this.annotation.deregisterEvents();
8800
+
8801
+ // @ts-ignore - fix types
8802
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8631
8803
  };
8632
8804
 
8633
8805
  /**
@@ -8665,10 +8837,12 @@ export default class Meeting extends StatelessWebexPlugin {
8665
8837
 
8666
8838
  return;
8667
8839
  }
8668
- const {keepAliveUrl} = this.joinedWith;
8840
+
8669
8841
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8670
8842
 
8671
8843
  this.keepAliveTimerId = setInterval(() => {
8844
+ const {keepAliveUrl} = this.joinedWith;
8845
+
8672
8846
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8673
8847
  LoggerProxy.logger.warn(
8674
8848
  `Meeting:index#startKeepAlive --> Stopping sending keepAlives to ${keepAliveUrl} after error ${error}`
@@ -342,4 +342,11 @@ export class LocusMediaRequest extends WebexPlugin {
342
342
  public isConfluenceCreated() {
343
343
  return this.confluenceState === 'created';
344
344
  }
345
+
346
+ /**
347
+ * This method needs to be called when we downgrade from multistream to transcoded connection.
348
+ */
349
+ public downgradeFromMultistreamToTranscoded() {
350
+ this.config.preferTranscoding = true;
351
+ }
345
352
  }
@@ -379,12 +379,7 @@ export class MuteState {
379
379
  }
380
380
  if (muted !== undefined) {
381
381
  this.state.server.remoteMute = muted;
382
-
383
- // We never want to unmute the local stream from a server remote mute update.
384
- // Moderated unmute is handled by a different function.
385
- if (muted) {
386
- this.muteLocalStream(meeting, muted, 'remotelyMuted');
387
- }
382
+ this.muteLocalStream(meeting, muted, 'remotelyMuted');
388
383
  }
389
384
  }
390
385