@webex/plugin-meetings 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/constants.d.ts +5 -4
  7. package/dist/constants.js +8 -4
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/interpretation/index.js +16 -2
  13. package/dist/interpretation/index.js.map +1 -1
  14. package/dist/interpretation/siLanguage.js +1 -1
  15. package/dist/locus-info/mediaSharesUtils.js +15 -1
  16. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  17. package/dist/locus-info/selfUtils.js +5 -0
  18. package/dist/locus-info/selfUtils.js.map +1 -1
  19. package/dist/media/MediaConnectionAwaiter.d.ts +61 -0
  20. package/dist/media/MediaConnectionAwaiter.js +163 -0
  21. package/dist/media/MediaConnectionAwaiter.js.map +1 -0
  22. package/dist/media/index.js +4 -1
  23. package/dist/media/index.js.map +1 -1
  24. package/dist/media/properties.js +4 -24
  25. package/dist/media/properties.js.map +1 -1
  26. package/dist/meeting/index.d.ts +26 -7
  27. package/dist/meeting/index.js +893 -677
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/muteState.d.ts +2 -8
  30. package/dist/meeting/muteState.js +37 -25
  31. package/dist/meeting/muteState.js.map +1 -1
  32. package/dist/meeting/request.d.ts +3 -0
  33. package/dist/meeting/request.js +32 -23
  34. package/dist/meeting/request.js.map +1 -1
  35. package/dist/meeting/util.js +1 -0
  36. package/dist/meeting/util.js.map +1 -1
  37. package/dist/meeting-info/utilv2.js +4 -1
  38. package/dist/meeting-info/utilv2.js.map +1 -1
  39. package/dist/meetings/index.d.ts +8 -0
  40. package/dist/meetings/index.js +20 -0
  41. package/dist/meetings/index.js.map +1 -1
  42. package/dist/multistream/mediaRequestManager.d.ts +2 -1
  43. package/dist/multistream/mediaRequestManager.js +1 -1
  44. package/dist/multistream/mediaRequestManager.js.map +1 -1
  45. package/dist/multistream/remoteMediaGroup.d.ts +2 -0
  46. package/dist/multistream/remoteMediaGroup.js +16 -2
  47. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  48. package/dist/multistream/remoteMediaManager.d.ts +15 -0
  49. package/dist/multistream/remoteMediaManager.js +179 -65
  50. package/dist/multistream/remoteMediaManager.js.map +1 -1
  51. package/dist/multistream/sendSlotManager.d.ts +9 -1
  52. package/dist/multistream/sendSlotManager.js +22 -0
  53. package/dist/multistream/sendSlotManager.js.map +1 -1
  54. package/dist/reachability/clusterReachability.d.ts +1 -0
  55. package/dist/reachability/clusterReachability.js +29 -15
  56. package/dist/reachability/clusterReachability.js.map +1 -1
  57. package/dist/reachability/index.d.ts +4 -0
  58. package/dist/reachability/index.js +18 -2
  59. package/dist/reachability/index.js.map +1 -1
  60. package/dist/reachability/request.js +12 -10
  61. package/dist/reachability/request.js.map +1 -1
  62. package/dist/reachability/util.d.ts +7 -0
  63. package/dist/reachability/util.js +19 -0
  64. package/dist/reachability/util.js.map +1 -1
  65. package/dist/reconnection-manager/index.js +2 -1
  66. package/dist/reconnection-manager/index.js.map +1 -1
  67. package/dist/roap/index.d.ts +10 -2
  68. package/dist/roap/index.js +15 -0
  69. package/dist/roap/index.js.map +1 -1
  70. package/dist/roap/request.js +3 -3
  71. package/dist/roap/request.js.map +1 -1
  72. package/dist/roap/turnDiscovery.d.ts +64 -17
  73. package/dist/roap/turnDiscovery.js +307 -126
  74. package/dist/roap/turnDiscovery.js.map +1 -1
  75. package/dist/statsAnalyzer/index.js +53 -30
  76. package/dist/statsAnalyzer/index.js.map +1 -1
  77. package/dist/webinar/index.js +1 -1
  78. package/package.json +22 -22
  79. package/src/config.ts +1 -0
  80. package/src/constants.ts +7 -3
  81. package/src/index.ts +1 -0
  82. package/src/interpretation/index.ts +18 -1
  83. package/src/locus-info/mediaSharesUtils.ts +16 -0
  84. package/src/locus-info/selfUtils.ts +5 -0
  85. package/src/media/MediaConnectionAwaiter.ts +174 -0
  86. package/src/media/index.ts +3 -1
  87. package/src/media/properties.ts +6 -31
  88. package/src/meeting/index.ts +321 -106
  89. package/src/meeting/muteState.ts +34 -20
  90. package/src/meeting/request.ts +18 -2
  91. package/src/meeting/util.ts +1 -0
  92. package/src/meeting-info/utilv2.ts +2 -1
  93. package/src/meetings/index.ts +18 -0
  94. package/src/multistream/mediaRequestManager.ts +4 -1
  95. package/src/multistream/remoteMediaGroup.ts +19 -0
  96. package/src/multistream/remoteMediaManager.ts +101 -16
  97. package/src/multistream/sendSlotManager.ts +28 -0
  98. package/src/reachability/clusterReachability.ts +20 -5
  99. package/src/reachability/index.ts +24 -1
  100. package/src/reachability/request.ts +15 -11
  101. package/src/reachability/util.ts +21 -0
  102. package/src/reconnection-manager/index.ts +1 -1
  103. package/src/roap/index.ts +25 -3
  104. package/src/roap/request.ts +3 -3
  105. package/src/roap/turnDiscovery.ts +244 -78
  106. package/src/statsAnalyzer/index.ts +63 -27
  107. package/test/integration/spec/journey.js +14 -14
  108. package/test/integration/spec/space-meeting.js +1 -1
  109. package/test/unit/spec/interpretation/index.ts +39 -3
  110. package/test/unit/spec/locus-info/index.js +28 -19
  111. package/test/unit/spec/locus-info/mediaSharesUtils.ts +9 -0
  112. package/test/unit/spec/locus-info/selfUtils.js +42 -12
  113. package/test/unit/spec/media/MediaConnectionAwaiter.ts +344 -0
  114. package/test/unit/spec/media/index.ts +89 -78
  115. package/test/unit/spec/media/properties.ts +16 -70
  116. package/test/unit/spec/meeting/index.js +638 -139
  117. package/test/unit/spec/meeting/muteState.js +219 -67
  118. package/test/unit/spec/meeting/request.js +21 -0
  119. package/test/unit/spec/meeting/utils.js +6 -1
  120. package/test/unit/spec/meeting-info/utilv2.js +6 -0
  121. package/test/unit/spec/meetings/index.js +40 -20
  122. package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
  123. package/test/unit/spec/multistream/remoteMediaGroup.ts +79 -1
  124. package/test/unit/spec/multistream/remoteMediaManager.ts +199 -1
  125. package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
  126. package/test/unit/spec/reachability/clusterReachability.ts +86 -22
  127. package/test/unit/spec/reachability/index.ts +197 -60
  128. package/test/unit/spec/reachability/request.js +15 -7
  129. package/test/unit/spec/reachability/util.ts +32 -2
  130. package/test/unit/spec/reconnection-manager/index.js +28 -0
  131. package/test/unit/spec/roap/index.ts +61 -6
  132. package/test/unit/spec/roap/turnDiscovery.ts +298 -16
  133. package/test/unit/spec/stats-analyzer/index.js +179 -0
  134. package/dist/member/member.types.d.ts +0 -11
  135. package/dist/member/member.types.js +0 -17
  136. package/dist/member/member.types.js.map +0 -1
  137. package/src/member/member.types.ts +0 -13
  138. /package/test/unit/spec/locus-info/{lib/selfConstant.js → selfConstant.js} +0 -0
@@ -150,15 +150,30 @@ export class MuteState {
150
150
  * @param {Boolean} [mute] true for muting, false for unmuting request
151
151
  * @returns {void}
152
152
  */
153
- public handleLocalStreamMuteStateChange(meeting?: object, mute?: boolean) {
153
+ public handleLocalStreamMuteStateChange(meeting?: any) {
154
154
  if (this.ignoreMuteStateChange) {
155
155
  return;
156
156
  }
157
+
158
+ // either user or system may have triggered a mute state change, but localMute should reflect both
159
+ let newMuteState: boolean;
160
+ let userMuteState: boolean;
161
+ let systemMuteState: boolean;
162
+ if (this.type === AUDIO) {
163
+ newMuteState = meeting.mediaProperties.audioStream?.muted;
164
+ userMuteState = meeting.mediaProperties.audioStream?.userMuted;
165
+ systemMuteState = meeting.mediaProperties.audioStream?.systemMuted;
166
+ } else {
167
+ newMuteState = meeting.mediaProperties.videoStream?.muted;
168
+ userMuteState = meeting.mediaProperties.videoStream?.userMuted;
169
+ systemMuteState = meeting.mediaProperties.videoStream?.systemMuted;
170
+ }
171
+
157
172
  LoggerProxy.logger.info(
158
- `Meeting:muteState#handleLocalStreamMuteStateChange --> ${this.type}: local stream new mute state: ${mute}`
173
+ `Meeting:muteState#handleLocalStreamMuteStateChange --> ${this.type}: local stream new mute state: ${newMuteState} (user mute: ${userMuteState}, system mute: ${systemMuteState})`
159
174
  );
160
175
 
161
- this.state.client.localMute = mute;
176
+ this.state.client.localMute = newMuteState;
162
177
 
163
178
  this.applyClientStateToServer(meeting);
164
179
  }
@@ -249,7 +264,12 @@ export class MuteState {
249
264
  `Meeting:muteState#applyClientStateToServer --> ${this.type}: error: ${e}`
250
265
  );
251
266
 
252
- this.applyServerMuteToLocalStream(meeting, 'clientRequestFailed');
267
+ // failed to apply client state to server, so revert stream mute state to server state
268
+ this.muteLocalStream(
269
+ meeting,
270
+ this.state.server.localMute || this.state.server.remoteMute,
271
+ 'clientRequestFailed'
272
+ );
253
273
  });
254
274
  }
255
275
 
@@ -325,18 +345,6 @@ export class MuteState {
325
345
  });
326
346
  }
327
347
 
328
- /** Sets the mute state of the local stream according to what server thinks is our state
329
- * @param {Object} meeting - the meeting object
330
- * @param {ServerMuteReason} serverMuteReason - reason why we're applying server mute to the local stream
331
- * @returns {void}
332
- */
333
- private applyServerMuteToLocalStream(meeting: any, serverMuteReason: ServerMuteReason) {
334
- const muted = this.state.server.localMute || this.state.server.remoteMute;
335
-
336
- // update the local stream mute state, but not this.state.client.localMute
337
- this.muteLocalStream(meeting, muted, serverMuteReason);
338
- }
339
-
340
348
  /** Applies the current value for unmute allowed to the underlying stream
341
349
  *
342
350
  * @param {Meeting} meeting
@@ -371,7 +379,7 @@ export class MuteState {
371
379
  }
372
380
  if (muted !== undefined) {
373
381
  this.state.server.remoteMute = muted;
374
- this.applyServerMuteToLocalStream(meeting, 'remotelyMuted');
382
+ this.muteLocalStream(meeting, muted, 'remotelyMuted');
375
383
  }
376
384
  }
377
385
 
@@ -383,7 +391,7 @@ export class MuteState {
383
391
  * @param {Object} [meeting] the meeting object
384
392
  * @returns {undefined}
385
393
  */
386
- public handleServerLocalUnmuteRequired(meeting?: object) {
394
+ public handleServerLocalUnmuteRequired(meeting?: any) {
387
395
  if (!this.state.client.enabled) {
388
396
  LoggerProxy.logger.warn(
389
397
  `Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received while ${this.type} is disabled -> local unmute will not result in ${this.type} being sent`
@@ -396,9 +404,15 @@ export class MuteState {
396
404
 
397
405
  // todo: I'm seeing "you can now unmute yourself " popup when this happens - but same thing happens on web.w.c so we can ignore for now
398
406
  this.state.server.remoteMute = false;
399
- this.state.client.localMute = false;
400
407
 
401
- this.applyClientStateLocally(meeting, 'localUnmuteRequired');
408
+ // change user mute state to false, but keep localMute true if overall mute state is still true
409
+ this.muteLocalStream(meeting, false, 'localUnmuteRequired');
410
+ if (this.type === AUDIO) {
411
+ this.state.client.localMute = meeting.mediaProperties.audioStream?.muted;
412
+ } else {
413
+ this.state.client.localMute = meeting.mediaProperties.videoStream?.muted;
414
+ }
415
+
402
416
  this.applyClientStateToServer(meeting);
403
417
  }
404
418
 
@@ -103,6 +103,7 @@ export default class MeetingRequest extends StatelessWebexPlugin {
103
103
  * @param {String} options.locale,
104
104
  * @param {Array} options.deviceCapabilities
105
105
  * @param {boolean} options.liveAnnotationSupported
106
+ * @param {String} options.alias
106
107
  * @returns {Promise}
107
108
  */
108
109
  async joinMeeting(options: {
@@ -122,11 +123,13 @@ export default class MeetingRequest extends StatelessWebexPlugin {
122
123
  meetingNumber: any;
123
124
  permissionToken: any;
124
125
  preferTranscoding: any;
126
+ reachability: any;
125
127
  breakoutsSupported: boolean;
126
128
  locale?: string;
127
129
  deviceCapabilities?: Array<string>;
128
130
  liveAnnotationSupported: boolean;
129
131
  ipVersion?: IP_VERSION;
132
+ alias?: string;
130
133
  }) {
131
134
  const {
132
135
  asResourceOccupant,
@@ -143,12 +146,14 @@ export default class MeetingRequest extends StatelessWebexPlugin {
143
146
  pin,
144
147
  moveToResource,
145
148
  roapMessage,
149
+ reachability,
146
150
  preferTranscoding,
147
151
  breakoutsSupported,
148
152
  locale,
149
153
  deviceCapabilities = [],
150
154
  liveAnnotationSupported,
151
155
  ipVersion,
156
+ alias,
152
157
  } = options;
153
158
 
154
159
  LoggerProxy.logger.info('Meeting:request#joinMeeting --> Joining a meeting', correlationId);
@@ -178,6 +183,10 @@ export default class MeetingRequest extends StatelessWebexPlugin {
178
183
  },
179
184
  };
180
185
 
186
+ if (alias) {
187
+ body.alias = alias;
188
+ }
189
+
181
190
  if (breakoutsSupported) {
182
191
  deviceCapabilities.push(BREAKOUTS.BREAKOUTS_SUPPORTED);
183
192
  }
@@ -260,8 +269,15 @@ export default class MeetingRequest extends StatelessWebexPlugin {
260
269
  };
261
270
  }
262
271
 
263
- if (roapMessage) {
264
- body.localMedias = roapMessage.localMedias;
272
+ if (roapMessage || reachability) {
273
+ body.localMedias = [
274
+ {
275
+ localSdp: JSON.stringify({
276
+ roapMessage,
277
+ reachability,
278
+ }),
279
+ },
280
+ ];
265
281
  }
266
282
 
267
283
  /// @ts-ignore
@@ -149,6 +149,7 @@ const MeetingUtil = {
149
149
  locusUrl: meeting.locusUrl,
150
150
  locusClusterUrl: meeting.meetingInfo?.locusClusterUrl,
151
151
  correlationId: meeting.correlationId,
152
+ reachability: options.reachability,
152
153
  roapMessage: options.roapMessage,
153
154
  permissionToken: meeting.permissionToken,
154
155
  resourceId: options.resourceId || null,
@@ -292,8 +292,9 @@ MeetingInfoUtil.getRequestBody = (options: {type: string; destination: object} |
292
292
  MeetingInfoUtil.getWebexSite = (uri: string) => {
293
293
  const exceptedDomains = ['meet.webex.com', 'meetup.webex.com', 'ciscospark.com'];
294
294
  const site = uri?.match(/.+@([^.]+\.[^.]+\.[^.]+)$/)?.[1];
295
+ const isExceptedDomain = !!site && exceptedDomains.some((domain) => site.includes(domain));
295
296
 
296
- return exceptedDomains.includes(site) ? null : site;
297
+ return isExceptedDomain ? null : site;
297
298
  };
298
299
 
299
300
  /**
@@ -717,6 +717,24 @@ export default class Meetings extends WebexPlugin {
717
717
  }
718
718
  }
719
719
 
720
+ /**
721
+ * API to toggle TLS reachability, needs to be called before webex.meetings.register()
722
+ * @param {Boolean} newValue
723
+ * @private
724
+ * @memberof Meetings
725
+ * @returns {undefined}
726
+ */
727
+ private _toggleTlsReachability(newValue: boolean) {
728
+ if (typeof newValue !== 'boolean') {
729
+ return;
730
+ }
731
+ // @ts-ignore
732
+ if (this.config.experimental.enableTlsReachability !== newValue) {
733
+ // @ts-ignore
734
+ this.config.experimental.enableTlsReachability = newValue;
735
+ }
736
+ }
737
+
720
738
  /**
721
739
  * Explicitly sets up the meetings plugin by registering
722
740
  * the device, connecting to mercury, and listening for locus events.
@@ -8,6 +8,7 @@ import {
8
8
  H264Codec,
9
9
  getRecommendedMaxBitrateForFrameSize,
10
10
  RecommendedOpusBitrates,
11
+ NamedMediaGroup,
11
12
  } from '@webex/internal-media-core';
12
13
  import {cloneDeepWith, debounce, isEmpty} from 'lodash';
13
14
 
@@ -22,6 +23,7 @@ export interface ActiveSpeakerPolicyInfo {
22
23
  crossPriorityDuplication: boolean;
23
24
  crossPolicyDuplication: boolean;
24
25
  preferLiveVideo: boolean;
26
+ namedMediaGroups?: NamedMediaGroup[];
25
27
  }
26
28
 
27
29
  export interface ReceiverSelectedPolicyInfo {
@@ -347,7 +349,8 @@ export class MediaRequestManager {
347
349
  mr.policyInfo.priority,
348
350
  mr.policyInfo.crossPriorityDuplication,
349
351
  mr.policyInfo.crossPolicyDuplication,
350
- mr.policyInfo.preferLiveVideo
352
+ mr.policyInfo.preferLiveVideo,
353
+ mr.policyInfo.namedMediaGroups
351
354
  )
352
355
  : new ReceiverSelectedInfo(mr.policyInfo.csi),
353
356
  mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
@@ -2,6 +2,7 @@
2
2
  /* eslint-disable require-jsdoc */
3
3
  /* eslint-disable import/prefer-default-export */
4
4
  import {forEach} from 'lodash';
5
+ import {NamedMediaGroup} from '@webex/internal-media-core';
5
6
  import LoggerProxy from '../common/logs/logger-proxy';
6
7
 
7
8
  import {getMaxFs, RemoteMedia, RemoteVideoResolution} from './remoteMedia';
@@ -11,6 +12,7 @@ import {CSI, ReceiveSlot} from './receiveSlot';
11
12
  type Options = {
12
13
  resolution?: RemoteVideoResolution; // applies only to groups of type MediaType.VideoMain and MediaType.VideoSlides
13
14
  preferLiveVideo?: boolean; // applies only to groups of type MediaType.VideoMain and MediaType.VideoSlides
15
+ namedMediaGroup?: NamedMediaGroup; // applies only to named media groups for audio
14
16
  };
15
17
 
16
18
  export class RemoteMediaGroup {
@@ -221,6 +223,9 @@ export class RemoteMediaGroup {
221
223
  crossPriorityDuplication: false,
222
224
  crossPolicyDuplication: false,
223
225
  preferLiveVideo: !!this.options?.preferLiveVideo,
226
+ namedMediaGroups: this.options.namedMediaGroup?.value
227
+ ? [this.options?.namedMediaGroup]
228
+ : undefined,
224
229
  },
225
230
  receiveSlots: this.unpinnedRemoteMedia.map((remoteMedia) =>
226
231
  remoteMedia.getUnderlyingReceiveSlot()
@@ -241,6 +246,20 @@ export class RemoteMediaGroup {
241
246
  }
242
247
  }
243
248
 
249
+ /**
250
+ * setNamedMediaGroup - sets named media group type and value
251
+ * @internal
252
+ */
253
+ public setNamedMediaGroup(namedMediaGroup: NamedMediaGroup, commit: boolean) {
254
+ if (
255
+ this.options.namedMediaGroup.value !== namedMediaGroup.value ||
256
+ this.options.namedMediaGroup.type !== namedMediaGroup.type
257
+ ) {
258
+ this.options.namedMediaGroup = namedMediaGroup;
259
+ this.sendActiveSpeakerMediaRequest(commit);
260
+ }
261
+ }
262
+
244
263
  /**
245
264
  * Invalidates the remote media group by clearing the references to the receive slots
246
265
  * used by all remote media from that group and cancelling all media requests.
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable valid-jsdoc */
2
2
  import {cloneDeep, forEach, remove} from 'lodash';
3
3
  import {EventMap} from 'typed-emitter';
4
- import {MediaType} from '@webex/internal-media-core';
4
+ import {MediaType, NamedMediaGroup} from '@webex/internal-media-core';
5
5
 
6
6
  import LoggerProxy from '../common/logs/logger-proxy';
7
7
  import EventsScope from '../common/events/events-scope';
@@ -11,6 +11,7 @@ import {ReceiveSlot, CSI} from './receiveSlot';
11
11
  import {ReceiveSlotManager} from './receiveSlotManager';
12
12
  import {RemoteMediaGroup} from './remoteMediaGroup';
13
13
  import {MediaRequestManager} from './mediaRequestManager';
14
+ import {NAMED_MEDIA_GROUP_TYPE_AUDIO} from '../constants';
14
15
 
15
16
  export type PaneSize = RemoteVideoResolution;
16
17
  export type LayoutId = string;
@@ -49,6 +50,7 @@ export interface Configuration {
49
50
 
50
51
  layouts: {[key: LayoutId]: VideoLayout}; // a map of all available layouts, a layout can be set via setLayout() method
51
52
  };
53
+ namedMediaGroup?: NamedMediaGroup;
52
54
  }
53
55
 
54
56
  /* Predefined layouts: */
@@ -173,6 +175,7 @@ export const DefaultConfiguration: Configuration = {
173
175
  export enum Event {
174
176
  // events for audio streams
175
177
  AudioCreated = 'AudioCreated',
178
+ InterpretationAudioCreated = 'InterpretationAudioCreated',
176
179
  ScreenShareAudioCreated = 'ScreenShareAudioCreated',
177
180
 
178
181
  // events for video streams
@@ -221,7 +224,10 @@ export class RemoteMediaManager extends EventsScope {
221
224
  private currentLayout?: VideoLayout;
222
225
 
223
226
  private slots: {
224
- audio: ReceiveSlot[];
227
+ audio: {
228
+ main: ReceiveSlot[];
229
+ si: ReceiveSlot;
230
+ };
225
231
  screenShare: {
226
232
  audio: ReceiveSlot[];
227
233
  video?: ReceiveSlot;
@@ -234,7 +240,10 @@ export class RemoteMediaManager extends EventsScope {
234
240
  };
235
241
 
236
242
  private media: {
237
- audio?: RemoteMediaGroup;
243
+ audio: {
244
+ main?: RemoteMediaGroup;
245
+ si?: RemoteMediaGroup;
246
+ };
238
247
  video: {
239
248
  activeSpeakerGroups: {
240
249
  [key: PaneGroupId]: RemoteMediaGroup;
@@ -277,7 +286,10 @@ export class RemoteMediaManager extends EventsScope {
277
286
  this.receiveSlotManager = receiveSlotManager;
278
287
  this.mediaRequestManagers = mediaRequestManagers;
279
288
  this.media = {
280
- audio: undefined,
289
+ audio: {
290
+ main: undefined,
291
+ si: undefined,
292
+ },
281
293
  video: {
282
294
  activeSpeakerGroups: {},
283
295
  memberPanes: {},
@@ -291,7 +303,10 @@ export class RemoteMediaManager extends EventsScope {
291
303
  this.checkConfigValidity();
292
304
 
293
305
  this.slots = {
294
- audio: [],
306
+ audio: {
307
+ main: [],
308
+ si: undefined,
309
+ },
295
310
  screenShare: {
296
311
  audio: [],
297
312
  video: undefined,
@@ -389,8 +404,11 @@ export class RemoteMediaManager extends EventsScope {
389
404
  });
390
405
 
391
406
  // release all audio receive slots
392
- this.slots.audio.forEach((slot) => this.receiveSlotManager.releaseSlot(slot));
393
- this.slots.audio.length = 0;
407
+ this.slots.audio.main.forEach((slot) => this.receiveSlotManager.releaseSlot(slot));
408
+ this.slots.audio.main.length = 0;
409
+ if (this.slots.audio.si) {
410
+ this.receiveSlotManager.releaseSlot(this.slots.audio.si);
411
+ }
394
412
 
395
413
  // release screen share slots
396
414
  this.slots.screenShare.audio.forEach((slot) => this.receiveSlotManager.releaseSlot(slot));
@@ -525,22 +543,54 @@ export class RemoteMediaManager extends EventsScope {
525
543
  this.mediaRequestManagers.video.commit();
526
544
  }
527
545
 
546
+ /**
547
+ * Sets which named media group need receiving
548
+ * @param {MediaType} mediaType of the stream
549
+ * @param {number} languageCode of the stream. If the languageId is 0, the named media group request will be canceled,
550
+ * and only receive the main audio stream.
551
+ * @returns {void}
552
+ */
553
+ public async setReceiveNamedMediaGroup(mediaType: MediaType, languageId: number) {
554
+ if (mediaType !== MediaType.AudioMain) {
555
+ throw new Error(`cannot set receive named media group which media type is ${mediaType}`);
556
+ }
557
+
558
+ const value = languageId;
559
+ if (value === this.config.namedMediaGroup?.value) {
560
+ return;
561
+ }
562
+
563
+ this.config.namedMediaGroup = {
564
+ type: NAMED_MEDIA_GROUP_TYPE_AUDIO,
565
+ value,
566
+ };
567
+
568
+ if (!this.media.audio.si) {
569
+ await this.createInterpretationAudioMedia(true);
570
+ } else {
571
+ this.media.audio.si.setNamedMediaGroup(this.config.namedMediaGroup, true);
572
+ }
573
+ }
574
+
528
575
  /**
529
576
  * Creates the audio slots
530
577
  */
531
578
  private async createAudioMedia() {
532
- // create the audio receive slots
579
+ // create si audio request
580
+ await this.createInterpretationAudioMedia(false);
581
+
582
+ // create main audio receive slots
533
583
  for (let i = 0; i < this.config.audio.numOfActiveSpeakerStreams; i += 1) {
534
584
  // eslint-disable-next-line no-await-in-loop
535
585
  const slot = await this.receiveSlotManager.allocateSlot(MediaType.AudioMain);
536
586
 
537
- this.slots.audio.push(slot);
587
+ this.slots.audio.main.push(slot);
538
588
  }
539
589
 
540
- // create a remote media group
541
- this.media.audio = new RemoteMediaGroup(
590
+ // create a remote media group for main audio
591
+ this.media.audio.main = new RemoteMediaGroup(
542
592
  this.mediaRequestManagers.audio,
543
- this.slots.audio,
593
+ this.slots.audio.main,
544
594
  255,
545
595
  true
546
596
  );
@@ -548,10 +598,40 @@ export class RemoteMediaManager extends EventsScope {
548
598
  this.emit(
549
599
  {file: 'multistream/remoteMediaManager', function: 'createAudioMedia'},
550
600
  Event.AudioCreated,
551
- this.media.audio
601
+ this.media.audio.main
552
602
  );
553
603
  }
554
604
 
605
+ /**
606
+ * Creates the audio slots for named media
607
+ */
608
+ private async createInterpretationAudioMedia(commitRequest: boolean) {
609
+ // create slot for interpretation language audio
610
+ if (
611
+ this.config.namedMediaGroup?.type === NAMED_MEDIA_GROUP_TYPE_AUDIO &&
612
+ this.config.namedMediaGroup?.value
613
+ ) {
614
+ this.slots.audio.si = await this.receiveSlotManager.allocateSlot(MediaType.AudioMain);
615
+
616
+ // create a remote media group for si audio
617
+ this.media.audio.si = new RemoteMediaGroup(
618
+ this.mediaRequestManagers.audio,
619
+ [this.slots.audio.si],
620
+ 255,
621
+ commitRequest,
622
+ {
623
+ namedMediaGroup: this.config.namedMediaGroup,
624
+ }
625
+ );
626
+
627
+ this.emit(
628
+ {file: 'multistream/remoteMediaManager', function: 'createInterpretationAudioMedia'},
629
+ Event.InterpretationAudioCreated,
630
+ this.media.audio.si
631
+ );
632
+ }
633
+ }
634
+
555
635
  /**
556
636
  * Creates receive slots required for receiving screen share audio and video
557
637
  */
@@ -748,7 +828,7 @@ export class RemoteMediaManager extends EventsScope {
748
828
  /** logs main audio slots */
749
829
  private logMainAudioReceiveSlots() {
750
830
  LoggerProxy.logger.log(
751
- `RemoteMediaManager#logMainAudioReceiveSlots --> MAIN AUDIO receive slots: ${this.slots.audio
831
+ `RemoteMediaManager#logMainAudioReceiveSlots --> MAIN AUDIO receive slots: ${this.slots.audio.main
752
832
  .map((slot) => slot.logString)
753
833
  .join(', ')}`
754
834
  );
@@ -924,8 +1004,13 @@ export class RemoteMediaManager extends EventsScope {
924
1004
  }) {
925
1005
  const {audio, video, screenShareAudio, screenShareVideo, commit} = options;
926
1006
 
927
- if (audio && this.media.audio) {
928
- this.media.audio.stop(commit);
1007
+ if (audio) {
1008
+ if (this.media.audio.main) {
1009
+ this.media.audio.main.stop(commit);
1010
+ }
1011
+ if (this.media.audio.si) {
1012
+ this.media.audio.si.stop(commit);
1013
+ }
929
1014
  }
930
1015
  if (video) {
931
1016
  Object.values(this.media.video.activeSpeakerGroups).forEach((remoteMediaGroup) => {
@@ -3,6 +3,7 @@ import {
3
3
  MediaType,
4
4
  LocalStream,
5
5
  MultistreamRoapMediaConnection,
6
+ NamedMediaGroup,
6
7
  } from '@webex/internal-media-core';
7
8
 
8
9
  export default class SendSlotManager {
@@ -55,6 +56,33 @@ export default class SendSlotManager {
55
56
  return slot;
56
57
  }
57
58
 
59
+ /**
60
+ * Allow users to specify 'namedMediaGroups' to indicate which named media group its audio should be sent to.
61
+ * @param {MediaType} mediaType MediaType of the sendSlot to which the audio stream needs to be send to the media group
62
+ * @param {[]}namedMediaGroups - Allow users to specify 'namedMediaGroups'.If the value of 'namedMediaGroups' is zero,
63
+ * named media group will be canceled and the audio stream will be sent to the floor.
64
+ * @returns {void}
65
+ */
66
+ public setNamedMediaGroups(mediaType: MediaType, namedMediaGroups: NamedMediaGroup[]) {
67
+ if (mediaType !== MediaType.AudioMain) {
68
+ throw new Error(
69
+ `sendSlotManager cannot set named media group which media type is ${mediaType}`
70
+ );
71
+ }
72
+
73
+ const slot = this.slots.get(mediaType);
74
+
75
+ if (!slot) {
76
+ throw new Error(`Slot for ${mediaType} does not exist`);
77
+ }
78
+
79
+ slot.setNamedMediaGroups(namedMediaGroups);
80
+
81
+ this.LoggerProxy.logger.info(
82
+ `SendSlotsManager->setNamedMediaGroups#set named media group ${namedMediaGroups}`
83
+ );
84
+ }
85
+
58
86
  /**
59
87
  * This method publishes the given stream to the sendSlot for the given mediaType
60
88
  * @param {MediaType} mediaType MediaType of the sendSlot to which a stream needs to be published (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
@@ -2,7 +2,7 @@ import {Defer} from '@webex/common';
2
2
 
3
3
  import LoggerProxy from '../common/logs/logger-proxy';
4
4
  import {ClusterNode} from './request';
5
- import {convertStunUrlToTurn} from './util';
5
+ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
6
6
 
7
7
  import {ICE_GATHERING_STATE, CONNECTION_STATE} from '../constants';
8
8
 
@@ -29,6 +29,7 @@ export type ClusterReachabilityResult = {
29
29
  export class ClusterReachability {
30
30
  private numUdpUrls: number;
31
31
  private numTcpUrls: number;
32
+ private numXTlsUrls: number;
32
33
  private result: ClusterReachabilityResult;
33
34
  private pc?: RTCPeerConnection;
34
35
  private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
@@ -46,6 +47,7 @@ export class ClusterReachability {
46
47
  this.isVideoMesh = clusterInfo.isVideoMesh;
47
48
  this.numUdpUrls = clusterInfo.udp.length;
48
49
  this.numTcpUrls = clusterInfo.tcp.length;
50
+ this.numXTlsUrls = clusterInfo.xtls.length;
49
51
 
50
52
  this.pc = this.createPeerConnection(clusterInfo);
51
53
 
@@ -94,8 +96,16 @@ export class ClusterReachability {
94
96
  };
95
97
  });
96
98
 
99
+ const turnTlsIceServers = cluster.xtls.map((urlString: string) => {
100
+ return {
101
+ username: 'webexturnreachuser',
102
+ credential: 'webexturnreachpwd',
103
+ urls: [convertStunUrlToTurnTls(urlString)],
104
+ };
105
+ });
106
+
97
107
  return {
98
- iceServers: [...udpIceServers, ...tcpIceServers],
108
+ iceServers: [...udpIceServers, ...tcpIceServers, ...turnTlsIceServers],
99
109
  iceCandidatePoolSize: 0,
100
110
  iceTransportPolicy: 'all',
101
111
  };
@@ -194,7 +204,7 @@ export class ClusterReachability {
194
204
  * @returns {boolean} true if we have all results, false otherwise
195
205
  */
196
206
  private haveWeGotAllResults(): boolean {
197
- return ['udp', 'tcp'].every(
207
+ return ['udp', 'tcp', 'xtls'].every(
198
208
  (protocol) =>
199
209
  this.result[protocol].result === 'reachable' || this.result[protocol].result === 'untested'
200
210
  );
@@ -207,7 +217,7 @@ export class ClusterReachability {
207
217
  * @param {number} latency
208
218
  * @returns {void}
209
219
  */
210
- private storeLatencyResult(protocol: 'udp' | 'tcp', latency: number) {
220
+ private storeLatencyResult(protocol: 'udp' | 'tcp' | 'xtls', latency: number) {
211
221
  const result = this.result[protocol];
212
222
 
213
223
  if (result.latencyInMilliseconds === undefined) {
@@ -227,6 +237,7 @@ export class ClusterReachability {
227
237
  */
228
238
  private registerIceCandidateListener() {
229
239
  this.pc.onicecandidate = (e) => {
240
+ const TURN_TLS_PORT = 443;
230
241
  const CANDIDATE_TYPES = {
231
242
  SERVER_REFLEXIVE: 'srflx',
232
243
  RELAY: 'relay',
@@ -239,7 +250,8 @@ export class ClusterReachability {
239
250
  }
240
251
 
241
252
  if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
242
- this.storeLatencyResult('tcp', this.getElapsedTime());
253
+ const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
254
+ this.storeLatencyResult(protocol, this.getElapsedTime());
243
255
  // we don't add public IP for TCP, because in the case of relay candidates
244
256
  // e.candidate.address is the TURN server address, not the client's public IP
245
257
  }
@@ -275,6 +287,9 @@ export class ClusterReachability {
275
287
  this.result.tcp = {
276
288
  result: this.numTcpUrls > 0 ? 'unreachable' : 'untested',
277
289
  };
290
+ this.result.xtls = {
291
+ result: this.numXTlsUrls > 0 ? 'unreachable' : 'untested',
292
+ };
278
293
 
279
294
  try {
280
295
  const offer = await this.pc.createOffer({offerToReceiveAudio: true});