@webex/plugin-meetings 3.7.0-next.4 → 3.7.0-next.41

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 (131) 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/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  6. package/dist/common/errors/join-webinar-error.js.map +1 -0
  7. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  8. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +36 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js +16 -11
  14. package/dist/index.js.map +1 -1
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/index.js +13 -2
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/selfUtils.js +30 -17
  20. package/dist/locus-info/selfUtils.js.map +1 -1
  21. package/dist/meeting/in-meeting-actions.js +13 -1
  22. package/dist/meeting/in-meeting-actions.js.map +1 -1
  23. package/dist/meeting/index.js +922 -800
  24. package/dist/meeting/index.js.map +1 -1
  25. package/dist/meeting/locusMediaRequest.js +9 -0
  26. package/dist/meeting/locusMediaRequest.js.map +1 -1
  27. package/dist/meeting/request.js +30 -0
  28. package/dist/meeting/request.js.map +1 -1
  29. package/dist/meeting/request.type.js.map +1 -1
  30. package/dist/meeting/util.js +16 -16
  31. package/dist/meeting/util.js.map +1 -1
  32. package/dist/meeting-info/meeting-info-v2.js +29 -17
  33. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  34. package/dist/meetings/index.js +6 -3
  35. package/dist/meetings/index.js.map +1 -1
  36. package/dist/meetings/util.js +1 -1
  37. package/dist/meetings/util.js.map +1 -1
  38. package/dist/member/index.js +9 -0
  39. package/dist/member/index.js.map +1 -1
  40. package/dist/member/types.js.map +1 -1
  41. package/dist/member/util.js +39 -28
  42. package/dist/member/util.js.map +1 -1
  43. package/dist/members/util.js +4 -2
  44. package/dist/members/util.js.map +1 -1
  45. package/dist/metrics/constants.js +1 -1
  46. package/dist/metrics/constants.js.map +1 -1
  47. package/dist/multistream/remoteMedia.js +30 -15
  48. package/dist/multistream/remoteMedia.js.map +1 -1
  49. package/dist/multistream/sendSlotManager.js +24 -0
  50. package/dist/multistream/sendSlotManager.js.map +1 -1
  51. package/dist/recording-controller/enums.js +8 -4
  52. package/dist/recording-controller/enums.js.map +1 -1
  53. package/dist/recording-controller/index.js +18 -9
  54. package/dist/recording-controller/index.js.map +1 -1
  55. package/dist/recording-controller/util.js +13 -9
  56. package/dist/recording-controller/util.js.map +1 -1
  57. package/dist/roap/index.js +10 -8
  58. package/dist/roap/index.js.map +1 -1
  59. package/dist/types/annotation/index.d.ts +5 -0
  60. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  61. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  62. package/dist/types/constants.d.ts +28 -1
  63. package/dist/types/index.d.ts +3 -3
  64. package/dist/types/locus-info/index.d.ts +2 -1
  65. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  66. package/dist/types/meeting/index.d.ts +20 -12
  67. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  68. package/dist/types/meeting/request.d.ts +12 -1
  69. package/dist/types/meeting/request.type.d.ts +6 -0
  70. package/dist/types/meeting/util.d.ts +1 -1
  71. package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
  72. package/dist/types/meetings/index.d.ts +3 -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/members/util.d.ts +2 -0
  76. package/dist/types/metrics/constants.d.ts +1 -1
  77. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  78. package/dist/types/recording-controller/enums.d.ts +5 -2
  79. package/dist/types/recording-controller/index.d.ts +1 -0
  80. package/dist/types/recording-controller/util.d.ts +2 -1
  81. package/dist/webinar/index.js +390 -7
  82. package/dist/webinar/index.js.map +1 -1
  83. package/package.json +23 -22
  84. package/src/annotation/index.ts +16 -0
  85. package/src/common/errors/join-webinar-error.ts +24 -0
  86. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  87. package/src/config.ts +1 -1
  88. package/src/constants.ts +33 -3
  89. package/src/index.ts +5 -3
  90. package/src/locus-info/index.ts +17 -2
  91. package/src/locus-info/selfUtils.ts +19 -6
  92. package/src/meeting/in-meeting-actions.ts +25 -0
  93. package/src/meeting/index.ts +257 -80
  94. package/src/meeting/locusMediaRequest.ts +7 -0
  95. package/src/meeting/request.ts +26 -1
  96. package/src/meeting/request.type.ts +7 -0
  97. package/src/meeting/util.ts +8 -10
  98. package/src/meeting-info/meeting-info-v2.ts +23 -11
  99. package/src/meetings/index.ts +8 -2
  100. package/src/meetings/util.ts +2 -1
  101. package/src/member/index.ts +9 -0
  102. package/src/member/types.ts +8 -0
  103. package/src/member/util.ts +34 -24
  104. package/src/members/util.ts +1 -0
  105. package/src/metrics/constants.ts +1 -1
  106. package/src/multistream/remoteMedia.ts +28 -15
  107. package/src/multistream/sendSlotManager.ts +31 -0
  108. package/src/recording-controller/enums.ts +5 -2
  109. package/src/recording-controller/index.ts +17 -4
  110. package/src/recording-controller/util.ts +20 -5
  111. package/src/roap/index.ts +10 -8
  112. package/src/webinar/index.ts +235 -9
  113. package/test/unit/spec/annotation/index.ts +46 -1
  114. package/test/unit/spec/locus-info/index.js +222 -0
  115. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  116. package/test/unit/spec/locus-info/selfUtils.js +91 -1
  117. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
  118. package/test/unit/spec/meeting/index.js +685 -102
  119. package/test/unit/spec/meeting/utils.js +22 -19
  120. package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
  121. package/test/unit/spec/meetings/index.js +9 -5
  122. package/test/unit/spec/meetings/utils.js +10 -0
  123. package/test/unit/spec/member/util.js +52 -11
  124. package/test/unit/spec/members/utils.js +95 -0
  125. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  126. package/test/unit/spec/recording-controller/index.js +61 -5
  127. package/test/unit/spec/recording-controller/util.js +39 -3
  128. package/test/unit/spec/roap/index.ts +47 -0
  129. package/test/unit/spec/webinar/index.ts +504 -0
  130. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  131. 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,8 @@ 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,
125
126
  } from '../constants';
126
127
  import BEHAVIORAL_METRICS from '../metrics/constants';
127
128
  import ParameterError from '../common/errors/parameter';
@@ -129,7 +130,7 @@ import {
129
130
  MeetingInfoV2PasswordError,
130
131
  MeetingInfoV2CaptchaError,
131
132
  MeetingInfoV2PolicyError,
132
- MeetingInfoV2WebinarRegistrationError,
133
+ MeetingInfoV2JoinWebinarError,
133
134
  } from '../meeting-info/meeting-info-v2';
134
135
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
135
136
  import SendSlotManager from '../multistream/sendSlotManager';
@@ -158,7 +159,9 @@ import ControlsOptionsManager from '../controls-options-manager';
158
159
  import PermissionError from '../common/errors/permission';
159
160
  import {LocusMediaRequest} from './locusMediaRequest';
160
161
  import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
161
- import WebinarRegistrationError from '../common/errors/webinar-registration-error';
162
+ import JoinWebinarError from '../common/errors/join-webinar-error';
163
+ import Member from '../member';
164
+ import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
162
165
 
163
166
  // default callback so we don't call an undefined function, but in practice it should never be used
164
167
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -848,7 +851,7 @@ export default class Meeting extends StatelessWebexPlugin {
848
851
  * @memberof Meeting
849
852
  */
850
853
  // @ts-ignore
851
- this.webinar = new Webinar({}, {parent: this.webex});
854
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
852
855
  /**
853
856
  * helper class for managing receive slots (for multistream media connections)
854
857
  */
@@ -1767,15 +1770,20 @@ export default class Meeting extends StatelessWebexPlugin {
1767
1770
  this.meetingInfo = err.meetingInfo;
1768
1771
  }
1769
1772
  throw new PermissionError();
1770
- } else if (err instanceof MeetingInfoV2WebinarRegistrationError) {
1773
+ } else if (err instanceof MeetingInfoV2JoinWebinarError) {
1771
1774
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION;
1775
+ if (WEBINAR_ERROR_WEBCAST.includes(err.wbxAppApiCode)) {
1776
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST;
1777
+ } else if (WEBINAR_ERROR_REGISTRATIONID.includes(err.wbxAppApiCode)) {
1778
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID;
1779
+ }
1772
1780
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1773
1781
 
1774
1782
  if (err.meetingInfo) {
1775
1783
  this.meetingInfo = err.meetingInfo;
1776
1784
  }
1777
1785
 
1778
- throw new WebinarRegistrationError();
1786
+ throw new JoinWebinarError();
1779
1787
  } else if (err instanceof MeetingInfoV2PasswordError) {
1780
1788
  LoggerProxy.logger.info(
1781
1789
  // @ts-ignore
@@ -2660,6 +2668,7 @@ export default class Meeting extends StatelessWebexPlugin {
2660
2668
  });
2661
2669
 
2662
2670
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, ({state}) => {
2671
+ this.webinar.updatePracticeSessionStatus(state);
2663
2672
  Trigger.trigger(
2664
2673
  this,
2665
2674
  {file: 'meeting/index', function: 'setupLocusControlsListener'},
@@ -2733,6 +2742,7 @@ export default class Meeting extends StatelessWebexPlugin {
2733
2742
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2734
2743
 
2735
2744
  if (
2745
+ !payload.forceUpdate &&
2736
2746
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2737
2747
  contentShare.disposition === previousContentShare?.disposition &&
2738
2748
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2779,7 +2789,11 @@ export default class Meeting extends StatelessWebexPlugin {
2779
2789
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2780
2790
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2781
2791
  // WHITEBOARD - sharing whiteboard
2782
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2792
+ // Webinar attendee should receive whiteboard as remote share
2793
+ newShareStatus =
2794
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2795
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2796
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2783
2797
  }
2784
2798
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2785
2799
  else if (
@@ -2794,6 +2808,7 @@ export default class Meeting extends StatelessWebexPlugin {
2794
2808
  LoggerProxy.logger.info(
2795
2809
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2796
2810
  );
2811
+
2797
2812
  if (newShareStatus !== this.shareStatus) {
2798
2813
  const oldShareStatus = this.shareStatus;
2799
2814
 
@@ -3051,7 +3066,20 @@ export default class Meeting extends StatelessWebexPlugin {
3051
3066
  */
3052
3067
  private setUpLocusResourcesListener() {
3053
3068
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3054
- this.webinar.updateWebcastUrl(payload);
3069
+ if (payload) {
3070
+ this.webinar.updateWebcastUrl(payload);
3071
+ Trigger.trigger(
3072
+ this,
3073
+ {
3074
+ file: 'meeting/index',
3075
+ function: 'setUpLocusInfoMeetingInfoListener',
3076
+ },
3077
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3078
+ {
3079
+ payload,
3080
+ }
3081
+ );
3082
+ }
3055
3083
  });
3056
3084
  }
3057
3085
 
@@ -3361,6 +3389,20 @@ export default class Meeting extends StatelessWebexPlugin {
3361
3389
  }
3362
3390
  });
3363
3391
 
3392
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
3393
+ Trigger.trigger(
3394
+ this,
3395
+ {
3396
+ file: 'meeting/index',
3397
+ function: 'setUpLocusInfoSelfListener',
3398
+ },
3399
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
3400
+ {
3401
+ payload,
3402
+ }
3403
+ );
3404
+ });
3405
+
3364
3406
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3365
3407
  const isModeratorOrCohost =
3366
3408
  payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3370,6 +3412,7 @@ export default class Meeting extends StatelessWebexPlugin {
3370
3412
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3371
3413
  );
3372
3414
  this.webinar.updateRoleChanged(payload);
3415
+
3373
3416
  Trigger.trigger(
3374
3417
  this,
3375
3418
  {
@@ -3516,6 +3559,7 @@ export default class Meeting extends StatelessWebexPlugin {
3516
3559
  emailAddress: string;
3517
3560
  email: string;
3518
3561
  phoneNumber: string;
3562
+ roles: Array<string>;
3519
3563
  },
3520
3564
  alertIfActive = true
3521
3565
  ) {
@@ -3563,6 +3607,50 @@ export default class Meeting extends StatelessWebexPlugin {
3563
3607
  return this.members.admitMembers(memberIds, locusUrls);
3564
3608
  }
3565
3609
 
3610
+ /**
3611
+ * Manages be right back status updates for the current participant.
3612
+ *
3613
+ * @param {boolean} enabled - Indicates whether the user enabled brb or not.
3614
+ * @returns {Promise<void>} resolves when the brb status is updated or does nothing if not in a multistream meeting.
3615
+ * @throws {Error} - Throws an error if the request fails.
3616
+ */
3617
+ public async beRightBack(enabled: boolean): Promise<void> {
3618
+ if (!this.isMultistream) {
3619
+ const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
3620
+ const error = new Error(errorMessage);
3621
+
3622
+ LoggerProxy.logger.error(error);
3623
+
3624
+ return Promise.reject(error);
3625
+ }
3626
+
3627
+ if (!this.mediaProperties.webrtcMediaConnection) {
3628
+ const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
3629
+ const error = new Error(errorMessage);
3630
+
3631
+ LoggerProxy.logger.error(error);
3632
+
3633
+ return Promise.reject(error);
3634
+ }
3635
+
3636
+ // this logic should be applied only to multistream meetings
3637
+ return this.meetingRequest
3638
+ .setBrb({
3639
+ enabled,
3640
+ locusUrl: this.locusUrl,
3641
+ deviceUrl: this.deviceUrl,
3642
+ selfId: this.selfId,
3643
+ })
3644
+ .then(() => {
3645
+ this.sendSlotManager.setSourceStateOverride(MediaType.VideoMain, enabled ? 'away' : null);
3646
+ })
3647
+ .catch((error) => {
3648
+ LoggerProxy.logger.error('Meeting:index#beRightBack --> Error ', error);
3649
+
3650
+ return Promise.reject(error);
3651
+ });
3652
+ }
3653
+
3566
3654
  /**
3567
3655
  * Remove the member from the meeting, boot them
3568
3656
  * @param {String} memberId
@@ -3772,6 +3860,10 @@ export default class Meeting extends StatelessWebexPlugin {
3772
3860
  this.userDisplayHints,
3773
3861
  this.selfUserPolicies
3774
3862
  ),
3863
+ isPremiseRecordingEnabled: RecordingUtil.isPremiseRecordingEnabled(
3864
+ this.userDisplayHints,
3865
+ this.selfUserPolicies
3866
+ ),
3775
3867
  canRaiseHand: MeetingUtil.canUserRaiseHand(this.userDisplayHints),
3776
3868
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(this.userDisplayHints),
3777
3869
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(this.userDisplayHints),
@@ -3798,6 +3890,7 @@ export default class Meeting extends StatelessWebexPlugin {
3798
3890
  this.userDisplayHints
3799
3891
  ),
3800
3892
  canManageBreakout: MeetingUtil.canManageBreakout(this.userDisplayHints),
3893
+ canStartBreakout: MeetingUtil.canStartBreakout(this.userDisplayHints),
3801
3894
  canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
3802
3895
  this.userDisplayHints,
3803
3896
  this.selfUserPolicies
@@ -3915,6 +4008,22 @@ export default class Meeting extends StatelessWebexPlugin {
3915
4008
  requiredHints: [DISPLAY_HINTS.DISABLE_STAGE_VIEW],
3916
4009
  displayHints: this.userDisplayHints,
3917
4010
  }),
4011
+ isPracticeSessionOn: ControlsOptionsUtil.hasHints({
4012
+ requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_ON],
4013
+ displayHints: this.userDisplayHints,
4014
+ }),
4015
+ isPracticeSessionOff: ControlsOptionsUtil.hasHints({
4016
+ requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_OFF],
4017
+ displayHints: this.userDisplayHints,
4018
+ }),
4019
+ canStartPracticeSession: ControlsOptionsUtil.hasHints({
4020
+ requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_START],
4021
+ displayHints: this.userDisplayHints,
4022
+ }),
4023
+ canStopPracticeSession: ControlsOptionsUtil.hasHints({
4024
+ requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_STOP],
4025
+ displayHints: this.userDisplayHints,
4026
+ }),
3918
4027
  canShareFile:
3919
4028
  (ControlsOptionsUtil.hasHints({
3920
4029
  requiredHints: [DISPLAY_HINTS.SHARE_FILE],
@@ -4077,10 +4186,11 @@ export default class Meeting extends StatelessWebexPlugin {
4077
4186
  */
4078
4187
  private setLogUploadTimer() {
4079
4188
  // 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
4080
- const LOG_UPLOAD_INTERVALS = [0.1, 1, 15, 15, 30, 30, 30, 60];
4189
+ const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
4081
4190
 
4082
4191
  const delay =
4083
4192
  1000 *
4193
+ 60 *
4084
4194
  // @ts-ignore - config coming from registerPlugin
4085
4195
  this.config.logUploadIntervalMultiplicationFactor *
4086
4196
  LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
@@ -4519,11 +4629,12 @@ export default class Meeting extends StatelessWebexPlugin {
4519
4629
  * Close the peer connections and remove them from the class.
4520
4630
  * Cleanup any media connection related things.
4521
4631
  *
4632
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4522
4633
  * @returns {Promise}
4523
4634
  * @public
4524
4635
  * @memberof Meeting
4525
4636
  */
4526
- public closePeerConnections() {
4637
+ public closePeerConnections(resetMuteStates = true) {
4527
4638
  if (this.mediaProperties.webrtcMediaConnection) {
4528
4639
  if (this.remoteMediaManager) {
4529
4640
  this.remoteMediaManager.stop();
@@ -4540,8 +4651,10 @@ export default class Meeting extends StatelessWebexPlugin {
4540
4651
  this.setNetworkStatus(undefined);
4541
4652
  }
4542
4653
 
4543
- this.audio = null;
4544
- this.video = null;
4654
+ if (resetMuteStates) {
4655
+ this.audio = null;
4656
+ this.video = null;
4657
+ }
4545
4658
 
4546
4659
  return Promise.resolve();
4547
4660
  }
@@ -4801,7 +4914,7 @@ export default class Meeting extends StatelessWebexPlugin {
4801
4914
  * @param {Object} options - options to join with media
4802
4915
  * @param {JoinOptions} [options.joinOptions] - see #join()
4803
4916
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4804
- * @returns {Promise} -- {join: see join(), media: see addMedia()}
4917
+ * @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
4805
4918
  * @public
4806
4919
  * @memberof Meeting
4807
4920
  * @example
@@ -4891,6 +5004,7 @@ export default class Meeting extends StatelessWebexPlugin {
4891
5004
  return {
4892
5005
  join: joinResponse,
4893
5006
  media: mediaResponse,
5007
+ multistreamEnabled: this.isMultistream,
4894
5008
  };
4895
5009
  } catch (error) {
4896
5010
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -4899,7 +5013,17 @@ export default class Meeting extends StatelessWebexPlugin {
4899
5013
 
4900
5014
  this.roap.abortTurnDiscovery();
4901
5015
 
4902
- if (joined && isRetry) {
5016
+ // if this was the first attempt, let's do a retry
5017
+ let shouldRetry = !isRetry;
5018
+
5019
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
5020
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
5021
+ // so there is no point doing a retry
5022
+ shouldRetry = false;
5023
+ }
5024
+
5025
+ // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
5026
+ if (joined && (isRetry || !shouldRetry)) {
4903
5027
  try {
4904
5028
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4905
5029
  } catch (e) {
@@ -4923,15 +5047,6 @@ export default class Meeting extends StatelessWebexPlugin {
4923
5047
  }
4924
5048
  );
4925
5049
 
4926
- // if this was the first attempt, let's do a retry
4927
- let shouldRetry = !isRetry;
4928
-
4929
- if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
4930
- // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
4931
- // so there is no point doing a retry
4932
- shouldRetry = false;
4933
- }
4934
-
4935
5050
  if (shouldRetry) {
4936
5051
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4937
5052
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5187,7 +5302,16 @@ export default class Meeting extends StatelessWebexPlugin {
5187
5302
  (this.config.receiveReactions || options.receiveReactions) &&
5188
5303
  this.isReactionsSupported()
5189
5304
  ) {
5190
- const {name} = this.members.membersCollection.get(e.data.sender.participantId);
5305
+ const member = this.members.membersCollection.get(e.data.sender.participantId);
5306
+ if (!member) {
5307
+ // @ts-ignore -- fix type
5308
+ LoggerProxy.logger.warn(
5309
+ `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
5310
+ );
5311
+ break;
5312
+ }
5313
+
5314
+ const {name} = member;
5191
5315
  const processedReaction: ProcessedReaction = {
5192
5316
  reaction: e.data.reaction,
5193
5317
  sender: {
@@ -5241,6 +5365,9 @@ export default class Meeting extends StatelessWebexPlugin {
5241
5365
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5242
5366
  );
5243
5367
 
5368
+ // @ts-ignore
5369
+ this.webex.internal.voicea.deregisterEvents();
5370
+
5244
5371
  this.areVoiceaEventsSetup = false;
5245
5372
  this.triggerStopReceivingTranscriptionEvent();
5246
5373
  }
@@ -5351,16 +5478,19 @@ export default class Meeting extends StatelessWebexPlugin {
5351
5478
  this.meetingFiniteStateMachine.reset();
5352
5479
  }
5353
5480
 
5354
- // @ts-ignore
5355
- this.webex.internal.newMetrics.submitClientEvent({
5356
- name: 'client.call.initiated',
5357
- payload: {
5358
- trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5359
- isRoapCallEnabled: true,
5360
- pstnAudioType: options?.pstnAudioType,
5361
- },
5362
- options: {meetingId: this.id},
5363
- });
5481
+ // send client.call.initiated unless told not to
5482
+ if (options.sendCallInitiated !== false) {
5483
+ // @ts-ignore
5484
+ this.webex.internal.newMetrics.submitClientEvent({
5485
+ name: 'client.call.initiated',
5486
+ payload: {
5487
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5488
+ isRoapCallEnabled: true,
5489
+ pstnAudioType: options?.pstnAudioType,
5490
+ },
5491
+ options: {meetingId: this.id},
5492
+ });
5493
+ }
5364
5494
 
5365
5495
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
5366
5496
 
@@ -5548,17 +5678,23 @@ export default class Meeting extends StatelessWebexPlugin {
5548
5678
  */
5549
5679
  async updateLLMConnection() {
5550
5680
  // @ts-ignore - Fix type
5551
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5681
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5552
5682
 
5553
5683
  const isJoined = this.isJoined();
5554
5684
 
5685
+ // webinar panelist should use new data channel in practice session
5686
+ const dataChannelUrl =
5687
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5688
+ ? practiceSessionDatachannelUrl
5689
+ : datachannelUrl;
5690
+
5555
5691
  // @ts-ignore - Fix type
5556
5692
  if (this.webex.internal.llm.isConnected()) {
5557
5693
  if (
5558
5694
  // @ts-ignore - Fix type
5559
5695
  url === this.webex.internal.llm.getLocusUrl() &&
5560
5696
  // @ts-ignore - Fix type
5561
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5697
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5562
5698
  isJoined
5563
5699
  ) {
5564
5700
  return undefined;
@@ -5575,7 +5711,7 @@ export default class Meeting extends StatelessWebexPlugin {
5575
5711
 
5576
5712
  // @ts-ignore - Fix type
5577
5713
  return this.webex.internal.llm
5578
- .registerAndConnect(url, datachannelUrl)
5714
+ .registerAndConnect(url, dataChannelUrl)
5579
5715
  .then((registerAndConnectResult) => {
5580
5716
  // @ts-ignore - Fix type
5581
5717
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5947,6 +6083,11 @@ export default class Meeting extends StatelessWebexPlugin {
5947
6083
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5948
6084
  const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5949
6085
 
6086
+ if (this.isMultistream && mediaServer !== 'homer') {
6087
+ throw new MultistreamNotSupportedError(
6088
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6089
+ );
6090
+ }
5950
6091
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5951
6092
 
5952
6093
  if (mediaServer) {
@@ -6069,16 +6210,20 @@ export default class Meeting extends StatelessWebexPlugin {
6069
6210
  logText: `${LOG_HEADER} Roap Offer`,
6070
6211
  }
6071
6212
  ).catch((error) => {
6213
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6214
+
6072
6215
  // @ts-ignore
6073
6216
  this.webex.internal.newMetrics.submitClientEvent({
6074
6217
  name: 'client.media-engine.remote-sdp-received',
6075
6218
  payload: {
6076
- canProceed: false,
6219
+ canProceed: multistreamNotSupported,
6077
6220
  errors: [
6078
6221
  // @ts-ignore
6079
6222
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6080
6223
  {
6081
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6224
+ clientErrorCode: multistreamNotSupported
6225
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6226
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6082
6227
  }
6083
6228
  ),
6084
6229
  ],
@@ -6086,7 +6231,7 @@ export default class Meeting extends StatelessWebexPlugin {
6086
6231
  options: {meetingId: this.id, rawError: error},
6087
6232
  });
6088
6233
 
6089
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6234
+ this.deferSDPAnswer.reject(error);
6090
6235
  clearTimeout(this.sdpResponseTimer);
6091
6236
  this.sdpResponseTimer = undefined;
6092
6237
  });
@@ -6414,6 +6559,14 @@ export default class Meeting extends StatelessWebexPlugin {
6414
6559
  this.webex.meetings.geoHintInfo?.clientAddress ||
6415
6560
  options.data.intervalMetadata.peerReflexiveIP ||
6416
6561
  MQA_STATS.DEFAULT_IP;
6562
+
6563
+ const {members} = this.getMembers().membersCollection;
6564
+
6565
+ // Count members that are in the meeting
6566
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6567
+ (member: Member) => member.isInMeeting
6568
+ ).length;
6569
+
6417
6570
  // @ts-ignore
6418
6571
  this.webex.internal.newMetrics.submitMQE({
6419
6572
  name: 'client.mediaquality.event',
@@ -6745,32 +6898,6 @@ export default class Meeting extends StatelessWebexPlugin {
6745
6898
  }
6746
6899
  }
6747
6900
 
6748
- /**
6749
- * Handles device logging
6750
- *
6751
- * @private
6752
- * @static
6753
- * @param {boolean} isAudioEnabled
6754
- * @param {boolean} isVideoEnabled
6755
- * @returns {Promise<void>}
6756
- */
6757
-
6758
- private static async handleDeviceLogging(isAudioEnabled, isVideoEnabled): Promise<void> {
6759
- try {
6760
- let devices = [];
6761
- if (isVideoEnabled && isAudioEnabled) {
6762
- devices = await getDevices();
6763
- } else if (isVideoEnabled) {
6764
- devices = await getDevices(Media.DeviceKind.VIDEO_INPUT);
6765
- } else if (isAudioEnabled) {
6766
- devices = await getDevices(Media.DeviceKind.AUDIO_INPUT);
6767
- }
6768
- MeetingUtil.handleDeviceLogging(devices);
6769
- } catch {
6770
- // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
6771
- }
6772
- }
6773
-
6774
6901
  /**
6775
6902
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6776
6903
  * once the remote sdp answer has been received.
@@ -6994,7 +7121,9 @@ export default class Meeting extends StatelessWebexPlugin {
6994
7121
 
6995
7122
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
6996
7123
 
6997
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7124
+ LoggerProxy.logger.info(
7125
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7126
+ );
6998
7127
 
6999
7128
  if (this.isMultistream) {
7000
7129
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7072,6 +7201,33 @@ export default class Meeting extends StatelessWebexPlugin {
7072
7201
  }
7073
7202
  }
7074
7203
 
7204
+ /**
7205
+ * Cleans up stats analyzer, peer connection and other things before
7206
+ * we can create a new transcoded media connection
7207
+ *
7208
+ * @private
7209
+ * @returns {Promise<void>}
7210
+ */
7211
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7212
+ if (this.statsAnalyzer) {
7213
+ await this.statsAnalyzer.stopAnalyzer();
7214
+ }
7215
+ this.statsAnalyzer = null;
7216
+
7217
+ this.isMultistream = false;
7218
+
7219
+ if (this.mediaProperties.webrtcMediaConnection) {
7220
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7221
+ this.closePeerConnections(false);
7222
+
7223
+ this.mediaProperties.unsetPeerConnection();
7224
+ }
7225
+
7226
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7227
+
7228
+ this.createStatsAnalyzer();
7229
+ }
7230
+
7075
7231
  /**
7076
7232
  * Sends stats report, closes peer connection and cleans up any media connection
7077
7233
  * related things before trying to establish media connection again with turn server
@@ -7266,19 +7422,33 @@ export default class Meeting extends StatelessWebexPlugin {
7266
7422
 
7267
7423
  this.createStatsAnalyzer();
7268
7424
 
7269
- await this.establishMediaConnection(
7270
- remoteMediaManagerConfig,
7271
- bundlePolicy,
7272
- forceTurnDiscovery,
7273
- turnServerInfo
7274
- );
7425
+ try {
7426
+ await this.establishMediaConnection(
7427
+ remoteMediaManagerConfig,
7428
+ bundlePolicy,
7429
+ forceTurnDiscovery,
7430
+ turnServerInfo
7431
+ );
7432
+ } catch (error) {
7433
+ if (error instanceof MultistreamNotSupportedError) {
7434
+ LoggerProxy.logger.warn(
7435
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7436
+ );
7275
7437
 
7276
- if (audioEnabled || videoEnabled) {
7277
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7278
- } else {
7279
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7280
- }
7438
+ await this.downgradeFromMultistreamToTranscoded();
7281
7439
 
7440
+ // Establish new media connection with forced TURN discovery
7441
+ // 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
7442
+ await this.establishMediaConnection(
7443
+ remoteMediaManagerConfig,
7444
+ bundlePolicy,
7445
+ true,
7446
+ undefined
7447
+ );
7448
+ } else {
7449
+ throw error;
7450
+ }
7451
+ }
7282
7452
  if (this.mediaProperties.hasLocalShareStream()) {
7283
7453
  await this.enqueueScreenShareFloorRequest();
7284
7454
  }
@@ -8248,7 +8418,7 @@ export default class Meeting extends StatelessWebexPlugin {
8248
8418
  if (layoutType) {
8249
8419
  if (!LAYOUT_TYPES.includes(layoutType)) {
8250
8420
  return this.rejectWithErrorLog(
8251
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8421
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8252
8422
  );
8253
8423
  }
8254
8424
 
@@ -8606,6 +8776,11 @@ export default class Meeting extends StatelessWebexPlugin {
8606
8776
  this.stopTranscription();
8607
8777
  this.transcription = undefined;
8608
8778
  }
8779
+
8780
+ this.annotation.deregisterEvents();
8781
+
8782
+ // @ts-ignore - fix types
8783
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8609
8784
  };
8610
8785
 
8611
8786
  /**
@@ -8643,10 +8818,12 @@ export default class Meeting extends StatelessWebexPlugin {
8643
8818
 
8644
8819
  return;
8645
8820
  }
8646
- const {keepAliveUrl} = this.joinedWith;
8821
+
8647
8822
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8648
8823
 
8649
8824
  this.keepAliveTimerId = setInterval(() => {
8825
+ const {keepAliveUrl} = this.joinedWith;
8826
+
8650
8827
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8651
8828
  LoggerProxy.logger.warn(
8652
8829
  `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
  }