@webex/plugin-meetings 3.7.0-next.9 → 3.7.0-wxcc.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 (124) 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 +30 -17
  22. package/dist/locus-info/selfUtils.js.map +1 -1
  23. package/dist/meeting/in-meeting-actions.js +2 -0
  24. package/dist/meeting/in-meeting-actions.js.map +1 -1
  25. package/dist/meeting/index.js +960 -832
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/locusMediaRequest.js +9 -0
  28. package/dist/meeting/locusMediaRequest.js.map +1 -1
  29. package/dist/meeting/request.js +30 -0
  30. package/dist/meeting/request.js.map +1 -1
  31. package/dist/meeting/request.type.js.map +1 -1
  32. package/dist/meeting/util.js +16 -16
  33. package/dist/meeting/util.js.map +1 -1
  34. package/dist/meeting-info/meeting-info-v2.js +96 -33
  35. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  36. package/dist/meeting-info/utilv2.js +1 -1
  37. package/dist/meeting-info/utilv2.js.map +1 -1
  38. package/dist/meetings/index.js +103 -54
  39. package/dist/meetings/index.js.map +1 -1
  40. package/dist/meetings/meetings.types.js +2 -0
  41. package/dist/meetings/meetings.types.js.map +1 -1
  42. package/dist/meetings/util.js +1 -1
  43. package/dist/meetings/util.js.map +1 -1
  44. package/dist/member/index.js +9 -0
  45. package/dist/member/index.js.map +1 -1
  46. package/dist/member/types.js.map +1 -1
  47. package/dist/member/util.js +39 -28
  48. package/dist/member/util.js.map +1 -1
  49. package/dist/metrics/constants.js +3 -2
  50. package/dist/metrics/constants.js.map +1 -1
  51. package/dist/multistream/remoteMedia.js +30 -15
  52. package/dist/multistream/remoteMedia.js.map +1 -1
  53. package/dist/multistream/sendSlotManager.js +24 -0
  54. package/dist/multistream/sendSlotManager.js.map +1 -1
  55. package/dist/roap/index.js +10 -8
  56. package/dist/roap/index.js.map +1 -1
  57. package/dist/types/annotation/index.d.ts +5 -0
  58. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  59. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  60. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  61. package/dist/types/constants.d.ts +38 -1
  62. package/dist/types/index.d.ts +3 -3
  63. package/dist/types/locus-info/index.d.ts +2 -1
  64. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  65. package/dist/types/meeting/index.d.ts +19 -12
  66. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  67. package/dist/types/meeting/request.d.ts +12 -1
  68. package/dist/types/meeting/request.type.d.ts +6 -0
  69. package/dist/types/meeting/util.d.ts +1 -1
  70. package/dist/types/meeting-info/meeting-info-v2.d.ts +27 -4
  71. package/dist/types/meetings/index.d.ts +16 -1
  72. package/dist/types/meetings/meetings.types.d.ts +8 -0
  73. package/dist/types/member/index.d.ts +1 -0
  74. package/dist/types/member/types.d.ts +7 -0
  75. package/dist/types/metrics/constants.d.ts +2 -1
  76. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  77. package/dist/webinar/index.js +354 -3
  78. package/dist/webinar/index.js.map +1 -1
  79. package/package.json +23 -22
  80. package/src/annotation/index.ts +16 -0
  81. package/src/common/errors/join-forbidden-error.ts +26 -0
  82. package/src/common/errors/join-webinar-error.ts +24 -0
  83. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  84. package/src/config.ts +1 -1
  85. package/src/constants.ts +43 -3
  86. package/src/index.ts +5 -3
  87. package/src/locus-info/index.ts +20 -3
  88. package/src/locus-info/selfUtils.ts +19 -6
  89. package/src/meeting/in-meeting-actions.ts +4 -0
  90. package/src/meeting/index.ts +259 -80
  91. package/src/meeting/locusMediaRequest.ts +7 -0
  92. package/src/meeting/request.ts +26 -1
  93. package/src/meeting/request.type.ts +7 -0
  94. package/src/meeting/util.ts +8 -10
  95. package/src/meeting-info/meeting-info-v2.ts +74 -11
  96. package/src/meeting-info/utilv2.ts +3 -1
  97. package/src/meetings/index.ts +73 -20
  98. package/src/meetings/meetings.types.ts +10 -0
  99. package/src/meetings/util.ts +2 -1
  100. package/src/member/index.ts +9 -0
  101. package/src/member/types.ts +8 -0
  102. package/src/member/util.ts +34 -24
  103. package/src/metrics/constants.ts +2 -1
  104. package/src/multistream/remoteMedia.ts +28 -15
  105. package/src/multistream/sendSlotManager.ts +31 -0
  106. package/src/roap/index.ts +10 -8
  107. package/src/webinar/index.ts +197 -3
  108. package/test/unit/spec/annotation/index.ts +46 -1
  109. package/test/unit/spec/locus-info/index.js +292 -60
  110. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  111. package/test/unit/spec/locus-info/selfUtils.js +91 -1
  112. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  113. package/test/unit/spec/meeting/index.js +722 -105
  114. package/test/unit/spec/meeting/utils.js +22 -19
  115. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  116. package/test/unit/spec/meeting-info/utilv2.js +17 -0
  117. package/test/unit/spec/meetings/index.js +150 -13
  118. package/test/unit/spec/meetings/utils.js +10 -0
  119. package/test/unit/spec/member/util.js +52 -11
  120. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  121. package/test/unit/spec/roap/index.ts +47 -0
  122. package/test/unit/spec/webinar/index.ts +457 -0
  123. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  124. 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,10 @@ 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 MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
167
+ import JoinForbiddenError from '../common/errors/join-forbidden-error';
162
168
 
163
169
  // default callback so we don't call an undefined function, but in practice it should never be used
164
170
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -848,7 +854,7 @@ export default class Meeting extends StatelessWebexPlugin {
848
854
  * @memberof Meeting
849
855
  */
850
856
  // @ts-ignore
851
- this.webinar = new Webinar({}, {parent: this.webex});
857
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
852
858
  /**
853
859
  * helper class for managing receive slots (for multistream media connections)
854
860
  */
@@ -1767,15 +1773,34 @@ export default class Meeting extends StatelessWebexPlugin {
1767
1773
  this.meetingInfo = err.meetingInfo;
1768
1774
  }
1769
1775
  throw new PermissionError();
1770
- } else if (err instanceof MeetingInfoV2WebinarRegistrationError) {
1776
+ } else if (err instanceof MeetingInfoV2JoinWebinarError) {
1771
1777
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION;
1778
+ if (WEBINAR_ERROR_WEBCAST.includes(err.wbxAppApiCode)) {
1779
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST;
1780
+ } else if (WEBINAR_ERROR_REGISTRATIONID.includes(err.wbxAppApiCode)) {
1781
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID;
1782
+ }
1772
1783
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1773
1784
 
1774
1785
  if (err.meetingInfo) {
1775
1786
  this.meetingInfo = err.meetingInfo;
1776
1787
  }
1777
1788
 
1778
- throw new WebinarRegistrationError();
1789
+ throw new JoinWebinarError();
1790
+ } else if (err instanceof MeetingInfoV2JoinForbiddenError) {
1791
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.JOIN_FORBIDDEN;
1792
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1793
+
1794
+ if (err.meetingInfo) {
1795
+ this.meetingInfo = err.meetingInfo;
1796
+ }
1797
+
1798
+ // Handle the case where user hasn't reached Join Before Host (JBH) time (error code 403003)
1799
+ if (JOIN_BEFORE_HOST === err.wbxAppApiCode) {
1800
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH;
1801
+ }
1802
+
1803
+ throw new JoinForbiddenError(this.meetingInfoFailureReason, err);
1779
1804
  } else if (err instanceof MeetingInfoV2PasswordError) {
1780
1805
  LoggerProxy.logger.info(
1781
1806
  // @ts-ignore
@@ -2734,6 +2759,7 @@ export default class Meeting extends StatelessWebexPlugin {
2734
2759
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2735
2760
 
2736
2761
  if (
2762
+ !payload.forceUpdate &&
2737
2763
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2738
2764
  contentShare.disposition === previousContentShare?.disposition &&
2739
2765
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2780,7 +2806,11 @@ export default class Meeting extends StatelessWebexPlugin {
2780
2806
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2781
2807
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2782
2808
  // WHITEBOARD - sharing whiteboard
2783
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2809
+ // Webinar attendee should receive whiteboard as remote share
2810
+ newShareStatus =
2811
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2812
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2813
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2784
2814
  }
2785
2815
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2786
2816
  else if (
@@ -2795,6 +2825,7 @@ export default class Meeting extends StatelessWebexPlugin {
2795
2825
  LoggerProxy.logger.info(
2796
2826
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2797
2827
  );
2828
+
2798
2829
  if (newShareStatus !== this.shareStatus) {
2799
2830
  const oldShareStatus = this.shareStatus;
2800
2831
 
@@ -3052,7 +3083,20 @@ export default class Meeting extends StatelessWebexPlugin {
3052
3083
  */
3053
3084
  private setUpLocusResourcesListener() {
3054
3085
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3055
- this.webinar.updateWebcastUrl(payload);
3086
+ if (payload) {
3087
+ this.webinar.updateWebcastUrl(payload);
3088
+ Trigger.trigger(
3089
+ this,
3090
+ {
3091
+ file: 'meeting/index',
3092
+ function: 'setUpLocusInfoMeetingInfoListener',
3093
+ },
3094
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3095
+ {
3096
+ payload,
3097
+ }
3098
+ );
3099
+ }
3056
3100
  });
3057
3101
  }
3058
3102
 
@@ -3362,6 +3406,20 @@ export default class Meeting extends StatelessWebexPlugin {
3362
3406
  }
3363
3407
  });
3364
3408
 
3409
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
3410
+ Trigger.trigger(
3411
+ this,
3412
+ {
3413
+ file: 'meeting/index',
3414
+ function: 'setUpLocusInfoSelfListener',
3415
+ },
3416
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
3417
+ {
3418
+ payload,
3419
+ }
3420
+ );
3421
+ });
3422
+
3365
3423
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3366
3424
  const isModeratorOrCohost =
3367
3425
  payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3371,6 +3429,7 @@ export default class Meeting extends StatelessWebexPlugin {
3371
3429
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3372
3430
  );
3373
3431
  this.webinar.updateRoleChanged(payload);
3432
+
3374
3433
  Trigger.trigger(
3375
3434
  this,
3376
3435
  {
@@ -3565,6 +3624,50 @@ export default class Meeting extends StatelessWebexPlugin {
3565
3624
  return this.members.admitMembers(memberIds, locusUrls);
3566
3625
  }
3567
3626
 
3627
+ /**
3628
+ * Manages be right back status updates for the current participant.
3629
+ *
3630
+ * @param {boolean} enabled - Indicates whether the user enabled brb or not.
3631
+ * @returns {Promise<void>} resolves when the brb status is updated or does nothing if not in a multistream meeting.
3632
+ * @throws {Error} - Throws an error if the request fails.
3633
+ */
3634
+ public async beRightBack(enabled: boolean): Promise<void> {
3635
+ if (!this.isMultistream) {
3636
+ const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
3637
+ const error = new Error(errorMessage);
3638
+
3639
+ LoggerProxy.logger.error(error);
3640
+
3641
+ return Promise.reject(error);
3642
+ }
3643
+
3644
+ if (!this.mediaProperties.webrtcMediaConnection) {
3645
+ const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
3646
+ const error = new Error(errorMessage);
3647
+
3648
+ LoggerProxy.logger.error(error);
3649
+
3650
+ return Promise.reject(error);
3651
+ }
3652
+
3653
+ // this logic should be applied only to multistream meetings
3654
+ return this.meetingRequest
3655
+ .setBrb({
3656
+ enabled,
3657
+ locusUrl: this.locusUrl,
3658
+ deviceUrl: this.deviceUrl,
3659
+ selfId: this.selfId,
3660
+ })
3661
+ .then(() => {
3662
+ this.sendSlotManager.setSourceStateOverride(MediaType.VideoMain, enabled ? 'away' : null);
3663
+ })
3664
+ .catch((error) => {
3665
+ LoggerProxy.logger.error('Meeting:index#beRightBack --> Error ', error);
3666
+
3667
+ return Promise.reject(error);
3668
+ });
3669
+ }
3670
+
3568
3671
  /**
3569
3672
  * Remove the member from the meeting, boot them
3570
3673
  * @param {String} memberId
@@ -3804,6 +3907,7 @@ export default class Meeting extends StatelessWebexPlugin {
3804
3907
  this.userDisplayHints
3805
3908
  ),
3806
3909
  canManageBreakout: MeetingUtil.canManageBreakout(this.userDisplayHints),
3910
+ canStartBreakout: MeetingUtil.canStartBreakout(this.userDisplayHints),
3807
3911
  canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
3808
3912
  this.userDisplayHints,
3809
3913
  this.selfUserPolicies
@@ -4099,10 +4203,11 @@ export default class Meeting extends StatelessWebexPlugin {
4099
4203
  */
4100
4204
  private setLogUploadTimer() {
4101
4205
  // 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];
4206
+ const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
4103
4207
 
4104
4208
  const delay =
4105
4209
  1000 *
4210
+ 60 *
4106
4211
  // @ts-ignore - config coming from registerPlugin
4107
4212
  this.config.logUploadIntervalMultiplicationFactor *
4108
4213
  LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
@@ -4541,11 +4646,12 @@ export default class Meeting extends StatelessWebexPlugin {
4541
4646
  * Close the peer connections and remove them from the class.
4542
4647
  * Cleanup any media connection related things.
4543
4648
  *
4649
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4544
4650
  * @returns {Promise}
4545
4651
  * @public
4546
4652
  * @memberof Meeting
4547
4653
  */
4548
- public closePeerConnections() {
4654
+ public closePeerConnections(resetMuteStates = true) {
4549
4655
  if (this.mediaProperties.webrtcMediaConnection) {
4550
4656
  if (this.remoteMediaManager) {
4551
4657
  this.remoteMediaManager.stop();
@@ -4558,12 +4664,15 @@ export default class Meeting extends StatelessWebexPlugin {
4558
4664
 
4559
4665
  this.receiveSlotManager.reset();
4560
4666
  this.mediaProperties.webrtcMediaConnection.close();
4667
+ this.mediaProperties.unsetPeerConnection();
4561
4668
  this.sendSlotManager.reset();
4562
4669
  this.setNetworkStatus(undefined);
4563
4670
  }
4564
4671
 
4565
- this.audio = null;
4566
- this.video = null;
4672
+ if (resetMuteStates) {
4673
+ this.audio = null;
4674
+ this.video = null;
4675
+ }
4567
4676
 
4568
4677
  return Promise.resolve();
4569
4678
  }
@@ -4823,7 +4932,7 @@ export default class Meeting extends StatelessWebexPlugin {
4823
4932
  * @param {Object} options - options to join with media
4824
4933
  * @param {JoinOptions} [options.joinOptions] - see #join()
4825
4934
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4826
- * @returns {Promise} -- {join: see join(), media: see addMedia()}
4935
+ * @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
4827
4936
  * @public
4828
4937
  * @memberof Meeting
4829
4938
  * @example
@@ -4913,6 +5022,7 @@ export default class Meeting extends StatelessWebexPlugin {
4913
5022
  return {
4914
5023
  join: joinResponse,
4915
5024
  media: mediaResponse,
5025
+ multistreamEnabled: this.isMultistream,
4916
5026
  };
4917
5027
  } catch (error) {
4918
5028
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -4921,7 +5031,17 @@ export default class Meeting extends StatelessWebexPlugin {
4921
5031
 
4922
5032
  this.roap.abortTurnDiscovery();
4923
5033
 
4924
- if (joined && isRetry) {
5034
+ // if this was the first attempt, let's do a retry
5035
+ let shouldRetry = !isRetry;
5036
+
5037
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
5038
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
5039
+ // so there is no point doing a retry
5040
+ shouldRetry = false;
5041
+ }
5042
+
5043
+ // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
5044
+ if (joined && (isRetry || !shouldRetry)) {
4925
5045
  try {
4926
5046
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4927
5047
  } catch (e) {
@@ -4945,15 +5065,6 @@ export default class Meeting extends StatelessWebexPlugin {
4945
5065
  }
4946
5066
  );
4947
5067
 
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
5068
  if (shouldRetry) {
4958
5069
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4959
5070
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5209,7 +5320,16 @@ export default class Meeting extends StatelessWebexPlugin {
5209
5320
  (this.config.receiveReactions || options.receiveReactions) &&
5210
5321
  this.isReactionsSupported()
5211
5322
  ) {
5212
- const {name} = this.members.membersCollection.get(e.data.sender.participantId);
5323
+ const member = this.members.membersCollection.get(e.data.sender.participantId);
5324
+ if (!member) {
5325
+ // @ts-ignore -- fix type
5326
+ LoggerProxy.logger.warn(
5327
+ `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
5328
+ );
5329
+ break;
5330
+ }
5331
+
5332
+ const {name} = member;
5213
5333
  const processedReaction: ProcessedReaction = {
5214
5334
  reaction: e.data.reaction,
5215
5335
  sender: {
@@ -5263,6 +5383,9 @@ export default class Meeting extends StatelessWebexPlugin {
5263
5383
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5264
5384
  );
5265
5385
 
5386
+ // @ts-ignore
5387
+ this.webex.internal.voicea.deregisterEvents();
5388
+
5266
5389
  this.areVoiceaEventsSetup = false;
5267
5390
  this.triggerStopReceivingTranscriptionEvent();
5268
5391
  }
@@ -5373,16 +5496,19 @@ export default class Meeting extends StatelessWebexPlugin {
5373
5496
  this.meetingFiniteStateMachine.reset();
5374
5497
  }
5375
5498
 
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
- });
5499
+ // send client.call.initiated unless told not to
5500
+ if (options.sendCallInitiated !== false) {
5501
+ // @ts-ignore
5502
+ this.webex.internal.newMetrics.submitClientEvent({
5503
+ name: 'client.call.initiated',
5504
+ payload: {
5505
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5506
+ isRoapCallEnabled: true,
5507
+ pstnAudioType: options?.pstnAudioType,
5508
+ },
5509
+ options: {meetingId: this.id},
5510
+ });
5511
+ }
5386
5512
 
5387
5513
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
5388
5514
 
@@ -5570,17 +5696,23 @@ export default class Meeting extends StatelessWebexPlugin {
5570
5696
  */
5571
5697
  async updateLLMConnection() {
5572
5698
  // @ts-ignore - Fix type
5573
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5699
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5574
5700
 
5575
5701
  const isJoined = this.isJoined();
5576
5702
 
5703
+ // webinar panelist should use new data channel in practice session
5704
+ const dataChannelUrl =
5705
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5706
+ ? practiceSessionDatachannelUrl
5707
+ : datachannelUrl;
5708
+
5577
5709
  // @ts-ignore - Fix type
5578
5710
  if (this.webex.internal.llm.isConnected()) {
5579
5711
  if (
5580
5712
  // @ts-ignore - Fix type
5581
5713
  url === this.webex.internal.llm.getLocusUrl() &&
5582
5714
  // @ts-ignore - Fix type
5583
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5715
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5584
5716
  isJoined
5585
5717
  ) {
5586
5718
  return undefined;
@@ -5597,7 +5729,7 @@ export default class Meeting extends StatelessWebexPlugin {
5597
5729
 
5598
5730
  // @ts-ignore - Fix type
5599
5731
  return this.webex.internal.llm
5600
- .registerAndConnect(url, datachannelUrl)
5732
+ .registerAndConnect(url, dataChannelUrl)
5601
5733
  .then((registerAndConnectResult) => {
5602
5734
  // @ts-ignore - Fix type
5603
5735
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5969,6 +6101,11 @@ export default class Meeting extends StatelessWebexPlugin {
5969
6101
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5970
6102
  const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5971
6103
 
6104
+ if (this.isMultistream && mediaServer !== 'homer') {
6105
+ throw new MultistreamNotSupportedError(
6106
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6107
+ );
6108
+ }
5972
6109
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5973
6110
 
5974
6111
  if (mediaServer) {
@@ -6091,16 +6228,20 @@ export default class Meeting extends StatelessWebexPlugin {
6091
6228
  logText: `${LOG_HEADER} Roap Offer`,
6092
6229
  }
6093
6230
  ).catch((error) => {
6231
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6232
+
6094
6233
  // @ts-ignore
6095
6234
  this.webex.internal.newMetrics.submitClientEvent({
6096
6235
  name: 'client.media-engine.remote-sdp-received',
6097
6236
  payload: {
6098
- canProceed: false,
6237
+ canProceed: multistreamNotSupported,
6099
6238
  errors: [
6100
6239
  // @ts-ignore
6101
6240
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6102
6241
  {
6103
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6242
+ clientErrorCode: multistreamNotSupported
6243
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6244
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6104
6245
  }
6105
6246
  ),
6106
6247
  ],
@@ -6108,7 +6249,7 @@ export default class Meeting extends StatelessWebexPlugin {
6108
6249
  options: {meetingId: this.id, rawError: error},
6109
6250
  });
6110
6251
 
6111
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6252
+ this.deferSDPAnswer.reject(error);
6112
6253
  clearTimeout(this.sdpResponseTimer);
6113
6254
  this.sdpResponseTimer = undefined;
6114
6255
  });
@@ -6436,6 +6577,14 @@ export default class Meeting extends StatelessWebexPlugin {
6436
6577
  this.webex.meetings.geoHintInfo?.clientAddress ||
6437
6578
  options.data.intervalMetadata.peerReflexiveIP ||
6438
6579
  MQA_STATS.DEFAULT_IP;
6580
+
6581
+ const {members} = this.getMembers().membersCollection;
6582
+
6583
+ // Count members that are in the meeting
6584
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6585
+ (member: Member) => member.isInMeeting
6586
+ ).length;
6587
+
6439
6588
  // @ts-ignore
6440
6589
  this.webex.internal.newMetrics.submitMQE({
6441
6590
  name: 'client.mediaquality.event',
@@ -6767,32 +6916,6 @@ export default class Meeting extends StatelessWebexPlugin {
6767
6916
  }
6768
6917
  }
6769
6918
 
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
6919
  /**
6797
6920
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6798
6921
  * once the remote sdp answer has been received.
@@ -7016,7 +7139,9 @@ export default class Meeting extends StatelessWebexPlugin {
7016
7139
 
7017
7140
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
7018
7141
 
7019
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7142
+ LoggerProxy.logger.info(
7143
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7144
+ );
7020
7145
 
7021
7146
  if (this.isMultistream) {
7022
7147
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7094,6 +7219,33 @@ export default class Meeting extends StatelessWebexPlugin {
7094
7219
  }
7095
7220
  }
7096
7221
 
7222
+ /**
7223
+ * Cleans up stats analyzer, peer connection and other things before
7224
+ * we can create a new transcoded media connection
7225
+ *
7226
+ * @private
7227
+ * @returns {Promise<void>}
7228
+ */
7229
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7230
+ if (this.statsAnalyzer) {
7231
+ await this.statsAnalyzer.stopAnalyzer();
7232
+ }
7233
+ this.statsAnalyzer = null;
7234
+
7235
+ this.isMultistream = false;
7236
+
7237
+ if (this.mediaProperties.webrtcMediaConnection) {
7238
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7239
+ this.closePeerConnections(false);
7240
+
7241
+ this.mediaProperties.unsetPeerConnection();
7242
+ }
7243
+
7244
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7245
+
7246
+ this.createStatsAnalyzer();
7247
+ }
7248
+
7097
7249
  /**
7098
7250
  * Sends stats report, closes peer connection and cleans up any media connection
7099
7251
  * related things before trying to establish media connection again with turn server
@@ -7288,19 +7440,33 @@ export default class Meeting extends StatelessWebexPlugin {
7288
7440
 
7289
7441
  this.createStatsAnalyzer();
7290
7442
 
7291
- await this.establishMediaConnection(
7292
- remoteMediaManagerConfig,
7293
- bundlePolicy,
7294
- forceTurnDiscovery,
7295
- turnServerInfo
7296
- );
7443
+ try {
7444
+ await this.establishMediaConnection(
7445
+ remoteMediaManagerConfig,
7446
+ bundlePolicy,
7447
+ forceTurnDiscovery,
7448
+ turnServerInfo
7449
+ );
7450
+ } catch (error) {
7451
+ if (error instanceof MultistreamNotSupportedError) {
7452
+ LoggerProxy.logger.warn(
7453
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7454
+ );
7297
7455
 
7298
- if (audioEnabled || videoEnabled) {
7299
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7300
- } else {
7301
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7302
- }
7456
+ await this.downgradeFromMultistreamToTranscoded();
7303
7457
 
7458
+ // Establish new media connection with forced TURN discovery
7459
+ // 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
7460
+ await this.establishMediaConnection(
7461
+ remoteMediaManagerConfig,
7462
+ bundlePolicy,
7463
+ true,
7464
+ undefined
7465
+ );
7466
+ } else {
7467
+ throw error;
7468
+ }
7469
+ }
7304
7470
  if (this.mediaProperties.hasLocalShareStream()) {
7305
7471
  await this.enqueueScreenShareFloorRequest();
7306
7472
  }
@@ -8270,7 +8436,7 @@ export default class Meeting extends StatelessWebexPlugin {
8270
8436
  if (layoutType) {
8271
8437
  if (!LAYOUT_TYPES.includes(layoutType)) {
8272
8438
  return this.rejectWithErrorLog(
8273
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8439
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8274
8440
  );
8275
8441
  }
8276
8442
 
@@ -8426,6 +8592,12 @@ export default class Meeting extends StatelessWebexPlugin {
8426
8592
  correlationId: this.correlationId,
8427
8593
  muted,
8428
8594
  encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
8595
+ // TypeScript 4 does not recognize the `displaySurface` property. Instead of upgrading the
8596
+ // SDK to TypeScript 5, which may affect other packages, use bracket notation for now, since
8597
+ // all we're doing here is adding metrics.
8598
+ // eslint-disable-next-line dot-notation
8599
+ displaySurface: this.mediaProperties?.shareVideoStream?.getSettings()['displaySurface'],
8600
+ isMultistream: this.isMultistream,
8429
8601
  });
8430
8602
  };
8431
8603
 
@@ -8628,6 +8800,11 @@ export default class Meeting extends StatelessWebexPlugin {
8628
8800
  this.stopTranscription();
8629
8801
  this.transcription = undefined;
8630
8802
  }
8803
+
8804
+ this.annotation.deregisterEvents();
8805
+
8806
+ // @ts-ignore - fix types
8807
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8631
8808
  };
8632
8809
 
8633
8810
  /**
@@ -8665,10 +8842,12 @@ export default class Meeting extends StatelessWebexPlugin {
8665
8842
 
8666
8843
  return;
8667
8844
  }
8668
- const {keepAliveUrl} = this.joinedWith;
8845
+
8669
8846
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8670
8847
 
8671
8848
  this.keepAliveTimerId = setInterval(() => {
8849
+ const {keepAliveUrl} = this.joinedWith;
8850
+
8672
8851
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8673
8852
  LoggerProxy.logger.warn(
8674
8853
  `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
  }
@@ -27,7 +27,7 @@ import {
27
27
  _SLIDES_,
28
28
  ANNOTATION,
29
29
  } from '../constants';
30
- import {SendReactionOptions, ToggleReactionsOptions} from './request.type';
30
+ import {SendReactionOptions, BrbOptions, ToggleReactionsOptions} from './request.type';
31
31
  import MeetingUtil from './util';
32
32
  import {AnnotationInfo} from '../annotation/annotation.types';
33
33
  import {ClientMediaPreferences} from '../reachability/reachability.types';
@@ -909,4 +909,29 @@ export default class MeetingRequest extends StatelessWebexPlugin {
909
909
  uri: locusUrl,
910
910
  });
911
911
  }
912
+
913
+ /**
914
+ * Sends a request to set be right back status.
915
+ *
916
+ * @param {Object} options - The options for brb request.
917
+ * @param {boolean} options.enabled - Whether brb status is enabled.
918
+ * @param {string} options.locusUrl - The URL of the locus.
919
+ * @param {string} options.deviceUrl - The URL of the device.
920
+ * @param {string} options.selfId - The ID of the participant.
921
+ * @returns {Promise}
922
+ */
923
+ setBrb({enabled, locusUrl, deviceUrl, selfId}: BrbOptions) {
924
+ const uri = `${locusUrl}/${PARTICIPANT}/${selfId}/${CONTROLS}`;
925
+
926
+ return this.locusDeltaRequest({
927
+ method: HTTP_VERBS.PATCH,
928
+ uri,
929
+ body: {
930
+ brb: {
931
+ enabled,
932
+ deviceUrl,
933
+ },
934
+ },
935
+ });
936
+ }
912
937
  }