@webex/plugin-meetings 3.7.0-next.32 → 3.7.0-next.34

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 (36) 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/multistream-not-supported-error.js +53 -0
  6. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  7. package/dist/constants.js +5 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/interpretation/index.js +1 -1
  10. package/dist/interpretation/siLanguage.js +1 -1
  11. package/dist/meeting/index.js +256 -166
  12. package/dist/meeting/index.js.map +1 -1
  13. package/dist/meeting/locusMediaRequest.js +9 -0
  14. package/dist/meeting/locusMediaRequest.js.map +1 -1
  15. package/dist/meetings/util.js +1 -1
  16. package/dist/meetings/util.js.map +1 -1
  17. package/dist/roap/index.js +10 -8
  18. package/dist/roap/index.js.map +1 -1
  19. package/dist/types/annotation/index.d.ts +5 -0
  20. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  21. package/dist/types/constants.d.ts +5 -0
  22. package/dist/types/meeting/index.d.ts +11 -2
  23. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  24. package/dist/webinar/index.js +1 -1
  25. package/package.json +21 -21
  26. package/src/annotation/index.ts +16 -0
  27. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  28. package/src/constants.ts +5 -0
  29. package/src/meeting/index.ts +110 -27
  30. package/src/meeting/locusMediaRequest.ts +7 -0
  31. package/src/meetings/util.ts +2 -1
  32. package/src/roap/index.ts +10 -8
  33. package/test/unit/spec/annotation/index.ts +46 -1
  34. package/test/unit/spec/meeting/index.js +367 -21
  35. package/test/unit/spec/meetings/utils.js +10 -0
  36. package/test/unit/spec/roap/index.ts +47 -0
@@ -161,6 +161,7 @@ import {LocusMediaRequest} from './locusMediaRequest';
161
161
  import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
162
162
  import JoinWebinarError from '../common/errors/join-webinar-error';
163
163
  import Member from '../member';
164
+ import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
164
165
 
165
166
  // default callback so we don't call an undefined function, but in practice it should never be used
166
167
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -4627,11 +4628,12 @@ export default class Meeting extends StatelessWebexPlugin {
4627
4628
  * Close the peer connections and remove them from the class.
4628
4629
  * Cleanup any media connection related things.
4629
4630
  *
4631
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4630
4632
  * @returns {Promise}
4631
4633
  * @public
4632
4634
  * @memberof Meeting
4633
4635
  */
4634
- public closePeerConnections() {
4636
+ public closePeerConnections(resetMuteStates = true) {
4635
4637
  if (this.mediaProperties.webrtcMediaConnection) {
4636
4638
  if (this.remoteMediaManager) {
4637
4639
  this.remoteMediaManager.stop();
@@ -4648,8 +4650,10 @@ export default class Meeting extends StatelessWebexPlugin {
4648
4650
  this.setNetworkStatus(undefined);
4649
4651
  }
4650
4652
 
4651
- this.audio = null;
4652
- this.video = null;
4653
+ if (resetMuteStates) {
4654
+ this.audio = null;
4655
+ this.video = null;
4656
+ }
4653
4657
 
4654
4658
  return Promise.resolve();
4655
4659
  }
@@ -4909,7 +4913,7 @@ export default class Meeting extends StatelessWebexPlugin {
4909
4913
  * @param {Object} options - options to join with media
4910
4914
  * @param {JoinOptions} [options.joinOptions] - see #join()
4911
4915
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4912
- * @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}
4913
4917
  * @public
4914
4918
  * @memberof Meeting
4915
4919
  * @example
@@ -4999,6 +5003,7 @@ export default class Meeting extends StatelessWebexPlugin {
4999
5003
  return {
5000
5004
  join: joinResponse,
5001
5005
  media: mediaResponse,
5006
+ multistreamEnabled: this.isMultistream,
5002
5007
  };
5003
5008
  } catch (error) {
5004
5009
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -5007,7 +5012,17 @@ export default class Meeting extends StatelessWebexPlugin {
5007
5012
 
5008
5013
  this.roap.abortTurnDiscovery();
5009
5014
 
5010
- 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)) {
5011
5026
  try {
5012
5027
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
5013
5028
  } catch (e) {
@@ -5031,15 +5046,6 @@ export default class Meeting extends StatelessWebexPlugin {
5031
5046
  }
5032
5047
  );
5033
5048
 
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
5049
  if (shouldRetry) {
5044
5050
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
5045
5051
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5295,7 +5301,16 @@ export default class Meeting extends StatelessWebexPlugin {
5295
5301
  (this.config.receiveReactions || options.receiveReactions) &&
5296
5302
  this.isReactionsSupported()
5297
5303
  ) {
5298
- 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;
5299
5314
  const processedReaction: ProcessedReaction = {
5300
5315
  reaction: e.data.reaction,
5301
5316
  sender: {
@@ -5349,6 +5364,9 @@ export default class Meeting extends StatelessWebexPlugin {
5349
5364
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5350
5365
  );
5351
5366
 
5367
+ // @ts-ignore
5368
+ this.webex.internal.voicea.deregisterEvents();
5369
+
5352
5370
  this.areVoiceaEventsSetup = false;
5353
5371
  this.triggerStopReceivingTranscriptionEvent();
5354
5372
  }
@@ -6064,6 +6082,11 @@ export default class Meeting extends StatelessWebexPlugin {
6064
6082
  public roapMessageReceived = (roapMessage: RoapMessage) => {
6065
6083
  const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
6066
6084
 
6085
+ if (this.isMultistream && mediaServer !== 'homer') {
6086
+ throw new MultistreamNotSupportedError(
6087
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6088
+ );
6089
+ }
6067
6090
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
6068
6091
 
6069
6092
  if (mediaServer) {
@@ -6186,16 +6209,20 @@ export default class Meeting extends StatelessWebexPlugin {
6186
6209
  logText: `${LOG_HEADER} Roap Offer`,
6187
6210
  }
6188
6211
  ).catch((error) => {
6212
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6213
+
6189
6214
  // @ts-ignore
6190
6215
  this.webex.internal.newMetrics.submitClientEvent({
6191
6216
  name: 'client.media-engine.remote-sdp-received',
6192
6217
  payload: {
6193
- canProceed: false,
6218
+ canProceed: multistreamNotSupported,
6194
6219
  errors: [
6195
6220
  // @ts-ignore
6196
6221
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6197
6222
  {
6198
- 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,
6199
6226
  }
6200
6227
  ),
6201
6228
  ],
@@ -6203,7 +6230,7 @@ export default class Meeting extends StatelessWebexPlugin {
6203
6230
  options: {meetingId: this.id, rawError: error},
6204
6231
  });
6205
6232
 
6206
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6233
+ this.deferSDPAnswer.reject(error);
6207
6234
  clearTimeout(this.sdpResponseTimer);
6208
6235
  this.sdpResponseTimer = undefined;
6209
6236
  });
@@ -7093,7 +7120,9 @@ export default class Meeting extends StatelessWebexPlugin {
7093
7120
 
7094
7121
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
7095
7122
 
7096
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7123
+ LoggerProxy.logger.info(
7124
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7125
+ );
7097
7126
 
7098
7127
  if (this.isMultistream) {
7099
7128
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7171,6 +7200,33 @@ export default class Meeting extends StatelessWebexPlugin {
7171
7200
  }
7172
7201
  }
7173
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
+
7174
7230
  /**
7175
7231
  * Sends stats report, closes peer connection and cleans up any media connection
7176
7232
  * related things before trying to establish media connection again with turn server
@@ -7365,13 +7421,33 @@ export default class Meeting extends StatelessWebexPlugin {
7365
7421
 
7366
7422
  this.createStatsAnalyzer();
7367
7423
 
7368
- await this.establishMediaConnection(
7369
- remoteMediaManagerConfig,
7370
- bundlePolicy,
7371
- forceTurnDiscovery,
7372
- turnServerInfo
7373
- );
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
+ );
7436
+
7437
+ await this.downgradeFromMultistreamToTranscoded();
7374
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
+ }
7375
7451
  if (this.mediaProperties.hasLocalShareStream()) {
7376
7452
  await this.enqueueScreenShareFloorRequest();
7377
7453
  }
@@ -8341,7 +8417,7 @@ export default class Meeting extends StatelessWebexPlugin {
8341
8417
  if (layoutType) {
8342
8418
  if (!LAYOUT_TYPES.includes(layoutType)) {
8343
8419
  return this.rejectWithErrorLog(
8344
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8420
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8345
8421
  );
8346
8422
  }
8347
8423
 
@@ -8699,6 +8775,11 @@ export default class Meeting extends StatelessWebexPlugin {
8699
8775
  this.stopTranscription();
8700
8776
  this.transcription = undefined;
8701
8777
  }
8778
+
8779
+ this.annotation.deregisterEvents();
8780
+
8781
+ // @ts-ignore - fix types
8782
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8702
8783
  };
8703
8784
 
8704
8785
  /**
@@ -8736,10 +8817,12 @@ export default class Meeting extends StatelessWebexPlugin {
8736
8817
 
8737
8818
  return;
8738
8819
  }
8739
- const {keepAliveUrl} = this.joinedWith;
8820
+
8740
8821
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8741
8822
 
8742
8823
  this.keepAliveTimerId = setInterval(() => {
8824
+ const {keepAliveUrl} = this.joinedWith;
8825
+
8743
8826
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8744
8827
  LoggerProxy.logger.warn(
8745
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
  }
@@ -90,7 +90,8 @@ MeetingsUtil.getMediaServer = (sdp) => {
90
90
  .find((line) => line.startsWith('o='))
91
91
  .split(' ')
92
92
  .shift()
93
- .replace('o=', '');
93
+ .replace('o=', '')
94
+ .toLowerCase();
94
95
  } catch {
95
96
  mediaServer = undefined;
96
97
  }
package/src/roap/index.ts CHANGED
@@ -231,14 +231,16 @@ export default class Roap extends StatelessWebexPlugin {
231
231
  headers,
232
232
  } = remoteSdp.roapMessage;
233
233
 
234
- roapAnswer = {
235
- seq: answerSeq,
236
- messageType,
237
- sdp: sdps[0],
238
- errorType,
239
- errorCause,
240
- headers,
241
- };
234
+ if (messageType === ROAP.ROAP_TYPES.ANSWER) {
235
+ roapAnswer = {
236
+ seq: answerSeq,
237
+ messageType,
238
+ sdp: sdps[0],
239
+ errorType,
240
+ errorCause,
241
+ headers,
242
+ };
243
+ }
242
244
  }
243
245
  }
244
246
 
@@ -413,6 +413,51 @@ describe('live-annotation', () => {
413
413
  });
414
414
  });
415
415
  });
416
- });
417
416
 
417
+ describe('#deregisterEvents', () => {
418
+ let llmOn;
419
+ let llmOff;
420
+ let mercuryOn;
421
+ let mercuryOff;
422
+
423
+ beforeEach(() => {
424
+ llmOn = sinon.spy(webex.internal.llm, 'on');
425
+ llmOff = sinon.spy(webex.internal.llm, 'off');
426
+ mercuryOn = sinon.spy(webex.internal.mercury, 'on');
427
+ mercuryOff = sinon.spy(webex.internal.mercury, 'off');
428
+ });
429
+
430
+ it('cleans up events', () => {
431
+ annotationService.locusUrlUpdate(locusUrl);
432
+ assert.calledWith(
433
+ mercuryOn,
434
+ 'event:locus.approval_request',
435
+ annotationService.eventCommandProcessor,
436
+ annotationService
437
+ );
438
+ assert.calledWith(
439
+ llmOn,
440
+ 'event:relay.event',
441
+ annotationService.eventDataProcessor,
442
+ annotationService
443
+ );
444
+ assert.match(annotationService.hasSubscribedToEvents, true);
445
+
446
+ annotationService.deregisterEvents();
447
+ assert.calledWith(llmOff, 'event:relay.event', annotationService.eventDataProcessor);
448
+ assert.calledWith(
449
+ mercuryOff,
450
+ 'event:locus.approval_request',
451
+ annotationService.eventCommandProcessor
452
+ );
453
+ assert.match(annotationService.hasSubscribedToEvents, false);
454
+ });
455
+
456
+ it('does not call llm off if events have not been registered', () => {
457
+ annotationService.deregisterEvents();
458
+ assert.notCalled(llmOff);
459
+ assert.notCalled(mercuryOff);
460
+ });
461
+ });
462
+ });
418
463
  });