@webex/plugin-meetings 3.0.0-beta.250 → 3.0.0-beta.252

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 (65) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +3 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.js +30 -24
  6. package/dist/index.js.map +1 -1
  7. package/dist/interpretation/index.js +1 -1
  8. package/dist/interpretation/siLanguage.js +1 -1
  9. package/dist/media/index.js +18 -19
  10. package/dist/media/index.js.map +1 -1
  11. package/dist/media/properties.js +52 -52
  12. package/dist/media/properties.js.map +1 -1
  13. package/dist/meeting/index.js +417 -372
  14. package/dist/meeting/index.js.map +1 -1
  15. package/dist/meeting/muteState.js +39 -38
  16. package/dist/meeting/muteState.js.map +1 -1
  17. package/dist/meeting/util.js +9 -9
  18. package/dist/meeting/util.js.map +1 -1
  19. package/dist/meeting-info/index.js +45 -23
  20. package/dist/meeting-info/index.js.map +1 -1
  21. package/dist/meeting-info/meeting-info-v2.js +22 -4
  22. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  23. package/dist/meetings/index.js +4 -2
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/multistream/sendSlotManager.js +233 -0
  26. package/dist/multistream/sendSlotManager.js.map +1 -0
  27. package/dist/reconnection-manager/index.js +10 -10
  28. package/dist/reconnection-manager/index.js.map +1 -1
  29. package/dist/types/constants.d.ts +2 -0
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/types/media/index.d.ts +2 -2
  32. package/dist/types/media/properties.d.ts +24 -24
  33. package/dist/types/meeting/index.d.ts +61 -51
  34. package/dist/types/meeting/muteState.d.ts +16 -16
  35. package/dist/types/meeting/util.d.ts +2 -2
  36. package/dist/types/meeting-info/index.d.ts +7 -0
  37. package/dist/types/meeting-info/meeting-info-v2.d.ts +1 -0
  38. package/dist/types/multistream/sendSlotManager.d.ts +61 -0
  39. package/dist/types/reconnection-manager/index.d.ts +2 -2
  40. package/package.json +20 -20
  41. package/src/constants.ts +2 -0
  42. package/src/index.ts +14 -13
  43. package/src/media/index.ts +32 -34
  44. package/src/media/properties.ts +47 -46
  45. package/src/meeting/index.ts +402 -331
  46. package/src/meeting/muteState.ts +35 -34
  47. package/src/meeting/util.ts +11 -10
  48. package/src/meeting-info/index.ts +45 -20
  49. package/src/meeting-info/meeting-info-v2.ts +25 -5
  50. package/src/meetings/index.ts +9 -2
  51. package/src/multistream/sendSlotManager.ts +170 -0
  52. package/src/reconnection-manager/index.ts +8 -8
  53. package/test/integration/spec/converged-space-meetings.js +7 -7
  54. package/test/integration/spec/journey.js +85 -103
  55. package/test/integration/spec/space-meeting.js +9 -9
  56. package/test/unit/spec/media/index.ts +23 -66
  57. package/test/unit/spec/meeting/index.js +786 -848
  58. package/test/unit/spec/meeting/muteState.js +113 -75
  59. package/test/unit/spec/meeting/utils.js +14 -16
  60. package/test/unit/spec/meeting-info/index.js +173 -61
  61. package/test/unit/spec/meeting-info/meetinginfov2.js +186 -52
  62. package/test/unit/spec/meetings/index.js +6 -5
  63. package/test/unit/spec/multistream/sendSlotManager.ts +242 -0
  64. package/test/unit/spec/reconnection-manager/index.js +4 -3
  65. package/test/utils/integrationTestUtils.js +4 -4
@@ -20,13 +20,14 @@ import {
20
20
 
21
21
  import {
22
22
  getDevices,
23
- LocalTrack,
24
- LocalCameraTrack,
25
- LocalDisplayTrack,
26
- LocalSystemAudioTrack,
27
- LocalMicrophoneTrack,
28
- LocalTrackEvents,
29
- TrackMuteEvent,
23
+ LocalStream,
24
+ LocalCameraStream,
25
+ LocalDisplayStream,
26
+ LocalSystemAudioStream,
27
+ LocalMicrophoneStream,
28
+ LocalStreamEventNames,
29
+ StreamEventNames,
30
+ RemoteStream,
30
31
  } from '@webex/media-helpers';
31
32
 
32
33
  import {
@@ -66,7 +67,6 @@ import {
66
67
  AUDIO,
67
68
  CONTENT,
68
69
  DISPLAY_HINTS,
69
- ENDED,
70
70
  EVENT_TRIGGERS,
71
71
  EVENT_TYPES,
72
72
  EVENTS,
@@ -105,6 +105,7 @@ import {
105
105
  } from '../meeting-info/meeting-info-v2';
106
106
  import BrowserDetection from '../common/browser-detection';
107
107
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
108
+ import SendSlotManager from '../multistream/sendSlotManager';
108
109
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
109
110
  import {
110
111
  Configuration as RemoteMediaManagerConfiguration,
@@ -146,20 +147,21 @@ const logRequest = (request: any, {logText = ''}) => {
146
147
  });
147
148
  };
148
149
 
149
- export type LocalTracks = {
150
- microphone?: LocalMicrophoneTrack;
151
- camera?: LocalCameraTrack;
150
+ export type LocalStreams = {
151
+ microphone?: LocalMicrophoneStream;
152
+ camera?: LocalCameraStream;
152
153
  screenShare?: {
153
- audio?: LocalSystemAudioTrack;
154
- video?: LocalDisplayTrack;
154
+ audio?: LocalSystemAudioStream;
155
+ video?: LocalDisplayStream;
155
156
  };
156
157
  };
157
158
 
158
159
  export type AddMediaOptions = {
159
- localTracks?: LocalTracks;
160
+ localStreams?: LocalStreams;
160
161
  audioEnabled?: boolean; // if not specified, default value true is used
161
162
  videoEnabled?: boolean; // if not specified, default value true is used
162
- receiveShare?: boolean; // if not specified, default value true is used
163
+ shareAudioEnabled?: boolean; // if not specified, default value true is used
164
+ shareVideoEnabled?: boolean; // if not specified, default value true is used
163
165
  remoteMediaManagerConfig?: RemoteMediaManagerConfiguration; // applies only to multistream meetings
164
166
  bundlePolicy?: BundlePolicy; // applies only to multistream meetings
165
167
  allowMediaInLobby?: boolean; // allows adding media when in the lobby
@@ -548,13 +550,14 @@ export default class Meeting extends StatelessWebexPlugin {
548
550
  resourceUrl: string;
549
551
  selfId: string;
550
552
  state: any;
551
- localAudioTrackMuteStateHandler: (event: TrackMuteEvent) => void;
552
- localVideoTrackMuteStateHandler: (event: TrackMuteEvent) => void;
553
- underlyingLocalTrackChangeHandler: () => void;
553
+ localAudioStreamMuteStateHandler: (muted: boolean) => void;
554
+ localVideoStreamMuteStateHandler: (muted: boolean) => void;
555
+ localOutputTrackChangeHandler: () => void;
554
556
  roles: any[];
555
557
  environment: string;
556
558
  namespace = MEETINGS;
557
559
  allowMediaInLobby: boolean;
560
+ private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
558
561
 
559
562
  /**
560
563
  * @param {Object} attrs
@@ -1226,19 +1229,19 @@ export default class Meeting extends StatelessWebexPlugin {
1226
1229
  */
1227
1230
  this.remoteMediaManager = null;
1228
1231
 
1229
- this.localAudioTrackMuteStateHandler = (event) => {
1230
- this.audio.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1232
+ this.localAudioStreamMuteStateHandler = (muted: boolean) => {
1233
+ this.audio.handleLocalStreamMuteStateChange(this, muted);
1231
1234
  };
1232
1235
 
1233
- this.localVideoTrackMuteStateHandler = (event) => {
1234
- this.video.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1236
+ this.localVideoStreamMuteStateHandler = (muted: boolean) => {
1237
+ this.video.handleLocalStreamMuteStateChange(this, muted);
1235
1238
  };
1236
1239
 
1237
- // The handling of underlying track changes should be done inside
1240
+ // The handling of output track changes should be done inside
1238
1241
  // @webex/internal-media-core, but for now we have to do it here, because
1239
1242
  // RoapMediaConnection has to use raw MediaStreamTracks in its API until
1240
- // the Calling SDK also moves to using webrtc-core tracks
1241
- this.underlyingLocalTrackChangeHandler = () => {
1243
+ // the Calling SDK also moves to using webrtc-core streams
1244
+ this.localOutputTrackChangeHandler = () => {
1242
1245
  if (!this.isMultistream) {
1243
1246
  this.updateTranscodedMediaConnection();
1244
1247
  }
@@ -1280,6 +1283,7 @@ export default class Meeting extends StatelessWebexPlugin {
1280
1283
  * @param {Object} options
1281
1284
  * @param {String} [options.password] optional
1282
1285
  * @param {String} [options.captchaCode] optional
1286
+ * @param {Boolean} [options.sendCAevents] optional - Whether to submit Call Analyzer events or not. Default: false.
1283
1287
  * @public
1284
1288
  * @memberof Meeting
1285
1289
  * @returns {Promise}
@@ -1288,10 +1292,12 @@ export default class Meeting extends StatelessWebexPlugin {
1288
1292
  password = null,
1289
1293
  captchaCode = null,
1290
1294
  extraParams = {},
1295
+ sendCAevents = false,
1291
1296
  }: {
1292
1297
  password?: string;
1293
1298
  captchaCode?: string;
1294
1299
  extraParams?: Record<string, any>;
1300
+ sendCAevents?: boolean;
1295
1301
  }) {
1296
1302
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1297
1303
  if (this.fetchMeetingInfoTimeoutId) {
@@ -1327,7 +1333,7 @@ export default class Meeting extends StatelessWebexPlugin {
1327
1333
  this.config.installedOrgID,
1328
1334
  this.locusId,
1329
1335
  extraParams,
1330
- {meetingId: this.id}
1336
+ {meetingId: this.id, sendCAevents}
1331
1337
  );
1332
1338
 
1333
1339
  this.parseMeetingInfo(info, this.destination);
@@ -1419,14 +1425,16 @@ export default class Meeting extends StatelessWebexPlugin {
1419
1425
  * password and captcha code were correct or not.
1420
1426
  * @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
1421
1427
  * @param {String} captchaCode - can be undefined if captcha was not required by the server
1428
+ * @param {Boolean} sendCAevents - whether Call Analyzer events should be sent when fetching meeting information
1422
1429
  * @public
1423
1430
  * @memberof Meeting
1424
1431
  * @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
1425
1432
  */
1426
- public verifyPassword(password: string, captchaCode: string) {
1433
+ public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
1427
1434
  return this.fetchMeetingInfo({
1428
1435
  password,
1429
1436
  captchaCode,
1437
+ sendCAevents,
1430
1438
  })
1431
1439
  .then(() => {
1432
1440
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
@@ -2272,9 +2280,9 @@ export default class Meeting extends StatelessWebexPlugin {
2272
2280
  this.mediaProperties.mediaDirection?.sendShare &&
2273
2281
  oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
2274
2282
  ) {
2275
- await this.unpublishTracks([
2276
- this.mediaProperties.shareVideoTrack,
2277
- this.mediaProperties.shareAudioTrack,
2283
+ await this.unpublishStreams([
2284
+ this.mediaProperties.shareVideoStream,
2285
+ this.mediaProperties.shareAudioStream,
2278
2286
  ]);
2279
2287
  }
2280
2288
  } finally {
@@ -2778,11 +2786,11 @@ export default class Meeting extends StatelessWebexPlugin {
2778
2786
 
2779
2787
  // TODO: Handle sharing and wireless sharing when meeting end
2780
2788
  if (this.wirelessShare) {
2781
- if (this.mediaProperties.shareVideoTrack) {
2782
- await this.setLocalShareVideoTrack(undefined);
2789
+ if (this.mediaProperties.shareVideoStream) {
2790
+ await this.setLocalShareVideoStream(undefined);
2783
2791
  }
2784
- if (this.mediaProperties.shareAudioTrack) {
2785
- await this.setLocalShareAudioTrack(undefined);
2792
+ if (this.mediaProperties.shareAudioStream) {
2793
+ await this.setLocalShareAudioStream(undefined);
2786
2794
  }
2787
2795
  }
2788
2796
  // when multiple WEB deviceType join with same user
@@ -3365,11 +3373,11 @@ export default class Meeting extends StatelessWebexPlugin {
3365
3373
  }
3366
3374
 
3367
3375
  /**
3368
- * Removes remote audio, video and share tracks from class instance's mediaProperties
3376
+ * Removes remote audio, video and share streams from class instance's mediaProperties
3369
3377
  * @returns {undefined}
3370
3378
  */
3371
- unsetRemoteTracks() {
3372
- this.mediaProperties.unsetRemoteTracks();
3379
+ unsetRemoteStreams() {
3380
+ this.mediaProperties.unsetRemoteStreams();
3373
3381
  }
3374
3382
 
3375
3383
  /**
@@ -3384,17 +3392,18 @@ export default class Meeting extends StatelessWebexPlugin {
3384
3392
  LoggerProxy.logger.warn(
3385
3393
  'Meeting:index#closeRemoteStream --> [DEPRECATION WARNING]: closeRemoteStream has been deprecated after v1.89.3'
3386
3394
  );
3387
- this.closeRemoteTracks();
3395
+ this.closeRemoteStreams();
3388
3396
  }
3389
3397
 
3390
3398
  /**
3391
- * Removes the remote tracks on the class instance and triggers an event
3399
+ * Removes the remote streams on the class instance and triggers an event
3392
3400
  * to developers
3393
3401
  * @returns {undefined}
3394
3402
  * @memberof Meeting
3395
3403
  */
3396
- closeRemoteTracks() {
3397
- const {remoteAudioTrack, remoteVideoTrack, remoteShare} = this.mediaProperties;
3404
+ closeRemoteStreams() {
3405
+ const {remoteAudioStream, remoteVideoStream, remoteShareStream, shareAudioStream} =
3406
+ this.mediaProperties;
3398
3407
 
3399
3408
  /**
3400
3409
  * Triggers an event to the developer
@@ -3408,7 +3417,7 @@ export default class Meeting extends StatelessWebexPlugin {
3408
3417
  this,
3409
3418
  {
3410
3419
  file: 'meeting/index',
3411
- function: 'closeRemoteTracks',
3420
+ function: 'closeRemoteStreams',
3412
3421
  },
3413
3422
  EVENT_TRIGGERS.MEDIA_STOPPED,
3414
3423
  {
@@ -3418,193 +3427,240 @@ export default class Meeting extends StatelessWebexPlugin {
3418
3427
  };
3419
3428
 
3420
3429
  /**
3421
- * Stops a media track and emits an event
3422
- * @param {MediaStreamTrack} track Media track to stop
3423
- * @param {string} type Media track type
3430
+ * Stops a media stream and emits an event
3431
+ * @param {RemoteStream} stream Media stream to stop
3432
+ * @param {string} type Media stream type
3424
3433
  * @returns {Promise}
3425
3434
  * @inner
3426
3435
  */
3427
3436
  // eslint-disable-next-line arrow-body-style
3428
- const stopTrack = (track: MediaStreamTrack, type: string) => {
3429
- return Media.stopTracks(track).then(() => {
3430
- const isTrackStopped = track && track.readyState === ENDED;
3431
- const isWrongReadyState = track && !isTrackStopped;
3432
-
3433
- if (isTrackStopped) {
3434
- triggerMediaStoppedEvent(type);
3435
- } else if (isWrongReadyState) {
3436
- LoggerProxy.logger.warn(
3437
- `Meeting:index#closeRemoteTracks --> Error: MediaStreamTrack.readyState is ${track.readyState} for ${type}`
3438
- );
3439
- }
3437
+ const stopStream = (stream: RemoteStream, type: string) => {
3438
+ return Media.stopStream(stream).then(() => {
3439
+ triggerMediaStoppedEvent(type);
3440
3440
  });
3441
3441
  };
3442
3442
 
3443
3443
  return Promise.all([
3444
- stopTrack(remoteAudioTrack, EVENT_TYPES.REMOTE_AUDIO),
3445
- stopTrack(remoteVideoTrack, EVENT_TYPES.REMOTE_VIDEO),
3446
- stopTrack(remoteShare, EVENT_TYPES.REMOTE_SHARE),
3444
+ stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
3445
+ stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
3446
+ stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
3447
+ stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
3447
3448
  ]);
3448
3449
  }
3449
3450
 
3450
3451
  /**
3451
- * Stores the reference to a new microphone track, sets up the required event listeners
3452
- * on it, cleans up previous track, etc.
3452
+ * Stores the reference to a new microphone stream, sets up the required event listeners
3453
+ * on it, cleans up previous stream, etc.
3453
3454
  *
3454
- * @param {LocalMicrophoneTrack | null} localTrack local microphone track
3455
+ * @param {LocalMicrophoneStream | null} localStream local microphone stream
3455
3456
  * @returns {Promise<void>}
3456
3457
  */
3457
- private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
3458
- const oldTrack = this.mediaProperties.audioTrack;
3458
+ private async setLocalAudioStream(localStream?: LocalMicrophoneStream) {
3459
+ const oldStream = this.mediaProperties.audioStream;
3459
3460
 
3460
- oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3461
- oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3461
+ oldStream?.off(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3462
+ oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3462
3463
 
3463
3464
  // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
3464
- this.mediaProperties.setLocalAudioTrack(localTrack);
3465
+ this.mediaProperties.setLocalAudioStream(localStream);
3465
3466
 
3466
- this.audio.handleLocalTrackChange(this);
3467
+ this.audio.handleLocalStreamChange(this);
3467
3468
 
3468
- localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3469
- localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3469
+ localStream?.on(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3470
+ localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3470
3471
 
3471
- if (!this.isMultistream || !localTrack) {
3472
- // for multistream WCME automatically un-publishes the old track when we publish a new one
3473
- await this.unpublishTrack(oldTrack);
3472
+ if (!this.isMultistream || !localStream) {
3473
+ // for multistream WCME automatically un-publishes the old stream when we publish a new one
3474
+ await this.unpublishStream(MediaType.AudioMain, oldStream);
3474
3475
  }
3475
- await this.publishTrack(this.mediaProperties.audioTrack);
3476
+ await this.publishStream(MediaType.AudioMain, this.mediaProperties.audioStream);
3476
3477
  }
3477
3478
 
3478
3479
  /**
3479
- * Stores the reference to a new camera track, sets up the required event listeners
3480
- * on it, cleans up previous track, etc.
3480
+ * Stores the reference to a new camera stream, sets up the required event listeners
3481
+ * on it, cleans up previous stream, etc.
3481
3482
  *
3482
- * @param {LocalCameraTrack | null} localTrack local camera track
3483
+ * @param {LocalCameraStream | null} localStream local camera stream
3483
3484
  * @returns {Promise<void>}
3484
3485
  */
3485
- private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
3486
- const oldTrack = this.mediaProperties.videoTrack;
3486
+ private async setLocalVideoStream(localStream?: LocalCameraStream) {
3487
+ const oldStream = this.mediaProperties.videoStream;
3487
3488
 
3488
- oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3489
- oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3489
+ oldStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3490
+ oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3490
3491
 
3491
3492
  // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
3492
- this.mediaProperties.setLocalVideoTrack(localTrack);
3493
+ this.mediaProperties.setLocalVideoStream(localStream);
3493
3494
 
3494
- this.video.handleLocalTrackChange(this);
3495
+ this.video.handleLocalStreamChange(this);
3495
3496
 
3496
- localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3497
- localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3497
+ localStream?.on(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3498
+ localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3498
3499
 
3499
- if (!this.isMultistream || !localTrack) {
3500
- // for multistream WCME automatically un-publishes the old track when we publish a new one
3501
- await this.unpublishTrack(oldTrack);
3500
+ if (!this.isMultistream || !localStream) {
3501
+ // for multistream WCME automatically un-publishes the old stream when we publish a new one
3502
+ await this.unpublishStream(MediaType.VideoMain, oldStream);
3502
3503
  }
3503
- await this.publishTrack(this.mediaProperties.videoTrack);
3504
+ await this.publishStream(MediaType.VideoMain, this.mediaProperties.videoStream);
3504
3505
  }
3505
3506
 
3506
3507
  /**
3507
- * Stores the reference to a new screen share video track, sets up the required event listeners
3508
- * on it, cleans up previous track, etc.
3508
+ * Stores the reference to a new screen share stream, sets up the required event listeners
3509
+ * on it, cleans up previous stream, etc.
3510
+ * It also sends the floor grant/release request.
3509
3511
  *
3510
- * @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
3512
+ * @param {LocalDisplayStream | undefined} localDisplayStream local display stream
3511
3513
  * @returns {Promise<void>}
3512
3514
  */
3513
- private async setLocalShareVideoTrack(localDisplayTrack?: LocalDisplayTrack) {
3514
- const oldTrack = this.mediaProperties.shareVideoTrack;
3515
+ private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3516
+ const oldStream = this.mediaProperties.shareVideoStream;
3515
3517
 
3516
- oldTrack?.off(LocalTrackEvents.Ended, this.handleShareVideoTrackEnded);
3517
- oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3518
+ oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3519
+ oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3518
3520
 
3519
- this.mediaProperties.setLocalShareVideoTrack(localDisplayTrack);
3521
+ this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3520
3522
 
3521
- localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareVideoTrackEnded);
3522
- localDisplayTrack?.on(
3523
- LocalTrackEvents.UnderlyingTrackChange,
3524
- this.underlyingLocalTrackChangeHandler
3523
+ localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3524
+ localDisplayStream?.on(
3525
+ LocalStreamEventNames.OutputTrackChange,
3526
+ this.localOutputTrackChangeHandler
3525
3527
  );
3526
3528
 
3527
- this.mediaProperties.mediaDirection.sendShare = this.mediaProperties.hasLocalShareTrack();
3529
+ this.mediaProperties.mediaDirection.sendShare = this.mediaProperties.hasLocalShareStream();
3528
3530
 
3529
- if (!this.isMultistream || !localDisplayTrack) {
3530
- // for multistream WCME automatically un-publishes the old track when we publish a new one
3531
- await this.unpublishTrack(oldTrack);
3531
+ if (!this.isMultistream || !localDisplayStream) {
3532
+ // for multistream WCME automatically un-publishes the old stream when we publish a new one
3533
+ await this.unpublishStream(MediaType.VideoSlides, oldStream);
3532
3534
  }
3533
- await this.publishTrack(this.mediaProperties.shareVideoTrack);
3535
+ await this.publishStream(MediaType.VideoSlides, this.mediaProperties.shareVideoStream);
3534
3536
  }
3535
3537
 
3536
3538
  /**
3537
- * Stores the reference to a new screen share audio track, sets up the required event listeners
3538
- * on it, cleans up previous track, etc.
3539
+ * Stores the reference to a new screen share audio stream, sets up the required event listeners
3540
+ * on it, cleans up previous stream, etc.
3539
3541
  *
3540
- * @param {LocalSystemAudioTrack | undefined} localSystemAudioTrack local system audio track
3542
+ * @param {LocalSystemAudioStream | undefined} localSystemAudioStream local system audio stream
3541
3543
  * @returns {Promise<void>}
3542
3544
  */
3543
- private async setLocalShareAudioTrack(localSystemAudioTrack?: LocalSystemAudioTrack) {
3544
- const oldTrack = this.mediaProperties.shareAudioTrack;
3545
+ private async setLocalShareAudioStream(localSystemAudioStream?: LocalSystemAudioStream) {
3546
+ const oldStream = this.mediaProperties.shareAudioStream;
3545
3547
 
3546
- oldTrack?.off(LocalTrackEvents.Ended, this.handleShareAudioTrackEnded);
3547
- oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3548
+ oldStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3549
+ oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3548
3550
 
3549
- this.mediaProperties.setLocalShareAudioTrack(localSystemAudioTrack);
3551
+ this.mediaProperties.setLocalShareAudioStream(localSystemAudioStream);
3550
3552
 
3551
- localSystemAudioTrack?.on(LocalTrackEvents.Ended, this.handleShareAudioTrackEnded);
3552
- localSystemAudioTrack?.on(
3553
- LocalTrackEvents.UnderlyingTrackChange,
3554
- this.underlyingLocalTrackChangeHandler
3553
+ localSystemAudioStream?.on(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3554
+ localSystemAudioStream?.on(
3555
+ LocalStreamEventNames.OutputTrackChange,
3556
+ this.localOutputTrackChangeHandler
3555
3557
  );
3556
3558
 
3557
- this.mediaProperties.mediaDirection.sendShare = this.mediaProperties.hasLocalShareTrack();
3559
+ this.mediaProperties.mediaDirection.sendShare = this.mediaProperties.hasLocalShareStream();
3558
3560
 
3559
- if (!this.isMultistream || !localSystemAudioTrack) {
3560
- await this.unpublishTrack(oldTrack);
3561
+ if (!this.isMultistream || !localSystemAudioStream) {
3562
+ // for multistream WCME automatically un-publishes the old stream when we publish a new one
3563
+ await this.unpublishStream(MediaType.AudioSlides, oldStream);
3561
3564
  }
3562
- await this.publishTrack(this.mediaProperties.shareAudioTrack);
3565
+ await this.publishStream(MediaType.AudioSlides, this.mediaProperties.shareAudioStream);
3563
3566
  }
3564
3567
 
3565
3568
  /**
3566
- * Removes references to local tracks. This function should be called
3569
+ * Handles the local audio stream publish state change event
3570
+ * @internal
3571
+ * @param {Object} options parameters functionName, isPublished, mediaType and stream needed to trigger event
3572
+ * @returns {undefined}
3573
+ */
3574
+ private emitPublishStateChangeEvent(options: {
3575
+ functionName: string;
3576
+ isPublished: boolean;
3577
+ mediaType: MediaType;
3578
+ stream: MediaStream;
3579
+ }) {
3580
+ const {functionName, isPublished, mediaType, stream} = options;
3581
+ Trigger.trigger(
3582
+ this,
3583
+ {
3584
+ file: 'meeting/index',
3585
+ function: functionName,
3586
+ },
3587
+ EVENT_TRIGGERS.MEETING_STREAM_PUBLISH_STATE_CHANGED,
3588
+ {
3589
+ isPublished,
3590
+ mediaType,
3591
+ stream,
3592
+ }
3593
+ );
3594
+ }
3595
+
3596
+ /**
3597
+ * Removes references to local streams. This function should be called
3567
3598
  * on cleanup when we leave the meeting etc.
3568
3599
  *
3569
3600
  * @internal
3570
3601
  * @returns {void}
3571
3602
  */
3572
- public cleanupLocalTracks() {
3573
- const {audioTrack, videoTrack, shareVideoTrack, shareAudioTrack} = this.mediaProperties;
3603
+ public cleanupLocalStreams() {
3604
+ const {audioStream, videoStream, shareAudioStream, shareVideoStream} = this.mediaProperties;
3574
3605
 
3575
- audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3576
- audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3606
+ audioStream?.off(StreamEventNames.MuteStateChange, this.localAudioStreamMuteStateHandler);
3607
+ audioStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3577
3608
 
3578
- videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3579
- videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3609
+ videoStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3610
+ videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3580
3611
 
3581
- shareVideoTrack?.off(LocalTrackEvents.Ended, this.handleShareVideoTrackEnded);
3582
- shareVideoTrack?.off(
3583
- LocalTrackEvents.UnderlyingTrackChange,
3584
- this.underlyingLocalTrackChangeHandler
3612
+ shareAudioStream?.off(StreamEventNames.MuteStateChange, this.handleShareAudioStreamEnded);
3613
+ shareAudioStream?.off(
3614
+ LocalStreamEventNames.OutputTrackChange,
3615
+ this.localOutputTrackChangeHandler
3585
3616
  );
3586
-
3587
- shareAudioTrack?.off(LocalTrackEvents.Ended, this.handleShareAudioTrackEnded);
3588
- shareAudioTrack?.off(
3589
- LocalTrackEvents.UnderlyingTrackChange,
3590
- this.underlyingLocalTrackChangeHandler
3617
+ shareVideoStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamEnded);
3618
+ shareVideoStream?.off(
3619
+ LocalStreamEventNames.OutputTrackChange,
3620
+ this.localOutputTrackChangeHandler
3591
3621
  );
3592
3622
 
3593
- this.mediaProperties.setLocalAudioTrack(undefined);
3594
- this.mediaProperties.setLocalVideoTrack(undefined);
3595
- this.mediaProperties.setLocalShareVideoTrack(undefined);
3596
- this.mediaProperties.setLocalShareAudioTrack(undefined);
3623
+ this.mediaProperties.setLocalAudioStream(undefined);
3624
+ this.mediaProperties.setLocalVideoStream(undefined);
3625
+ this.mediaProperties.setLocalShareAudioStream(undefined);
3626
+ this.mediaProperties.setLocalShareVideoStream(undefined);
3597
3627
 
3598
3628
  this.mediaProperties.mediaDirection.sendAudio = false;
3599
3629
  this.mediaProperties.mediaDirection.sendVideo = false;
3600
3630
  this.mediaProperties.mediaDirection.sendShare = false;
3601
3631
 
3602
- // WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
3603
- // (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
3604
- audioTrack?.setPublished(false);
3605
- videoTrack?.setPublished(false);
3606
- shareVideoTrack?.setPublished(false);
3607
- shareAudioTrack?.setPublished(false);
3632
+ if (audioStream) {
3633
+ this.emitPublishStateChangeEvent({
3634
+ functionName: 'cleanupLocalStreams',
3635
+ isPublished: false,
3636
+ mediaType: MediaType.AudioMain,
3637
+ stream: audioStream,
3638
+ });
3639
+ }
3640
+ if (videoStream) {
3641
+ this.emitPublishStateChangeEvent({
3642
+ functionName: 'cleanupLocalStreams',
3643
+ isPublished: false,
3644
+ mediaType: MediaType.VideoMain,
3645
+ stream: videoStream,
3646
+ });
3647
+ }
3648
+ if (shareVideoStream) {
3649
+ this.emitPublishStateChangeEvent({
3650
+ functionName: 'cleanupLocalStreams',
3651
+ isPublished: false,
3652
+ mediaType: MediaType.VideoSlides,
3653
+ stream: shareVideoStream,
3654
+ });
3655
+ }
3656
+ if (shareAudioStream) {
3657
+ this.emitPublishStateChangeEvent({
3658
+ functionName: 'cleanupLocalStreams',
3659
+ isPublished: false,
3660
+ mediaType: MediaType.AudioSlides,
3661
+ stream: shareAudioStream,
3662
+ });
3663
+ }
3608
3664
  }
3609
3665
 
3610
3666
  /**
@@ -3671,6 +3727,7 @@ export default class Meeting extends StatelessWebexPlugin {
3671
3727
 
3672
3728
  this.receiveSlotManager.reset();
3673
3729
  this.mediaProperties.webrtcMediaConnection.close();
3730
+ this.sendSlotManager.reset();
3674
3731
  }
3675
3732
 
3676
3733
  this.audio = null;
@@ -3744,7 +3801,7 @@ export default class Meeting extends StatelessWebexPlugin {
3744
3801
  this.audio
3745
3802
  .handleClientRequest(this, true)
3746
3803
  .then(() => {
3747
- MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
3804
+ MeetingUtil.handleAudioLogging(this.mediaProperties.audioStream);
3748
3805
  // @ts-ignore
3749
3806
  this.webex.internal.newMetrics.submitClientEvent({
3750
3807
  name: 'client.muted',
@@ -3794,7 +3851,7 @@ export default class Meeting extends StatelessWebexPlugin {
3794
3851
  this.audio
3795
3852
  .handleClientRequest(this, false)
3796
3853
  .then(() => {
3797
- MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
3854
+ MeetingUtil.handleAudioLogging(this.mediaProperties.audioStream);
3798
3855
  // @ts-ignore
3799
3856
  this.webex.internal.newMetrics.submitClientEvent({
3800
3857
  name: 'client.unmuted',
@@ -3843,7 +3900,7 @@ export default class Meeting extends StatelessWebexPlugin {
3843
3900
  this.video
3844
3901
  .handleClientRequest(this, true)
3845
3902
  .then(() => {
3846
- MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
3903
+ MeetingUtil.handleVideoLogging(this.mediaProperties.videoStream);
3847
3904
  // @ts-ignore
3848
3905
  this.webex.internal.newMetrics.submitClientEvent({
3849
3906
  name: 'client.muted',
@@ -3892,7 +3949,7 @@ export default class Meeting extends StatelessWebexPlugin {
3892
3949
  this.video
3893
3950
  .handleClientRequest(this, false)
3894
3951
  .then(() => {
3895
- MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
3952
+ MeetingUtil.handleVideoLogging(this.mediaProperties.videoStream);
3896
3953
  // @ts-ignore
3897
3954
  this.webex.internal.newMetrics.submitClientEvent({
3898
3955
  name: 'client.unmuted',
@@ -3928,7 +3985,7 @@ export default class Meeting extends StatelessWebexPlugin {
3928
3985
  * joinWithMedia({
3929
3986
  * joinOptions: {resourceId: 'resourceId' },
3930
3987
  * mediaOptions: {
3931
- * localTracks: { microphone: microphoneTrack, camera: cameraTrack }
3988
+ * localStreams: { microphone: microphoneStream, camera: cameraStream }
3932
3989
  * }
3933
3990
  * })
3934
3991
  */
@@ -4344,23 +4401,6 @@ export default class Meeting extends StatelessWebexPlugin {
4344
4401
  options: {meetingId: this.id},
4345
4402
  });
4346
4403
 
4347
- if (!isEmpty(this.meetingInfo)) {
4348
- // @ts-ignore
4349
- this.webex.internal.newMetrics.submitClientEvent({
4350
- name: 'client.meetinginfo.request',
4351
- options: {meetingId: this.id},
4352
- });
4353
-
4354
- // @ts-ignore
4355
- this.webex.internal.newMetrics.submitClientEvent({
4356
- name: 'client.meetinginfo.response',
4357
- payload: {
4358
- identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
4359
- },
4360
- options: {meetingId: this.id},
4361
- });
4362
- }
4363
-
4364
4404
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4365
4405
 
4366
4406
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -4700,7 +4740,7 @@ export default class Meeting extends StatelessWebexPlugin {
4700
4740
  });
4701
4741
 
4702
4742
  this.locusInfo.once(LOCUSINFO.EVENTS.SELF_OBSERVING, async () => {
4703
- // Clean up the camera , microphone track and re initiate it
4743
+ // Clean up the camera , microphone stream and re initiate it
4704
4744
 
4705
4745
  try {
4706
4746
  if (this.screenShareFloorState === ScreenShareFloorStatus.GRANTED) {
@@ -4717,7 +4757,7 @@ export default class Meeting extends StatelessWebexPlugin {
4717
4757
  },
4718
4758
  };
4719
4759
 
4720
- this.cleanupLocalTracks();
4760
+ this.cleanupLocalStreams();
4721
4761
 
4722
4762
  this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4723
4763
  this.mediaProperties.unsetRemoteMedia();
@@ -4999,46 +5039,47 @@ export default class Meeting extends StatelessWebexPlugin {
4999
5039
  )}`
5000
5040
  );
5001
5041
 
5002
- const mediaTrack = event.track;
5042
+ if (event.track) {
5043
+ const mediaTrack = event.track;
5044
+ const remoteStream = new RemoteStream(MediaUtil.createMediaStream([mediaTrack]));
5003
5045
 
5004
- // eslint-disable-next-line @typescript-eslint/no-shadow
5005
- let eventType;
5046
+ // eslint-disable-next-line @typescript-eslint/no-shadow
5047
+ let eventType;
5006
5048
 
5007
- switch (event.type) {
5008
- case RemoteTrackType.AUDIO:
5009
- eventType = EVENT_TYPES.REMOTE_AUDIO;
5010
- this.mediaProperties.setRemoteAudioTrack(event.track);
5011
- break;
5012
- case RemoteTrackType.VIDEO:
5013
- eventType = EVENT_TYPES.REMOTE_VIDEO;
5014
- this.mediaProperties.setRemoteVideoTrack(event.track);
5015
- break;
5016
- case RemoteTrackType.SCREENSHARE_VIDEO:
5017
- if (event.track) {
5049
+ switch (event.type) {
5050
+ case RemoteTrackType.AUDIO:
5051
+ eventType = EVENT_TYPES.REMOTE_AUDIO;
5052
+ this.mediaProperties.setRemoteAudioStream(remoteStream);
5053
+ break;
5054
+ case RemoteTrackType.VIDEO:
5055
+ eventType = EVENT_TYPES.REMOTE_VIDEO;
5056
+ this.mediaProperties.setRemoteVideoStream(remoteStream);
5057
+ break;
5058
+ case RemoteTrackType.SCREENSHARE_VIDEO:
5018
5059
  eventType = EVENT_TYPES.REMOTE_SHARE;
5019
- this.mediaProperties.setRemoteShare(event.track);
5060
+ this.mediaProperties.setRemoteShareStream(remoteStream);
5061
+ break;
5062
+ default: {
5063
+ LoggerProxy.logger.log(
5064
+ 'Meeting:index#setupMediaConnectionListeners --> unexpected track'
5065
+ );
5020
5066
  }
5021
- break;
5022
- default: {
5023
- LoggerProxy.logger.log(
5024
- 'Meeting:index#setupMediaConnectionListeners --> unexpected track'
5025
- );
5026
5067
  }
5027
- }
5028
5068
 
5029
- if (eventType && mediaTrack) {
5030
- Trigger.trigger(
5031
- this,
5032
- {
5033
- file: 'meeting/index',
5034
- function: 'setupRemoteTrackListener:Event.REMOTE_TRACK_ADDED',
5035
- },
5036
- EVENT_TRIGGERS.MEDIA_READY,
5037
- {
5038
- type: eventType,
5039
- stream: MediaUtil.createMediaStream([mediaTrack]),
5040
- }
5041
- );
5069
+ if (eventType && mediaTrack) {
5070
+ Trigger.trigger(
5071
+ this,
5072
+ {
5073
+ file: 'meeting/index',
5074
+ function: 'setupRemoteTrackListener:Event.REMOTE_TRACK_ADDED',
5075
+ },
5076
+ EVENT_TRIGGERS.MEDIA_READY,
5077
+ {
5078
+ type: eventType,
5079
+ stream: remoteStream.outputStream,
5080
+ }
5081
+ );
5082
+ }
5042
5083
  }
5043
5084
  });
5044
5085
 
@@ -5127,7 +5168,7 @@ export default class Meeting extends StatelessWebexPlugin {
5127
5168
  }
5128
5169
  });
5129
5170
 
5130
- this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
5171
+ this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (csis) => {
5131
5172
  Trigger.trigger(
5132
5173
  this,
5133
5174
  {
@@ -5136,8 +5177,7 @@ export default class Meeting extends StatelessWebexPlugin {
5136
5177
  },
5137
5178
  EVENT_TRIGGERS.ACTIVE_SPEAKER_CHANGED,
5138
5179
  {
5139
- seqNum: msg.seqNum,
5140
- memberIds: msg.csis
5180
+ memberIds: csis
5141
5181
  // @ts-ignore
5142
5182
  .map((csi) => this.members.findMemberByCsi(csi)?.id)
5143
5183
  .filter((item) => item !== undefined),
@@ -5281,10 +5321,11 @@ export default class Meeting extends StatelessWebexPlugin {
5281
5321
  }
5282
5322
 
5283
5323
  /**
5284
- * Creates a webrtc media connection and publishes tracks to it
5324
+ * Creates a webrtc media connection and publishes streams to it
5285
5325
  *
5286
5326
  * @param {Object} turnServerInfo TURN server information
5287
5327
  * @param {BundlePolicy} [bundlePolicy] Bundle policy settings
5328
+ * @param {AddMediaOptions} [options] Options for enabling/disabling audio/video
5288
5329
  * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
5289
5330
  */
5290
5331
  private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
@@ -5310,18 +5351,34 @@ export default class Meeting extends StatelessWebexPlugin {
5310
5351
  this.mediaProperties.setMediaPeerConnection(mc);
5311
5352
  this.setupMediaConnectionListeners();
5312
5353
 
5313
- // publish the tracks
5314
- if (this.mediaProperties.audioTrack) {
5315
- await this.publishTrack(this.mediaProperties.audioTrack);
5354
+ if (this.isMultistream) {
5355
+ const [audioEnabled, videoEnabled, shareEnabled] = [
5356
+ this.mediaProperties.mediaDirection.sendAudio ||
5357
+ this.mediaProperties.mediaDirection.receiveAudio,
5358
+ this.mediaProperties.mediaDirection.sendVideo ||
5359
+ this.mediaProperties.mediaDirection.receiveVideo,
5360
+ this.mediaProperties.mediaDirection.sendShare ||
5361
+ this.mediaProperties.mediaDirection.receiveShare,
5362
+ ];
5363
+
5364
+ this.sendSlotManager.createSlot(mc, MediaType.VideoMain, audioEnabled);
5365
+ this.sendSlotManager.createSlot(mc, MediaType.AudioMain, videoEnabled);
5366
+ this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
5367
+ this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
5316
5368
  }
5317
- if (this.mediaProperties.videoTrack) {
5318
- await this.publishTrack(this.mediaProperties.videoTrack);
5369
+
5370
+ // publish the streams
5371
+ if (this.mediaProperties.audioStream) {
5372
+ await this.publishStream(MediaType.AudioMain, this.mediaProperties.audioStream);
5319
5373
  }
5320
- if (this.mediaProperties.shareVideoTrack) {
5321
- await this.publishTrack(this.mediaProperties.shareVideoTrack);
5374
+ if (this.mediaProperties.videoStream) {
5375
+ await this.publishStream(MediaType.VideoMain, this.mediaProperties.videoStream);
5322
5376
  }
5323
- if (this.isMultistream && this.mediaProperties.shareAudioTrack) {
5324
- await this.publishTrack(this.mediaProperties.shareAudioTrack);
5377
+ if (this.mediaProperties.shareVideoStream) {
5378
+ await this.publishStream(MediaType.VideoSlides, this.mediaProperties.shareVideoStream);
5379
+ }
5380
+ if (this.isMultistream && this.mediaProperties.shareAudioStream) {
5381
+ await this.publishStream(MediaType.AudioSlides, this.mediaProperties.shareAudioStream);
5325
5382
  }
5326
5383
 
5327
5384
  return mc;
@@ -5375,10 +5432,11 @@ export default class Meeting extends StatelessWebexPlugin {
5375
5432
  }
5376
5433
 
5377
5434
  const {
5378
- localTracks,
5435
+ localStreams,
5379
5436
  audioEnabled = true,
5380
5437
  videoEnabled = true,
5381
- receiveShare = true,
5438
+ shareAudioEnabled = true,
5439
+ shareVideoEnabled = true,
5382
5440
  remoteMediaManagerConfig,
5383
5441
  bundlePolicy,
5384
5442
  allowMediaInLobby,
@@ -5416,7 +5474,7 @@ export default class Meeting extends StatelessWebexPlugin {
5416
5474
  options: {meetingId: this.id},
5417
5475
  });
5418
5476
 
5419
- // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
5477
+ // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any streams are published
5420
5478
  // to avoid doing an extra SDP exchange when they are published for the first time
5421
5479
  this.mediaProperties.setMediaDirection({
5422
5480
  sendAudio: audioEnabled,
@@ -5424,7 +5482,7 @@ export default class Meeting extends StatelessWebexPlugin {
5424
5482
  sendShare: false,
5425
5483
  receiveAudio: audioEnabled,
5426
5484
  receiveVideo: videoEnabled,
5427
- receiveShare,
5485
+ receiveShare: shareAudioEnabled || shareVideoEnabled,
5428
5486
  });
5429
5487
 
5430
5488
  this.locusMediaRequest = new LocusMediaRequest(
@@ -5451,19 +5509,19 @@ export default class Meeting extends StatelessWebexPlugin {
5451
5509
  this.video = createMuteState(VIDEO, this, videoEnabled);
5452
5510
  const promises = [];
5453
5511
 
5454
- // setup all the references to local tracks in this.mediaProperties before creating media connection
5512
+ // setup all the references to local streams in this.mediaProperties before creating media connection
5455
5513
  // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5456
- if (localTracks?.microphone) {
5457
- promises.push(this.setLocalAudioTrack(localTracks.microphone));
5514
+ if (localStreams?.microphone) {
5515
+ promises.push(this.setLocalAudioStream(localStreams.microphone));
5458
5516
  }
5459
- if (localTracks?.camera) {
5460
- promises.push(this.setLocalVideoTrack(localTracks.camera));
5517
+ if (localStreams?.camera) {
5518
+ promises.push(this.setLocalVideoStream(localStreams.camera));
5461
5519
  }
5462
- if (localTracks?.screenShare?.video) {
5463
- promises.push(this.setLocalShareVideoTrack(localTracks.screenShare.video));
5520
+ if (localStreams?.screenShare?.video) {
5521
+ promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5464
5522
  }
5465
- if (this.isMultistream && localTracks?.screenShare?.audio) {
5466
- promises.push(this.setLocalShareAudioTrack(localTracks.screenShare.audio));
5523
+ if (localStreams?.screenShare?.audio) {
5524
+ promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5467
5525
  }
5468
5526
 
5469
5527
  return Promise.all(promises)
@@ -5595,7 +5653,7 @@ export default class Meeting extends StatelessWebexPlugin {
5595
5653
  })
5596
5654
  )
5597
5655
  .then(() => {
5598
- if (this.mediaProperties.hasLocalShareTrack()) {
5656
+ if (this.mediaProperties.hasLocalShareStream()) {
5599
5657
  return this.enqueueScreenShareFloorRequest();
5600
5658
  }
5601
5659
 
@@ -5805,11 +5863,12 @@ export default class Meeting extends StatelessWebexPlugin {
5805
5863
  public async updateMedia(options: {
5806
5864
  audioEnabled?: boolean;
5807
5865
  videoEnabled?: boolean;
5808
- receiveShare?: boolean;
5866
+ shareAudioEnabled?: boolean;
5867
+ shareVideoEnabled?: boolean;
5809
5868
  }) {
5810
5869
  this.checkMediaConnection();
5811
5870
 
5812
- const {audioEnabled, videoEnabled, receiveShare} = options;
5871
+ const {audioEnabled, videoEnabled, shareAudioEnabled, shareVideoEnabled} = options;
5813
5872
 
5814
5873
  LoggerProxy.logger.log(
5815
5874
  `Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
@@ -5820,40 +5879,40 @@ export default class Meeting extends StatelessWebexPlugin {
5820
5879
  }
5821
5880
 
5822
5881
  if (this.isMultistream) {
5823
- if (videoEnabled !== undefined) {
5824
- throw new Error(
5825
- 'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
5826
- );
5827
- }
5828
-
5829
- if (receiveShare !== undefined) {
5882
+ if (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined) {
5830
5883
  throw new Error(
5831
- 'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5884
+ 'toggling shareAudioEnabled or shareVideoEnabled in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5832
5885
  );
5833
5886
  }
5887
+ } else if (shareAudioEnabled !== undefined) {
5888
+ throw new Error(
5889
+ 'toggling shareAudioEnabled in a transcoded meeting is not supported as of now'
5890
+ );
5834
5891
  }
5835
5892
 
5836
5893
  if (audioEnabled !== undefined) {
5837
5894
  this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
5838
5895
  this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
5839
5896
  this.audio.enable(this, audioEnabled);
5897
+ if (this.isMultistream) {
5898
+ this.sendSlotManager.setActive(MediaType.AudioMain, audioEnabled);
5899
+ }
5840
5900
  }
5841
5901
 
5842
5902
  if (videoEnabled !== undefined) {
5843
5903
  this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
5844
5904
  this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
5845
5905
  this.video.enable(this, videoEnabled);
5906
+ if (this.isMultistream) {
5907
+ this.sendSlotManager.setActive(MediaType.VideoMain, videoEnabled);
5908
+ }
5846
5909
  }
5847
5910
 
5848
- if (receiveShare !== undefined) {
5849
- this.mediaProperties.mediaDirection.receiveShare = receiveShare;
5911
+ if (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined) {
5912
+ this.mediaProperties.mediaDirection.receiveShare = !!(shareAudioEnabled || shareVideoEnabled);
5850
5913
  }
5851
5914
 
5852
- if (this.isMultistream) {
5853
- if (audioEnabled !== undefined) {
5854
- await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5855
- }
5856
- } else {
5915
+ if (!this.isMultistream) {
5857
5916
  await this.updateTranscodedMediaConnection();
5858
5917
  }
5859
5918
 
@@ -6188,15 +6247,13 @@ export default class Meeting extends StatelessWebexPlugin {
6188
6247
  */
6189
6248
  private requestScreenShareFloor() {
6190
6249
  if (
6191
- !this.mediaProperties.hasLocalShareTrack() ||
6250
+ !this.mediaProperties.hasLocalShareStream() ||
6192
6251
  !this.mediaProperties.mediaDirection.sendShare
6193
6252
  ) {
6194
6253
  LoggerProxy.logger.log(
6195
- `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareVideoTrack=${
6196
- this.mediaProperties.shareVideoTrack ? 'yes' : 'no'
6197
- }, shareAudioTrack=${this.mediaProperties.shareAudioTrack ? 'yes' : 'no'}, sendShare=${
6198
- this.mediaProperties.mediaDirection.sendShare
6199
- })`
6254
+ `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share stream anymore (shareStream=${
6255
+ this.mediaProperties.shareVideoStream ? 'yes' : 'no'
6256
+ }, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
6200
6257
  );
6201
6258
  this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
6202
6259
 
@@ -6496,12 +6553,12 @@ export default class Meeting extends StatelessWebexPlugin {
6496
6553
  } = {} as any
6497
6554
  ) {
6498
6555
  const {main, content} = renderInfo;
6499
- const {mediaDirection, remoteShare, remoteVideoTrack} = this.mediaProperties;
6556
+ const {mediaDirection, remoteShareStream, remoteVideoStream} = this.mediaProperties;
6500
6557
 
6501
6558
  const layoutInfo = cloneDeep(this.lastVideoLayoutInfo);
6502
6559
 
6503
6560
  // TODO: We need a real time value for Audio, Video and Share send indicator
6504
- if (mediaDirection.receiveVideo !== true || !remoteVideoTrack) {
6561
+ if (mediaDirection.receiveVideo !== true || !remoteVideoStream) {
6505
6562
  return this.rejectWithErrorLog(
6506
6563
  'Meeting:index#changeVideoLayout --> cannot change video layout, you are not recieving any video/share stream'
6507
6564
  );
@@ -6532,7 +6589,7 @@ export default class Meeting extends StatelessWebexPlugin {
6532
6589
  }
6533
6590
 
6534
6591
  if (content) {
6535
- if (this.mediaProperties.mediaDirection.receiveShare && remoteShare) {
6592
+ if (this.mediaProperties.mediaDirection.receiveShare && remoteShareStream) {
6536
6593
  const contentWidth = Math.round(content.width);
6537
6594
  const contentHeight = Math.round(content.height);
6538
6595
 
@@ -6630,23 +6687,22 @@ export default class Meeting extends StatelessWebexPlugin {
6630
6687
  * @memberof Meeting
6631
6688
  * @returns {undefined}
6632
6689
  */
6633
- private handleShareAudioTrackEnded = async () => {
6634
- // current share audio track has ended, but there might be an active
6635
- // share video track. we only leave from wireless share if share has
6636
- // completely ended, which means no share audio or video tracks active
6637
- if (this.wirelessShare && !this.mediaProperties.shareVideoTrack) {
6690
+ private handleShareAudioStreamEnded = async () => {
6691
+ // current share audio stream has ended, but there might be an active
6692
+ // share video stream. we only leave from wireless share if share has
6693
+ // completely ended, which means no share audio or video streams active
6694
+ if (this.wirelessShare && !this.mediaProperties.shareVideoStream) {
6638
6695
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
6639
6696
  } else {
6640
6697
  try {
6641
- await this.unpublishTracks([this.mediaProperties.shareAudioTrack]);
6698
+ await this.unpublishStreams([this.mediaProperties.shareAudioStream]);
6642
6699
  } catch (error) {
6643
6700
  LoggerProxy.logger.log(
6644
- 'Meeting:index#handleShareAudioTrackEnded --> Error stopping share: ',
6701
+ 'Meeting:index#handleShareAudioStreamEnded --> Error stopping share: ',
6645
6702
  error
6646
6703
  );
6647
6704
  }
6648
6705
  }
6649
- this.triggerStoppedSharing();
6650
6706
  };
6651
6707
 
6652
6708
  /**
@@ -6655,18 +6711,18 @@ export default class Meeting extends StatelessWebexPlugin {
6655
6711
  * @memberof Meeting
6656
6712
  * @returns {undefined}
6657
6713
  */
6658
- private handleShareVideoTrackEnded = async () => {
6659
- // current share video track has ended, but there might be an active
6660
- // share audio track. we only leave from wireless share if share has
6661
- // completely ended, which means no share audio or video tracks active
6662
- if (this.wirelessShare && !this.mediaProperties.shareAudioTrack) {
6714
+ private handleShareVideoStreamEnded = async () => {
6715
+ // current share video stream has ended, but there might be an active
6716
+ // share audio stream. we only leave from wireless share if share has
6717
+ // completely ended, which means no share audio or video streams active
6718
+ if (this.wirelessShare && !this.mediaProperties.shareAudioStream) {
6663
6719
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
6664
6720
  } else {
6665
6721
  try {
6666
- await this.unpublishTracks([this.mediaProperties.shareVideoTrack]);
6722
+ await this.unpublishStreams([this.mediaProperties.shareVideoStream]);
6667
6723
  } catch (error) {
6668
6724
  LoggerProxy.logger.log(
6669
- 'Meeting:index#handleShareVideoTrackEnded --> Error stopping share: ',
6725
+ 'Meeting:index#handleShareVideoStreamEnded --> Error stopping share: ',
6670
6726
  error
6671
6727
  );
6672
6728
  }
@@ -6681,12 +6737,12 @@ export default class Meeting extends StatelessWebexPlugin {
6681
6737
  * @memberof Meeting
6682
6738
  */
6683
6739
  private triggerStoppedSharing = () => {
6684
- if (!this.mediaProperties.hasLocalShareTrack()) {
6740
+ if (!this.mediaProperties.hasLocalShareStream()) {
6685
6741
  Trigger.trigger(
6686
6742
  this,
6687
6743
  {
6688
6744
  file: 'meeting/index',
6689
- function: 'handleShareTrackEnded',
6745
+ function: 'handleShareStreamEnded',
6690
6746
  },
6691
6747
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
6692
6748
  {
@@ -6729,11 +6785,11 @@ export default class Meeting extends StatelessWebexPlugin {
6729
6785
  * @returns {undefined}
6730
6786
  */
6731
6787
  private handleMediaLogging(mediaProperties: {
6732
- audioTrack?: LocalMicrophoneTrack;
6733
- videoTrack?: LocalCameraTrack;
6788
+ audioStream?: LocalMicrophoneStream;
6789
+ videoStream?: LocalCameraStream;
6734
6790
  }) {
6735
- MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
6736
- MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
6791
+ MeetingUtil.handleVideoLogging(mediaProperties.videoStream);
6792
+ MeetingUtil.handleAudioLogging(mediaProperties.audioStream);
6737
6793
  }
6738
6794
 
6739
6795
  /**
@@ -6973,7 +7029,7 @@ export default class Meeting extends StatelessWebexPlugin {
6973
7029
  }
6974
7030
 
6975
7031
  /**
6976
- * Method to enable or disable the 'Music mode' effect on audio track
7032
+ * Method to enable or disable the 'Music mode' effect on audio stream
6977
7033
  *
6978
7034
  * @param {boolean} shouldEnableMusicMode
6979
7035
  * @returns {Promise}
@@ -6986,12 +7042,12 @@ export default class Meeting extends StatelessWebexPlugin {
6986
7042
  }
6987
7043
 
6988
7044
  if (shouldEnableMusicMode) {
6989
- await this.mediaProperties.webrtcMediaConnection.setCodecParameters(MediaType.AudioMain, {
7045
+ await this.sendSlotManager.setCodecParameters(MediaType.AudioMain, {
6990
7046
  maxaveragebitrate: '64000',
6991
7047
  maxplaybackrate: '48000',
6992
7048
  });
6993
7049
  } else {
6994
- await this.mediaProperties.webrtcMediaConnection.deleteCodecParameters(MediaType.AudioMain, [
7050
+ await this.sendSlotManager.deleteCodecParameters(MediaType.AudioMain, [
6995
7051
  'maxaveragebitrate',
6996
7052
  'maxplaybackrate',
6997
7053
  ]);
@@ -7013,10 +7069,12 @@ export default class Meeting extends StatelessWebexPlugin {
7013
7069
 
7014
7070
  return this.mediaProperties.webrtcMediaConnection
7015
7071
  .update({
7072
+ // TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
7016
7073
  localTracks: {
7017
- audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
7018
- video: this.mediaProperties.videoTrack?.underlyingTrack || null,
7019
- screenShareVideo: this.mediaProperties.shareVideoTrack?.underlyingTrack || null,
7074
+ audio: this.mediaProperties.audioStream?.outputTrack || null,
7075
+ video: this.mediaProperties.videoStream?.outputTrack || null,
7076
+ screenShareVideo: this.mediaProperties.shareVideoStream?.outputTrack || null,
7077
+ screenShareAudio: this.mediaProperties.shareAudioStream?.outputTrack || null,
7020
7078
  },
7021
7079
  direction: {
7022
7080
  audio: Media.getDirection(
@@ -7055,56 +7113,68 @@ export default class Meeting extends StatelessWebexPlugin {
7055
7113
  }
7056
7114
 
7057
7115
  /**
7058
- * Publishes a track.
7116
+ * Publishes a stream.
7059
7117
  *
7060
- * @param {LocalTrack} track to publish
7118
+ * @param {MediaType} mediaType of the stream
7119
+ * @param {LocalStream} stream to publish
7061
7120
  * @returns {Promise}
7062
7121
  */
7063
- private async publishTrack(track?: LocalTrack) {
7064
- if (!track) {
7122
+ private async publishStream(mediaType: MediaType, stream?: LocalStream) {
7123
+ if (!stream) {
7065
7124
  return;
7066
7125
  }
7067
7126
 
7068
7127
  if (this.mediaProperties.webrtcMediaConnection) {
7069
- if (this.isMultistream) {
7070
- await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
7071
- } else {
7072
- track.setPublished(true); // for multistream, this call is done by WCME
7128
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
7129
+ await this.sendSlotManager.publishStream(mediaType, stream);
7073
7130
  }
7131
+
7132
+ this.emitPublishStateChangeEvent({
7133
+ isPublished: true,
7134
+ mediaType,
7135
+ stream,
7136
+ functionName: 'publishStream',
7137
+ });
7074
7138
  }
7075
7139
  }
7076
7140
 
7077
7141
  /**
7078
- * Un-publishes a track.
7142
+ * Un-publishes a stream.
7079
7143
  *
7080
- * @param {LocalTrack} track to unpublish
7144
+ * @param {MediaType} mediaType of the stream
7145
+ * @param {LocalStream} stream to unpublish
7081
7146
  * @returns {Promise}
7082
7147
  */
7083
- private async unpublishTrack(track?: LocalTrack) {
7084
- if (!track) {
7148
+ private async unpublishStream(mediaType: MediaType, stream?: LocalStream) {
7149
+ if (!stream) {
7085
7150
  return;
7086
7151
  }
7087
7152
 
7088
7153
  if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
7089
- await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
7090
- } else {
7091
- track.setPublished(false); // for multistream, this call is done by WCME
7154
+ await this.sendSlotManager.unpublishStream(mediaType);
7092
7155
  }
7156
+
7157
+ this.emitPublishStateChangeEvent({
7158
+ isPublished: false,
7159
+ mediaType,
7160
+ stream,
7161
+ functionName: 'unpublishStream',
7162
+ });
7093
7163
  }
7094
7164
 
7095
7165
  /**
7096
- * Publishes specified local tracks in the meeting
7166
+ * Publishes specified local streams in the meeting
7097
7167
  *
7098
- * @param {Object} tracks
7168
+ * @param {Object} streams
7099
7169
  * @returns {Promise}
7100
7170
  */
7101
- async publishTracks(tracks: LocalTracks): Promise<void> {
7171
+ async publishStreams(streams: LocalStreams): Promise<void> {
7102
7172
  this.checkMediaConnection();
7103
7173
  if (
7104
- !tracks.microphone &&
7105
- !tracks.camera &&
7106
- !tracks.screenShare?.audio &&
7107
- !tracks.screenShare?.video
7174
+ !streams.microphone &&
7175
+ !streams.camera &&
7176
+ !streams.screenShare?.audio &&
7177
+ !streams.screenShare?.video
7108
7178
  ) {
7109
7179
  // nothing to do
7110
7180
  return;
@@ -7112,24 +7182,25 @@ export default class Meeting extends StatelessWebexPlugin {
7112
7182
 
7113
7183
  let floorRequestNeeded = false;
7114
7184
 
7115
- if (tracks.screenShare?.video) {
7116
- await this.setLocalShareVideoTrack(tracks.screenShare?.video);
7185
+ // Screenshare Audio is supported only in multi stream. So we check for screenshare audio presence only if it's a multi stream meeting
7186
+ if (this.isMultistream && streams.screenShare?.audio) {
7187
+ await this.setLocalShareAudioStream(streams.screenShare.audio);
7117
7188
 
7118
7189
  floorRequestNeeded = this.screenShareFloorState === ScreenShareFloorStatus.RELEASED;
7119
7190
  }
7120
7191
 
7121
- if (this.isMultistream && tracks.screenShare?.audio) {
7122
- await this.setLocalShareAudioTrack(tracks.screenShare.audio);
7192
+ if (streams.screenShare?.video) {
7193
+ await this.setLocalShareVideoStream(streams.screenShare?.video);
7123
7194
 
7124
7195
  floorRequestNeeded = this.screenShareFloorState === ScreenShareFloorStatus.RELEASED;
7125
7196
  }
7126
7197
 
7127
- if (tracks.microphone) {
7128
- await this.setLocalAudioTrack(tracks.microphone);
7198
+ if (streams.microphone) {
7199
+ await this.setLocalAudioStream(streams.microphone);
7129
7200
  }
7130
7201
 
7131
- if (tracks.camera) {
7132
- await this.setLocalVideoTrack(tracks.camera);
7202
+ if (streams.camera) {
7203
+ await this.setLocalVideoStream(streams.camera);
7133
7204
  }
7134
7205
 
7135
7206
  if (!this.isMultistream) {
@@ -7145,31 +7216,31 @@ export default class Meeting extends StatelessWebexPlugin {
7145
7216
  }
7146
7217
 
7147
7218
  /**
7148
- * Un-publishes specified local tracks in the meeting
7219
+ * Un-publishes specified local streams in the meeting
7149
7220
  *
7150
- * @param {Array<MediaStreamTrack>} tracks
7221
+ * @param {Array<LocalStream>} streams
7151
7222
  * @returns {Promise}
7152
7223
  */
7153
- async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
7224
+ async unpublishStreams(streams: LocalStream[]): Promise<void> {
7154
7225
  this.checkMediaConnection();
7155
7226
 
7156
7227
  const promises = [];
7157
7228
 
7158
- for (const track of tracks.filter((t) => !!t)) {
7159
- if (track === this.mediaProperties.shareVideoTrack) {
7160
- promises.push(this.setLocalShareVideoTrack(undefined));
7229
+ for (const stream of streams.filter((t) => !!t)) {
7230
+ if (stream === this.mediaProperties.shareAudioStream) {
7231
+ promises.push(this.setLocalShareAudioStream(undefined));
7161
7232
  }
7162
7233
 
7163
- if (track === this.mediaProperties.shareAudioTrack) {
7164
- promises.push(this.setLocalShareAudioTrack(undefined));
7234
+ if (stream === this.mediaProperties.shareVideoStream) {
7235
+ promises.push(this.setLocalShareVideoStream(undefined));
7165
7236
  }
7166
7237
 
7167
- if (track === this.mediaProperties.audioTrack) {
7168
- promises.push(this.setLocalAudioTrack(undefined));
7238
+ if (stream === this.mediaProperties.audioStream) {
7239
+ promises.push(this.setLocalAudioStream(undefined));
7169
7240
  }
7170
7241
 
7171
- if (track === this.mediaProperties.videoTrack) {
7172
- promises.push(this.setLocalVideoTrack(undefined));
7242
+ if (stream === this.mediaProperties.videoStream) {
7243
+ promises.push(this.setLocalVideoStream(undefined));
7173
7244
  }
7174
7245
  }
7175
7246
 
@@ -7181,8 +7252,8 @@ export default class Meeting extends StatelessWebexPlugin {
7181
7252
 
7182
7253
  // we're allowing for the SDK to support just audio share as well
7183
7254
  // so a share could be active with only video, only audio, or both
7184
- // we're only releasing the floor if both tracks have ended
7185
- if (!this.mediaProperties.hasLocalShareTrack()) {
7255
+ // we're only releasing the floor if both streams have ended
7256
+ if (!this.mediaProperties.hasLocalShareStream()) {
7186
7257
  try {
7187
7258
  this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
7188
7259
  } catch (e) {