@webex/plugin-meetings 3.7.0-next.8 → 3.7.0-web-workers-keepalive.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 (107) 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 +26 -5
  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/index.js +903 -800
  22. package/dist/meeting/index.js.map +1 -1
  23. package/dist/meeting/locusMediaRequest.js +9 -0
  24. package/dist/meeting/locusMediaRequest.js.map +1 -1
  25. package/dist/meeting/request.js +30 -0
  26. package/dist/meeting/request.js.map +1 -1
  27. package/dist/meeting/request.type.js.map +1 -1
  28. package/dist/meeting/util.js +16 -16
  29. package/dist/meeting/util.js.map +1 -1
  30. package/dist/meeting-info/meeting-info-v2.js +29 -17
  31. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  32. package/dist/meetings/index.js +6 -3
  33. package/dist/meetings/index.js.map +1 -1
  34. package/dist/meetings/util.js +1 -1
  35. package/dist/meetings/util.js.map +1 -1
  36. package/dist/member/index.js +9 -0
  37. package/dist/member/index.js.map +1 -1
  38. package/dist/member/types.js.map +1 -1
  39. package/dist/member/util.js +39 -28
  40. package/dist/member/util.js.map +1 -1
  41. package/dist/metrics/constants.js +1 -1
  42. package/dist/metrics/constants.js.map +1 -1
  43. package/dist/multistream/remoteMedia.js +30 -15
  44. package/dist/multistream/remoteMedia.js.map +1 -1
  45. package/dist/multistream/sendSlotManager.js +24 -0
  46. package/dist/multistream/sendSlotManager.js.map +1 -1
  47. package/dist/roap/index.js +10 -8
  48. package/dist/roap/index.js.map +1 -1
  49. package/dist/types/annotation/index.d.ts +5 -0
  50. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  51. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  52. package/dist/types/constants.d.ts +20 -1
  53. package/dist/types/index.d.ts +3 -3
  54. package/dist/types/locus-info/index.d.ts +2 -1
  55. package/dist/types/meeting/index.d.ts +19 -12
  56. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  57. package/dist/types/meeting/request.d.ts +12 -1
  58. package/dist/types/meeting/request.type.d.ts +6 -0
  59. package/dist/types/meeting/util.d.ts +1 -1
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
  61. package/dist/types/meetings/index.d.ts +3 -0
  62. package/dist/types/member/index.d.ts +1 -0
  63. package/dist/types/member/types.d.ts +7 -0
  64. package/dist/types/metrics/constants.d.ts +1 -1
  65. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  66. package/dist/webinar/index.js +354 -3
  67. package/dist/webinar/index.js.map +1 -1
  68. package/package.json +23 -22
  69. package/src/annotation/index.ts +16 -0
  70. package/src/common/errors/join-webinar-error.ts +24 -0
  71. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  72. package/src/config.ts +1 -1
  73. package/src/constants.ts +23 -3
  74. package/src/index.ts +5 -3
  75. package/src/locus-info/index.ts +17 -2
  76. package/src/locus-info/selfUtils.ts +19 -6
  77. package/src/meeting/index.ts +234 -80
  78. package/src/meeting/locusMediaRequest.ts +7 -0
  79. package/src/meeting/request.ts +26 -1
  80. package/src/meeting/request.type.ts +7 -0
  81. package/src/meeting/util.ts +8 -10
  82. package/src/meeting-info/meeting-info-v2.ts +23 -11
  83. package/src/meetings/index.ts +8 -2
  84. package/src/meetings/util.ts +2 -1
  85. package/src/member/index.ts +9 -0
  86. package/src/member/types.ts +8 -0
  87. package/src/member/util.ts +34 -24
  88. package/src/metrics/constants.ts +1 -1
  89. package/src/multistream/remoteMedia.ts +28 -15
  90. package/src/multistream/sendSlotManager.ts +31 -0
  91. package/src/roap/index.ts +10 -8
  92. package/src/webinar/index.ts +197 -3
  93. package/test/unit/spec/annotation/index.ts +46 -1
  94. package/test/unit/spec/locus-info/index.js +222 -0
  95. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  96. package/test/unit/spec/locus-info/selfUtils.js +91 -1
  97. package/test/unit/spec/meeting/index.js +683 -103
  98. package/test/unit/spec/meeting/utils.js +22 -19
  99. package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
  100. package/test/unit/spec/meetings/index.js +9 -5
  101. package/test/unit/spec/meetings/utils.js +10 -0
  102. package/test/unit/spec/member/util.js +52 -11
  103. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  104. package/test/unit/spec/roap/index.ts +47 -0
  105. package/test/unit/spec/webinar/index.ts +457 -0
  106. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  107. 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
@@ -2734,6 +2742,7 @@ export default class Meeting extends StatelessWebexPlugin {
2734
2742
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2735
2743
 
2736
2744
  if (
2745
+ !payload.forceUpdate &&
2737
2746
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2738
2747
  contentShare.disposition === previousContentShare?.disposition &&
2739
2748
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2780,7 +2789,11 @@ export default class Meeting extends StatelessWebexPlugin {
2780
2789
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2781
2790
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2782
2791
  // WHITEBOARD - sharing whiteboard
2783
- 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;
2784
2797
  }
2785
2798
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2786
2799
  else if (
@@ -2795,6 +2808,7 @@ export default class Meeting extends StatelessWebexPlugin {
2795
2808
  LoggerProxy.logger.info(
2796
2809
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2797
2810
  );
2811
+
2798
2812
  if (newShareStatus !== this.shareStatus) {
2799
2813
  const oldShareStatus = this.shareStatus;
2800
2814
 
@@ -3052,7 +3066,20 @@ export default class Meeting extends StatelessWebexPlugin {
3052
3066
  */
3053
3067
  private setUpLocusResourcesListener() {
3054
3068
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3055
- 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
+ }
3056
3083
  });
3057
3084
  }
3058
3085
 
@@ -3362,6 +3389,20 @@ export default class Meeting extends StatelessWebexPlugin {
3362
3389
  }
3363
3390
  });
3364
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
+
3365
3406
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3366
3407
  const isModeratorOrCohost =
3367
3408
  payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3371,6 +3412,7 @@ export default class Meeting extends StatelessWebexPlugin {
3371
3412
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3372
3413
  );
3373
3414
  this.webinar.updateRoleChanged(payload);
3415
+
3374
3416
  Trigger.trigger(
3375
3417
  this,
3376
3418
  {
@@ -3565,6 +3607,50 @@ export default class Meeting extends StatelessWebexPlugin {
3565
3607
  return this.members.admitMembers(memberIds, locusUrls);
3566
3608
  }
3567
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
+
3568
3654
  /**
3569
3655
  * Remove the member from the meeting, boot them
3570
3656
  * @param {String} memberId
@@ -4099,10 +4185,11 @@ export default class Meeting extends StatelessWebexPlugin {
4099
4185
  */
4100
4186
  private setLogUploadTimer() {
4101
4187
  // 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];
4188
+ const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
4103
4189
 
4104
4190
  const delay =
4105
4191
  1000 *
4192
+ 60 *
4106
4193
  // @ts-ignore - config coming from registerPlugin
4107
4194
  this.config.logUploadIntervalMultiplicationFactor *
4108
4195
  LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
@@ -4541,11 +4628,12 @@ export default class Meeting extends StatelessWebexPlugin {
4541
4628
  * Close the peer connections and remove them from the class.
4542
4629
  * Cleanup any media connection related things.
4543
4630
  *
4631
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4544
4632
  * @returns {Promise}
4545
4633
  * @public
4546
4634
  * @memberof Meeting
4547
4635
  */
4548
- public closePeerConnections() {
4636
+ public closePeerConnections(resetMuteStates = true) {
4549
4637
  if (this.mediaProperties.webrtcMediaConnection) {
4550
4638
  if (this.remoteMediaManager) {
4551
4639
  this.remoteMediaManager.stop();
@@ -4562,8 +4650,10 @@ export default class Meeting extends StatelessWebexPlugin {
4562
4650
  this.setNetworkStatus(undefined);
4563
4651
  }
4564
4652
 
4565
- this.audio = null;
4566
- this.video = null;
4653
+ if (resetMuteStates) {
4654
+ this.audio = null;
4655
+ this.video = null;
4656
+ }
4567
4657
 
4568
4658
  return Promise.resolve();
4569
4659
  }
@@ -4823,7 +4913,7 @@ export default class Meeting extends StatelessWebexPlugin {
4823
4913
  * @param {Object} options - options to join with media
4824
4914
  * @param {JoinOptions} [options.joinOptions] - see #join()
4825
4915
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4826
- * @returns {Promise} -- {join: see join(), media: see addMedia()}
4916
+ * @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
4827
4917
  * @public
4828
4918
  * @memberof Meeting
4829
4919
  * @example
@@ -4913,6 +5003,7 @@ export default class Meeting extends StatelessWebexPlugin {
4913
5003
  return {
4914
5004
  join: joinResponse,
4915
5005
  media: mediaResponse,
5006
+ multistreamEnabled: this.isMultistream,
4916
5007
  };
4917
5008
  } catch (error) {
4918
5009
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -4921,7 +5012,17 @@ export default class Meeting extends StatelessWebexPlugin {
4921
5012
 
4922
5013
  this.roap.abortTurnDiscovery();
4923
5014
 
4924
- if (joined && isRetry) {
5015
+ // if this was the first attempt, let's do a retry
5016
+ let shouldRetry = !isRetry;
5017
+
5018
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
5019
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
5020
+ // so there is no point doing a retry
5021
+ shouldRetry = false;
5022
+ }
5023
+
5024
+ // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
5025
+ if (joined && (isRetry || !shouldRetry)) {
4925
5026
  try {
4926
5027
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4927
5028
  } catch (e) {
@@ -4945,15 +5046,6 @@ export default class Meeting extends StatelessWebexPlugin {
4945
5046
  }
4946
5047
  );
4947
5048
 
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
5049
  if (shouldRetry) {
4958
5050
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4959
5051
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5209,7 +5301,16 @@ export default class Meeting extends StatelessWebexPlugin {
5209
5301
  (this.config.receiveReactions || options.receiveReactions) &&
5210
5302
  this.isReactionsSupported()
5211
5303
  ) {
5212
- const {name} = this.members.membersCollection.get(e.data.sender.participantId);
5304
+ const member = this.members.membersCollection.get(e.data.sender.participantId);
5305
+ if (!member) {
5306
+ // @ts-ignore -- fix type
5307
+ LoggerProxy.logger.warn(
5308
+ `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
5309
+ );
5310
+ break;
5311
+ }
5312
+
5313
+ const {name} = member;
5213
5314
  const processedReaction: ProcessedReaction = {
5214
5315
  reaction: e.data.reaction,
5215
5316
  sender: {
@@ -5263,6 +5364,9 @@ export default class Meeting extends StatelessWebexPlugin {
5263
5364
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5264
5365
  );
5265
5366
 
5367
+ // @ts-ignore
5368
+ this.webex.internal.voicea.deregisterEvents();
5369
+
5266
5370
  this.areVoiceaEventsSetup = false;
5267
5371
  this.triggerStopReceivingTranscriptionEvent();
5268
5372
  }
@@ -5373,16 +5477,19 @@ export default class Meeting extends StatelessWebexPlugin {
5373
5477
  this.meetingFiniteStateMachine.reset();
5374
5478
  }
5375
5479
 
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
- });
5480
+ // send client.call.initiated unless told not to
5481
+ if (options.sendCallInitiated !== false) {
5482
+ // @ts-ignore
5483
+ this.webex.internal.newMetrics.submitClientEvent({
5484
+ name: 'client.call.initiated',
5485
+ payload: {
5486
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5487
+ isRoapCallEnabled: true,
5488
+ pstnAudioType: options?.pstnAudioType,
5489
+ },
5490
+ options: {meetingId: this.id},
5491
+ });
5492
+ }
5386
5493
 
5387
5494
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
5388
5495
 
@@ -5570,17 +5677,23 @@ export default class Meeting extends StatelessWebexPlugin {
5570
5677
  */
5571
5678
  async updateLLMConnection() {
5572
5679
  // @ts-ignore - Fix type
5573
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5680
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5574
5681
 
5575
5682
  const isJoined = this.isJoined();
5576
5683
 
5684
+ // webinar panelist should use new data channel in practice session
5685
+ const dataChannelUrl =
5686
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5687
+ ? practiceSessionDatachannelUrl
5688
+ : datachannelUrl;
5689
+
5577
5690
  // @ts-ignore - Fix type
5578
5691
  if (this.webex.internal.llm.isConnected()) {
5579
5692
  if (
5580
5693
  // @ts-ignore - Fix type
5581
5694
  url === this.webex.internal.llm.getLocusUrl() &&
5582
5695
  // @ts-ignore - Fix type
5583
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5696
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5584
5697
  isJoined
5585
5698
  ) {
5586
5699
  return undefined;
@@ -5597,7 +5710,7 @@ export default class Meeting extends StatelessWebexPlugin {
5597
5710
 
5598
5711
  // @ts-ignore - Fix type
5599
5712
  return this.webex.internal.llm
5600
- .registerAndConnect(url, datachannelUrl)
5713
+ .registerAndConnect(url, dataChannelUrl)
5601
5714
  .then((registerAndConnectResult) => {
5602
5715
  // @ts-ignore - Fix type
5603
5716
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5969,6 +6082,11 @@ export default class Meeting extends StatelessWebexPlugin {
5969
6082
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5970
6083
  const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5971
6084
 
6085
+ if (this.isMultistream && mediaServer !== 'homer') {
6086
+ throw new MultistreamNotSupportedError(
6087
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6088
+ );
6089
+ }
5972
6090
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5973
6091
 
5974
6092
  if (mediaServer) {
@@ -6091,16 +6209,20 @@ export default class Meeting extends StatelessWebexPlugin {
6091
6209
  logText: `${LOG_HEADER} Roap Offer`,
6092
6210
  }
6093
6211
  ).catch((error) => {
6212
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6213
+
6094
6214
  // @ts-ignore
6095
6215
  this.webex.internal.newMetrics.submitClientEvent({
6096
6216
  name: 'client.media-engine.remote-sdp-received',
6097
6217
  payload: {
6098
- canProceed: false,
6218
+ canProceed: multistreamNotSupported,
6099
6219
  errors: [
6100
6220
  // @ts-ignore
6101
6221
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6102
6222
  {
6103
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6223
+ clientErrorCode: multistreamNotSupported
6224
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6225
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6104
6226
  }
6105
6227
  ),
6106
6228
  ],
@@ -6108,7 +6230,7 @@ export default class Meeting extends StatelessWebexPlugin {
6108
6230
  options: {meetingId: this.id, rawError: error},
6109
6231
  });
6110
6232
 
6111
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6233
+ this.deferSDPAnswer.reject(error);
6112
6234
  clearTimeout(this.sdpResponseTimer);
6113
6235
  this.sdpResponseTimer = undefined;
6114
6236
  });
@@ -6436,6 +6558,14 @@ export default class Meeting extends StatelessWebexPlugin {
6436
6558
  this.webex.meetings.geoHintInfo?.clientAddress ||
6437
6559
  options.data.intervalMetadata.peerReflexiveIP ||
6438
6560
  MQA_STATS.DEFAULT_IP;
6561
+
6562
+ const {members} = this.getMembers().membersCollection;
6563
+
6564
+ // Count members that are in the meeting
6565
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6566
+ (member: Member) => member.isInMeeting
6567
+ ).length;
6568
+
6439
6569
  // @ts-ignore
6440
6570
  this.webex.internal.newMetrics.submitMQE({
6441
6571
  name: 'client.mediaquality.event',
@@ -6767,32 +6897,6 @@ export default class Meeting extends StatelessWebexPlugin {
6767
6897
  }
6768
6898
  }
6769
6899
 
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
6900
  /**
6797
6901
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6798
6902
  * once the remote sdp answer has been received.
@@ -7016,7 +7120,9 @@ export default class Meeting extends StatelessWebexPlugin {
7016
7120
 
7017
7121
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
7018
7122
 
7019
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7123
+ LoggerProxy.logger.info(
7124
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7125
+ );
7020
7126
 
7021
7127
  if (this.isMultistream) {
7022
7128
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7094,6 +7200,33 @@ export default class Meeting extends StatelessWebexPlugin {
7094
7200
  }
7095
7201
  }
7096
7202
 
7203
+ /**
7204
+ * Cleans up stats analyzer, peer connection and other things before
7205
+ * we can create a new transcoded media connection
7206
+ *
7207
+ * @private
7208
+ * @returns {Promise<void>}
7209
+ */
7210
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7211
+ if (this.statsAnalyzer) {
7212
+ await this.statsAnalyzer.stopAnalyzer();
7213
+ }
7214
+ this.statsAnalyzer = null;
7215
+
7216
+ this.isMultistream = false;
7217
+
7218
+ if (this.mediaProperties.webrtcMediaConnection) {
7219
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7220
+ this.closePeerConnections(false);
7221
+
7222
+ this.mediaProperties.unsetPeerConnection();
7223
+ }
7224
+
7225
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7226
+
7227
+ this.createStatsAnalyzer();
7228
+ }
7229
+
7097
7230
  /**
7098
7231
  * Sends stats report, closes peer connection and cleans up any media connection
7099
7232
  * related things before trying to establish media connection again with turn server
@@ -7288,19 +7421,33 @@ export default class Meeting extends StatelessWebexPlugin {
7288
7421
 
7289
7422
  this.createStatsAnalyzer();
7290
7423
 
7291
- await this.establishMediaConnection(
7292
- remoteMediaManagerConfig,
7293
- bundlePolicy,
7294
- forceTurnDiscovery,
7295
- turnServerInfo
7296
- );
7424
+ try {
7425
+ await this.establishMediaConnection(
7426
+ remoteMediaManagerConfig,
7427
+ bundlePolicy,
7428
+ forceTurnDiscovery,
7429
+ turnServerInfo
7430
+ );
7431
+ } catch (error) {
7432
+ if (error instanceof MultistreamNotSupportedError) {
7433
+ LoggerProxy.logger.warn(
7434
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7435
+ );
7297
7436
 
7298
- if (audioEnabled || videoEnabled) {
7299
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7300
- } else {
7301
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7302
- }
7437
+ await this.downgradeFromMultistreamToTranscoded();
7303
7438
 
7439
+ // Establish new media connection with forced TURN discovery
7440
+ // 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
7441
+ await this.establishMediaConnection(
7442
+ remoteMediaManagerConfig,
7443
+ bundlePolicy,
7444
+ true,
7445
+ undefined
7446
+ );
7447
+ } else {
7448
+ throw error;
7449
+ }
7450
+ }
7304
7451
  if (this.mediaProperties.hasLocalShareStream()) {
7305
7452
  await this.enqueueScreenShareFloorRequest();
7306
7453
  }
@@ -8270,7 +8417,7 @@ export default class Meeting extends StatelessWebexPlugin {
8270
8417
  if (layoutType) {
8271
8418
  if (!LAYOUT_TYPES.includes(layoutType)) {
8272
8419
  return this.rejectWithErrorLog(
8273
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8420
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8274
8421
  );
8275
8422
  }
8276
8423
 
@@ -8628,6 +8775,11 @@ export default class Meeting extends StatelessWebexPlugin {
8628
8775
  this.stopTranscription();
8629
8776
  this.transcription = undefined;
8630
8777
  }
8778
+
8779
+ this.annotation.deregisterEvents();
8780
+
8781
+ // @ts-ignore - fix types
8782
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8631
8783
  };
8632
8784
 
8633
8785
  /**
@@ -8665,10 +8817,12 @@ export default class Meeting extends StatelessWebexPlugin {
8665
8817
 
8666
8818
  return;
8667
8819
  }
8668
- const {keepAliveUrl} = this.joinedWith;
8820
+
8669
8821
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8670
8822
 
8671
8823
  this.keepAliveTimerId = setInterval(() => {
8824
+ const {keepAliveUrl} = this.joinedWith;
8825
+
8672
8826
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8673
8827
  LoggerProxy.logger.warn(
8674
8828
  `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
  }
@@ -11,3 +11,10 @@ export type ToggleReactionsOptions = {
11
11
  locusUrl: string;
12
12
  requestingParticipantId: string;
13
13
  };
14
+
15
+ export type BrbOptions = {
16
+ enabled: boolean;
17
+ locusUrl: string;
18
+ deviceUrl: string;
19
+ selfId: string;
20
+ };