@webex/plugin-meetings 3.0.0-beta.146 → 3.0.0-beta.147

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 (56) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/common/errors/webex-errors.js +3 -2
  4. package/dist/common/errors/webex-errors.js.map +1 -1
  5. package/dist/config.js +1 -7
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +7 -15
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.js +6 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/media/index.js +5 -56
  12. package/dist/media/index.js.map +1 -1
  13. package/dist/media/properties.js +15 -93
  14. package/dist/media/properties.js.map +1 -1
  15. package/dist/meeting/index.js +1092 -1865
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting/muteState.js +88 -184
  18. package/dist/meeting/muteState.js.map +1 -1
  19. package/dist/meeting/util.js +1 -23
  20. package/dist/meeting/util.js.map +1 -1
  21. package/dist/meetings/index.js +1 -2
  22. package/dist/meetings/index.js.map +1 -1
  23. package/dist/reconnection-manager/index.js +153 -134
  24. package/dist/reconnection-manager/index.js.map +1 -1
  25. package/dist/roap/index.js +8 -7
  26. package/dist/roap/index.js.map +1 -1
  27. package/dist/types/common/errors/webex-errors.d.ts +1 -1
  28. package/dist/types/config.d.ts +0 -6
  29. package/dist/types/constants.d.ts +1 -18
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/types/media/properties.d.ts +16 -38
  32. package/dist/types/meeting/index.d.ts +90 -353
  33. package/dist/types/meeting/muteState.d.ts +36 -38
  34. package/dist/types/meeting/util.d.ts +2 -4
  35. package/package.json +19 -19
  36. package/src/common/errors/webex-errors.ts +6 -2
  37. package/src/config.ts +0 -6
  38. package/src/constants.ts +1 -14
  39. package/src/index.ts +1 -0
  40. package/src/media/index.ts +10 -53
  41. package/src/media/properties.ts +32 -92
  42. package/src/meeting/index.ts +530 -1566
  43. package/src/meeting/muteState.ts +87 -178
  44. package/src/meeting/util.ts +3 -24
  45. package/src/meetings/index.ts +0 -1
  46. package/src/reconnection-manager/index.ts +4 -9
  47. package/src/roap/index.ts +13 -14
  48. package/test/integration/spec/converged-space-meetings.js +59 -3
  49. package/test/integration/spec/journey.js +330 -256
  50. package/test/integration/spec/space-meeting.js +75 -3
  51. package/test/unit/spec/meeting/index.js +767 -1344
  52. package/test/unit/spec/meeting/muteState.js +238 -394
  53. package/test/unit/spec/meeting/utils.js +2 -9
  54. package/test/unit/spec/multistream/receiveSlot.ts +1 -1
  55. package/test/unit/spec/roap/index.ts +2 -2
  56. package/test/utils/integrationTestUtils.js +5 -23
@@ -1,5 +1,5 @@
1
1
  import uuid from 'uuid';
2
- import {cloneDeep, isEqual, pick, isString, defer, isEmpty} from 'lodash';
2
+ import {cloneDeep, isEqual, pick, defer, isEmpty} from 'lodash';
3
3
  // @ts-ignore - Fix this
4
4
  import {StatelessWebexPlugin} from '@webex/webex-core';
5
5
  import {
@@ -12,6 +12,7 @@ import {
12
12
  } from '@webex/internal-media-core';
13
13
 
14
14
  import {
15
+ getDevices,
15
16
  LocalTrack,
16
17
  LocalCameraTrack,
17
18
  LocalDisplayTrack,
@@ -31,18 +32,13 @@ import NetworkQualityMonitor from '../networkQualityMonitor';
31
32
  import LoggerProxy from '../common/logs/logger-proxy';
32
33
  import Trigger from '../common/events/trigger-proxy';
33
34
  import Roap from '../roap/index';
34
- import Media from '../media';
35
+ import Media, {type BundlePolicy} from '../media';
35
36
  import MediaProperties from '../media/properties';
36
37
  import MeetingStateMachine from './state';
37
38
  import {createMuteState} from './muteState';
38
39
  import LocusInfo from '../locus-info';
39
40
  import Metrics from '../metrics';
40
- import {
41
- trigger,
42
- mediaType as MetricsMediaType,
43
- error as MetricsError,
44
- eventType,
45
- } from '../metrics/config';
41
+ import {trigger, error as MetricsError, eventType} from '../metrics/config';
46
42
  import ReconnectionManager from '../reconnection-manager';
47
43
  import MeetingRequest from './request';
48
44
  import Members from '../members/index';
@@ -88,14 +84,12 @@ import {
88
84
  RECORDING_STATE,
89
85
  SHARE_STATUS,
90
86
  SHARE_STOPPED_REASON,
91
- VIDEO_RESOLUTIONS,
92
87
  VIDEO,
93
88
  HTTP_VERBS,
94
89
  SELF_ROLES,
95
90
  } from '../constants';
96
91
  import BEHAVIORAL_METRICS from '../metrics/constants';
97
92
  import ParameterError from '../common/errors/parameter';
98
- import MediaError from '../common/errors/media';
99
93
  import {
100
94
  MeetingInfoV2PasswordError,
101
95
  MeetingInfoV2CaptchaError,
@@ -105,6 +99,7 @@ import BrowserDetection from '../common/browser-detection';
105
99
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
106
100
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
107
101
  import {
102
+ Configuration as RemoteMediaManagerConfiguration,
108
103
  RemoteMediaManager,
109
104
  Event as RemoteMediaManagerEvent,
110
105
  } from '../multistream/remoteMediaManager';
@@ -143,12 +138,29 @@ const logRequest = (request: any, {logText = ''}) => {
143
138
  });
144
139
  };
145
140
 
141
+ export type LocalTracks = {
142
+ microphone?: LocalMicrophoneTrack;
143
+ camera?: LocalCameraTrack;
144
+ screenShare?: {
145
+ audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
146
+ video?: LocalDisplayTrack;
147
+ };
148
+ annotationInfo?: AnnotationInfo;
149
+ };
150
+
151
+ export type AddMediaOptions = {
152
+ localTracks?: LocalTracks;
153
+ audioEnabled?: boolean; // if not specified, default value true is used
154
+ videoEnabled?: boolean; // if not specified, default value true is used
155
+ receiveShare?: boolean; // if not specified, default value true is used
156
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration; // applies only to multistream meetings
157
+ bundlePolicy?: BundlePolicy; // applies only to multistream meetings
158
+ };
159
+
146
160
  export const MEDIA_UPDATE_TYPE = {
147
- ALL: 'ALL',
148
- AUDIO: 'AUDIO',
149
- VIDEO: 'VIDEO',
150
- SHARE: 'SHARE',
161
+ TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
151
162
  LAMBDA: 'LAMBDA',
163
+ UPDATE_MEDIA: 'UPDATE_MEDIA',
152
164
  };
153
165
 
154
166
  /**
@@ -163,16 +175,6 @@ export const MEDIA_UPDATE_TYPE = {
163
175
  * @property {boolean} isSharing
164
176
  */
165
177
 
166
- /**
167
- * AudioVideo
168
- * @typedef {Object} AudioVideo
169
- * @property {Object} audio
170
- * @property {String} audio.deviceId
171
- * @property {Object} video
172
- * @property {String} video.deviceId
173
- * @property {String} video.localVideoQuality // [240p, 360p, 480p, 720p, 1080p]
174
- */
175
-
176
178
  /**
177
179
  * SharePreferences
178
180
  * @typedef {Object} SharePreferences
@@ -187,21 +189,12 @@ export const MEDIA_UPDATE_TYPE = {
187
189
  * @property {String} [pin]
188
190
  * @property {Boolean} [moderator]
189
191
  * @property {String|Object} [meetingQuality]
190
- * @property {String} [meetingQuality.local]
191
192
  * @property {String} [meetingQuality.remote]
192
193
  * @property {Boolean} [rejoin]
193
194
  * @property {Boolean} [enableMultistream]
194
195
  * @property {String} [correlationId]
195
196
  */
196
197
 
197
- /**
198
- * SendOptions
199
- * @typedef {Object} SendOptions
200
- * @property {Boolean} sendAudio
201
- * @property {Boolean} sendVideo
202
- * @property {Boolean} sendShare
203
- */
204
-
205
198
  /**
206
199
  * Recording
207
200
  * @typedef {Object} Recording
@@ -535,6 +528,7 @@ export default class Meeting extends StatelessWebexPlugin {
535
528
  state: any;
536
529
  localAudioTrackMuteStateHandler: (event: TrackMuteEvent) => void;
537
530
  localVideoTrackMuteStateHandler: (event: TrackMuteEvent) => void;
531
+ underlyingLocalTrackChangeHandler: () => void;
538
532
  roles: any[];
539
533
  environment: string;
540
534
  namespace = MEETINGS;
@@ -783,7 +777,7 @@ export default class Meeting extends StatelessWebexPlugin {
783
777
  */
784
778
  this.reconnectionManager = new ReconnectionManager(this);
785
779
  /**
786
- * created later
780
+ * created with media connection
787
781
  * @instance
788
782
  * @type {MuteState}
789
783
  * @private
@@ -791,7 +785,7 @@ export default class Meeting extends StatelessWebexPlugin {
791
785
  */
792
786
  this.audio = null;
793
787
  /**
794
- * created later
788
+ * created with media connection
795
789
  * @instance
796
790
  * @type {MuteState}
797
791
  * @private
@@ -1202,6 +1196,16 @@ export default class Meeting extends StatelessWebexPlugin {
1202
1196
  this.localVideoTrackMuteStateHandler = (event) => {
1203
1197
  this.video.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1204
1198
  };
1199
+
1200
+ // The handling of underlying track changes should be done inside
1201
+ // @webex/internal-media-core, but for now we have to do it here, because
1202
+ // RoapMediaConnection has to use raw MediaStreamTracks in its API until
1203
+ // the Calling SDK also moves to using webrtc-core tracks
1204
+ this.underlyingLocalTrackChangeHandler = () => {
1205
+ if (!this.isMultistream) {
1206
+ this.updateTranscodedMediaConnection();
1207
+ }
1208
+ };
1205
1209
  }
1206
1210
 
1207
1211
  /**
@@ -1912,11 +1916,7 @@ export default class Meeting extends StatelessWebexPlugin {
1912
1916
  this.pstnUpdate(payload);
1913
1917
 
1914
1918
  // If user moved to a JOINED state and there is a pending floor grant trigger it
1915
- if (this.floorGrantPending && payload.newSelf.state === MEETING_STATE.STATES.JOINED) {
1916
- this.requestScreenShareFloor().then(() => {
1917
- this.floorGrantPending = false;
1918
- });
1919
- }
1919
+ this.requestScreenShareFloorIfPending();
1920
1920
  });
1921
1921
  }
1922
1922
 
@@ -2254,29 +2254,8 @@ export default class Meeting extends StatelessWebexPlugin {
2254
2254
  this.selfId === contentShare.beneficiaryId &&
2255
2255
  contentShare.disposition === FLOOR_ACTION.GRANTED
2256
2256
  ) {
2257
- // @ts-ignore originalTrack is private - this will be fixed when SPARK-399695 is done
2258
- const localShareTrack = this.mediaProperties.shareTrack?.originalTrack;
2259
-
2260
- // todo: remove this block of code and instead make sure we have LocalTrackEvents.Ended listener always registered (SPARK-399695)
2261
- if (localShareTrack?.readyState === 'ended') {
2262
- try {
2263
- if (this.isMultistream) {
2264
- await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2265
- } else {
2266
- await this.stopShare({
2267
- skipSignalingCheck: true,
2268
- });
2269
- }
2270
- } catch (error) {
2271
- LoggerProxy.logger.log(
2272
- 'Meeting:index#setUpLocusMediaSharesListener --> Error stopping share: ',
2273
- error
2274
- );
2275
- }
2276
- } else {
2277
- // CONTENT - sharing content local
2278
- newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2279
- }
2257
+ // CONTENT - sharing content local
2258
+ newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2280
2259
  }
2281
2260
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
2282
2261
  // There is no concept of local/remote share for whiteboard
@@ -2372,14 +2351,7 @@ export default class Meeting extends StatelessWebexPlugin {
2372
2351
  this.mediaProperties.mediaDirection?.sendShare &&
2373
2352
  oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
2374
2353
  ) {
2375
- if (this.isMultistream) {
2376
- await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2377
- } else {
2378
- await this.updateShare({
2379
- sendShare: false,
2380
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
2381
- });
2382
- }
2354
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2383
2355
  }
2384
2356
  } finally {
2385
2357
  sendStartedSharingRemote();
@@ -2997,13 +2969,13 @@ export default class Meeting extends StatelessWebexPlugin {
2997
2969
  });
2998
2970
  }
2999
2971
  });
3000
- this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
2972
+ this.locusInfo.on(EVENTS.DESTROY_MEETING, async (payload) => {
3001
2973
  // if self state is NOT left
3002
2974
 
3003
2975
  // TODO: Handle sharing and wireless sharing when meeting end
3004
2976
  if (this.wirelessShare) {
3005
2977
  if (this.mediaProperties.shareTrack) {
3006
- this.setLocalShareTrack(null);
2978
+ await this.setLocalShareTrack(undefined);
3007
2979
  }
3008
2980
  }
3009
2981
  // when multiple WEB deviceType join with same user
@@ -3017,18 +2989,18 @@ export default class Meeting extends StatelessWebexPlugin {
3017
2989
  if (payload.shouldLeave) {
3018
2990
  // TODO: We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
3019
2991
 
3020
- this.leave({reason: payload.reason})
3021
- .then(() => {
3022
- LoggerProxy.logger.warn(
3023
- 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
3024
- );
3025
- })
3026
- .catch((error) => {
3027
- // @ts-ignore
3028
- LoggerProxy.logger.error(
3029
- `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
3030
- );
3031
- });
2992
+ try {
2993
+ await this.leave({reason: payload.reason});
2994
+
2995
+ LoggerProxy.logger.warn(
2996
+ 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
2997
+ );
2998
+ } catch (error) {
2999
+ // @ts-ignore
3000
+ LoggerProxy.logger.error(
3001
+ `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
3002
+ );
3003
+ }
3032
3004
  } else {
3033
3005
  LoggerProxy.logger.info(
3034
3006
  'Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON',
@@ -3179,66 +3151,6 @@ export default class Meeting extends StatelessWebexPlugin {
3179
3151
  return this.members;
3180
3152
  }
3181
3153
 
3182
- /**
3183
- * Truthy when a meeting has an audio connection established
3184
- * @returns {Boolean} true if meeting audio is connected otherwise false
3185
- * @public
3186
- * @memberof Meeting
3187
- */
3188
- public isAudioConnected() {
3189
- return !!this.audio;
3190
- }
3191
-
3192
- /**
3193
- * Convenience function to tell whether a meeting is muted
3194
- * @returns {Boolean} if meeting audio muted or not
3195
- * @public
3196
- * @memberof Meeting
3197
- */
3198
- public isAudioMuted() {
3199
- return this.audio && this.audio.isMuted();
3200
- }
3201
-
3202
- /**
3203
- * Convenience function to tell if the end user last changed the audio state
3204
- * @returns {Boolean} if audio was manipulated by the end user
3205
- * @public
3206
- * @memberof Meeting
3207
- */
3208
- public isAudioSelf() {
3209
- return this.audio && this.audio.isSelf();
3210
- }
3211
-
3212
- /**
3213
- * Truthy when a meeting has a video connection established
3214
- * @returns {Boolean} true if meeting video connected otherwise false
3215
- * @public
3216
- * @memberof Meeting
3217
- */
3218
- public isVideoConnected() {
3219
- return !!this.video;
3220
- }
3221
-
3222
- /**
3223
- * Convenience function to tell whether video is muted
3224
- * @returns {Boolean} if meeting video is muted or not
3225
- * @public
3226
- * @memberof Meeting
3227
- */
3228
- public isVideoMuted() {
3229
- return this.video && this.video.isMuted();
3230
- }
3231
-
3232
- /**
3233
- * Convenience function to tell whether the end user changed the video state
3234
- * @returns {Boolean} if meeting video is muted or not
3235
- * @public
3236
- * @memberof Meeting
3237
- */
3238
- public isVideoSelf() {
3239
- return this.video && this.video.isSelf();
3240
- }
3241
-
3242
3154
  /**
3243
3155
  * Sets the meeting info on the class instance
3244
3156
  * @param {Object} meetingInfo
@@ -3369,21 +3281,6 @@ export default class Meeting extends StatelessWebexPlugin {
3369
3281
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
3370
3282
  }
3371
3283
 
3372
- /**
3373
- * Removes remote audio and video stream on the class instance and triggers an event
3374
- * to developers
3375
- * @returns {undefined}
3376
- * @public
3377
- * @memberof Meeting
3378
- * @deprecated after v1.89.3
3379
- */
3380
- public unsetRemoteStream() {
3381
- LoggerProxy.logger.warn(
3382
- 'Meeting:index#unsetRemoteStream --> [DEPRECATION WARNING]: unsetRemoteStream has been deprecated after v1.89.3'
3383
- );
3384
- this.mediaProperties.unsetRemoteMedia();
3385
- }
3386
-
3387
3284
  /**
3388
3285
  * Removes remote audio, video and share tracks from class instance's mediaProperties
3389
3286
  * @returns {undefined}
@@ -3468,274 +3365,124 @@ export default class Meeting extends StatelessWebexPlugin {
3468
3365
  }
3469
3366
 
3470
3367
  /**
3471
- * Emits the 'media:ready' event with a local stream that consists of 1 local audio and 1 local video track
3472
- * @returns {undefined}
3473
- * @private
3474
- * @memberof Meeting
3475
- */
3476
- private sendLocalMediaReadyEvent() {
3477
- Trigger.trigger(
3478
- this,
3479
- {
3480
- file: 'meeting/index',
3481
- function: 'sendLocalMediaReadyEvent',
3482
- },
3483
- EVENT_TRIGGERS.MEDIA_READY,
3484
- {
3485
- type: EVENT_TYPES.LOCAL,
3486
- stream: MediaUtil.createMediaStream([
3487
- this.mediaProperties.audioTrack?.underlyingTrack,
3488
- this.mediaProperties.videoTrack?.underlyingTrack,
3489
- ]),
3490
- }
3491
- );
3492
- }
3493
-
3494
- /**
3495
- * Sets the local audio track on the class and emits an event to the developer
3496
- * @param {MediaStreamTrack} rawAudioTrack
3497
- * @param {Boolean} emitEvent if true, a media ready event is emitted to the developer
3498
- * @returns {undefined}
3499
- * @private
3500
- * @memberof Meeting
3368
+ * Stores the reference to a new microphone track, sets up the required event listeners
3369
+ * on it, cleans up previous track, etc.
3370
+ *
3371
+ * @param {LocalMicrophoneTrack | null} localTrack local microphone track
3372
+ * @returns {Promise<void>}
3501
3373
  */
3502
- private setLocalAudioTrack(rawAudioTrack: MediaStreamTrack | null, emitEvent = true) {
3503
- if (this.isMultistream) {
3504
- throw new Error('this method is only supposed to be used for transcoded meetings');
3505
- }
3374
+ private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
3375
+ const oldTrack = this.mediaProperties.audioTrack;
3506
3376
 
3507
- if (rawAudioTrack) {
3508
- const settings = rawAudioTrack.getSettings();
3377
+ oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3378
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3509
3379
 
3510
- const localMicrophoneTrack = new LocalMicrophoneTrack(
3511
- MediaUtil.createMediaStream([rawAudioTrack])
3512
- );
3380
+ // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
3381
+ this.mediaProperties.setLocalAudioTrack(localTrack);
3513
3382
 
3514
- this.mediaProperties.setMediaSettings('audio', {
3515
- echoCancellation: settings.echoCancellation,
3516
- noiseSuppression: settings.noiseSuppression,
3517
- });
3383
+ this.audio.handleLocalTrackChange(this);
3518
3384
 
3519
- LoggerProxy.logger.log(
3520
- 'Meeting:index#setLocalAudioTrack --> Audio settings.',
3521
- JSON.stringify(this.mediaProperties.mediaSettings.audio)
3522
- );
3523
- this.mediaProperties.setLocalAudioTrack(localMicrophoneTrack);
3524
- if (this.audio) this.audio.applyClientStateLocally(this);
3525
- } else {
3526
- this.mediaProperties.setLocalAudioTrack(null);
3527
- }
3385
+ localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3386
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3528
3387
 
3529
- if (emitEvent) {
3530
- this.sendLocalMediaReadyEvent();
3388
+ if (!this.isMultistream || !localTrack) {
3389
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3390
+ await this.unpublishTrack(oldTrack);
3531
3391
  }
3392
+ await this.publishTrack(this.mediaProperties.audioTrack);
3532
3393
  }
3533
3394
 
3534
3395
  /**
3535
- * Sets the local video track on the class and emits an event to the developer
3536
- * @param {MediaStreamTrack} rawVideoTrack
3537
- * @param {Boolean} emitEvent if true, a media ready event is emitted to the developer
3538
- * @returns {undefined}
3539
- * @private
3540
- * @memberof Meeting
3396
+ * Stores the reference to a new camera track, sets up the required event listeners
3397
+ * on it, cleans up previous track, etc.
3398
+ *
3399
+ * @param {LocalCameraTrack | null} localTrack local camera track
3400
+ * @returns {Promise<void>}
3541
3401
  */
3542
- private setLocalVideoTrack(rawVideoTrack: MediaStreamTrack | null, emitEvent = true) {
3543
- if (this.isMultistream) {
3544
- throw new Error('this method is only supposed to be used for transcoded meetings');
3545
- }
3546
-
3547
- if (rawVideoTrack) {
3548
- const {aspectRatio, frameRate, height, width, deviceId} = rawVideoTrack.getSettings();
3549
-
3550
- const {localQualityLevel} = this.mediaProperties;
3551
-
3552
- const localCameraTrack = new LocalCameraTrack(MediaUtil.createMediaStream([rawVideoTrack]));
3553
-
3554
- if (Number(localQualityLevel.slice(0, -1)) > height) {
3555
- LoggerProxy.logger
3556
- .warn(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
3557
- downscaling to highest possible resolution of ${height}p`);
3558
-
3559
- this.mediaProperties.setLocalQualityLevel(`${height}p`);
3560
- }
3402
+ private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
3403
+ const oldTrack = this.mediaProperties.videoTrack;
3561
3404
 
3562
- this.mediaProperties.setLocalVideoTrack(localCameraTrack);
3563
- if (this.video) this.video.applyClientStateLocally(this);
3405
+ oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3406
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3564
3407
 
3565
- this.mediaProperties.setMediaSettings('video', {
3566
- aspectRatio,
3567
- frameRate,
3568
- height,
3569
- width,
3570
- });
3571
- // store and save the selected video input device
3572
- if (deviceId) {
3573
- this.mediaProperties.setVideoDeviceId(deviceId);
3574
- }
3575
- LoggerProxy.logger.log(
3576
- 'Meeting:index#setLocalVideoTrack --> Video settings.',
3577
- JSON.stringify(this.mediaProperties.mediaSettings.video)
3578
- );
3579
- } else {
3580
- this.mediaProperties.setLocalVideoTrack(null);
3581
- }
3582
-
3583
- if (emitEvent) {
3584
- this.sendLocalMediaReadyEvent();
3585
- }
3586
- }
3587
-
3588
- /**
3589
- * Sets the local media stream on the class and emits an event to the developer
3590
- * @param {Stream} localStream the local media stream
3591
- * @returns {undefined}
3592
- * @public
3593
- * @memberof Meeting
3594
- */
3595
- public setLocalTracks(localStream: any) {
3596
- if (localStream) {
3597
- if (this.isMultistream) {
3598
- throw new Error(
3599
- 'addMedia() and updateMedia() APIs are not supported with multistream, use publishTracks/unpublishTracks instead'
3600
- );
3601
- }
3408
+ // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
3409
+ this.mediaProperties.setLocalVideoTrack(localTrack);
3602
3410
 
3603
- const {audioTrack, videoTrack} = MeetingUtil.getTrack(localStream);
3411
+ this.video.handleLocalTrackChange(this);
3604
3412
 
3605
- this.setLocalAudioTrack(audioTrack, false);
3606
- this.setLocalVideoTrack(videoTrack, false);
3413
+ localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3414
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3607
3415
 
3608
- this.sendLocalMediaReadyEvent();
3416
+ if (!this.isMultistream || !localTrack) {
3417
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3418
+ await this.unpublishTrack(oldTrack);
3609
3419
  }
3420
+ await this.publishTrack(this.mediaProperties.videoTrack);
3610
3421
  }
3611
3422
 
3612
3423
  /**
3613
- * Sets the local media stream on the class and emits an event to the developer
3614
- * @param {MediaStreamTrack} rawLocalShareTrack the local share media track
3615
- * @returns {undefined}
3616
- * @public
3617
- * @memberof Meeting
3424
+ * Stores the reference to a new screen share track, sets up the required event listeners
3425
+ * on it, cleans up previous track, etc.
3426
+ * It also sends the floor grant/release request.
3427
+ *
3428
+ * @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
3429
+ * @returns {Promise<void>}
3618
3430
  */
3619
- public setLocalShareTrack(rawLocalShareTrack: MediaStreamTrack | null) {
3620
- if (rawLocalShareTrack) {
3621
- const settings = rawLocalShareTrack.getSettings();
3431
+ private async setLocalShareTrack(localDisplayTrack?: LocalDisplayTrack) {
3432
+ const oldTrack = this.mediaProperties.shareTrack;
3622
3433
 
3623
- const localDisplayTrack = new LocalDisplayTrack(
3624
- MediaUtil.createMediaStream([rawLocalShareTrack])
3625
- );
3434
+ oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3435
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3626
3436
 
3627
- this.mediaProperties.setLocalShareTrack(localDisplayTrack);
3437
+ this.mediaProperties.setLocalShareTrack(localDisplayTrack);
3628
3438
 
3629
- this.mediaProperties.setMediaSettings('screen', {
3630
- aspectRatio: settings.aspectRatio,
3631
- frameRate: settings.frameRate,
3632
- height: settings.height,
3633
- width: settings.width,
3634
- // @ts-ignore
3635
- displaySurface: settings.displaySurface,
3636
- // @ts-ignore
3637
- cursor: settings.cursor,
3638
- });
3639
- LoggerProxy.logger.log(
3640
- 'Meeting:index#setLocalShareTrack --> Screen settings.',
3641
- JSON.stringify(this.mediaProperties.mediaSettings.screen)
3642
- );
3439
+ localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3440
+ localDisplayTrack?.on(
3441
+ LocalTrackEvents.UnderlyingTrackChange,
3442
+ this.underlyingLocalTrackChangeHandler
3443
+ );
3643
3444
 
3644
- localDisplayTrack.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3445
+ this.mediaProperties.mediaDirection.sendShare = !!localDisplayTrack;
3645
3446
 
3646
- Trigger.trigger(
3647
- this,
3648
- {
3649
- file: 'meeting/index',
3650
- function: 'setLocalShareTrack',
3651
- },
3652
- EVENT_TRIGGERS.MEDIA_READY,
3653
- {
3654
- type: EVENT_TYPES.LOCAL_SHARE,
3655
- track: rawLocalShareTrack,
3656
- }
3657
- );
3658
- } else if (this.mediaProperties.shareTrack) {
3659
- this.mediaProperties.shareTrack.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3660
- this.mediaProperties.shareTrack.stop(); // todo: this line should be removed once SPARK-399695 is done
3661
- this.mediaProperties.setLocalShareTrack(null);
3447
+ if (!this.isMultistream || !localDisplayTrack) {
3448
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3449
+ await this.unpublishTrack(oldTrack);
3662
3450
  }
3451
+ await this.publishTrack(this.mediaProperties.shareTrack);
3663
3452
  }
3664
3453
 
3665
3454
  /**
3666
- * Closes the local stream from the class and emits an event to the developer
3667
- * @returns {undefined}
3668
- * @event media:stopped
3669
- * @public
3670
- * @memberof Meeting
3455
+ * Removes references to local tracks. This function should be called
3456
+ * on cleanup when we leave the meeting etc.
3457
+ *
3458
+ * @internal
3459
+ * @returns {void}
3671
3460
  */
3672
- public closeLocalStream() {
3673
- const {audioTrack, videoTrack} = this.mediaProperties;
3461
+ public cleanupLocalTracks() {
3462
+ const {audioTrack, videoTrack, shareTrack} = this.mediaProperties;
3674
3463
 
3675
- return Media.stopTracks(audioTrack)
3676
- .then(() => Media.stopTracks(videoTrack))
3677
- .then(() => {
3678
- if (audioTrack || videoTrack) {
3679
- Trigger.trigger(
3680
- this,
3681
- {
3682
- file: 'meeting/index',
3683
- function: 'closeLocalStream',
3684
- },
3685
- EVENT_TRIGGERS.MEDIA_STOPPED,
3686
- {
3687
- type: EVENT_TYPES.LOCAL,
3688
- }
3689
- );
3690
- }
3691
- });
3692
- }
3464
+ audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3465
+ audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3693
3466
 
3694
- /**
3695
- * Closes the local stream from the class and emits an event to the developer
3696
- * @returns {undefined}
3697
- * @event media:stopped
3698
- * @public
3699
- * @memberof Meeting
3700
- */
3701
- public closeLocalShare() {
3702
- const track = this.mediaProperties.shareTrack;
3467
+ videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3468
+ videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3703
3469
 
3704
- return Media.stopTracks(track).then(() => {
3705
- if (track) {
3706
- Trigger.trigger(
3707
- this,
3708
- {
3709
- file: 'meeting/index',
3710
- function: 'closeLocalShare',
3711
- },
3712
- EVENT_TRIGGERS.MEDIA_STOPPED,
3713
- {
3714
- type: EVENT_TYPES.LOCAL_SHARE,
3715
- }
3716
- );
3717
- }
3718
- });
3719
- }
3470
+ shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3471
+ shareTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3720
3472
 
3721
- /**
3722
- * Removes the local stream from the class and emits an event to the developer
3723
- * @returns {undefined}
3724
- * @public
3725
- * @memberof Meeting
3726
- */
3727
- public unsetLocalVideoTrack() {
3728
- this.mediaProperties.unsetLocalVideoTrack();
3729
- }
3473
+ this.mediaProperties.setLocalAudioTrack(undefined);
3474
+ this.mediaProperties.setLocalVideoTrack(undefined);
3475
+ this.mediaProperties.setLocalShareTrack(undefined);
3730
3476
 
3731
- /**
3732
- * Removes the local share from the class and emits an event to the developer
3733
- * @returns {undefined}
3734
- * @public
3735
- * @memberof Meeting
3736
- */
3737
- public unsetLocalShareTrack() {
3738
- this.mediaProperties.unsetLocalShareTrack();
3477
+ this.mediaProperties.mediaDirection.sendAudio = false;
3478
+ this.mediaProperties.mediaDirection.sendVideo = false;
3479
+ this.mediaProperties.mediaDirection.sendShare = false;
3480
+
3481
+ // WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
3482
+ // (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
3483
+ audioTrack?.setPublished(false);
3484
+ videoTrack?.setPublished(false);
3485
+ shareTrack?.setPublished(false);
3739
3486
  }
3740
3487
 
3741
3488
  /**
@@ -3802,6 +3549,9 @@ export default class Meeting extends StatelessWebexPlugin {
3802
3549
  this.mediaProperties.webrtcMediaConnection.close();
3803
3550
  }
3804
3551
 
3552
+ this.audio = null;
3553
+ this.video = null;
3554
+
3805
3555
  return Promise.resolve();
3806
3556
  }
3807
3557
 
@@ -3833,266 +3583,54 @@ export default class Meeting extends StatelessWebexPlugin {
3833
3583
  }
3834
3584
 
3835
3585
  /**
3836
- * Mute the audio for a meeting
3837
- * @returns {Promise} resolves the data from muting audio {mute, self} or rejects if there is no audio set
3586
+ * Shorthand function to join AND set up media
3587
+ * @param {Object} options - options to join with media
3588
+ * @param {JoinOptions} [options.joinOptions] - see #join()
3589
+ * @param {MediaDirection} [options.mediaOptions] - see #addMedia()
3590
+ * @returns {Promise} -- {join: see join(), media: see addMedia()}
3838
3591
  * @public
3839
3592
  * @memberof Meeting
3593
+ * @example
3594
+ * joinWithMedia({
3595
+ * joinOptions: {resourceId: 'resourceId' },
3596
+ * mediaOptions: {
3597
+ * localTracks: { microphone: microphoneTrack, camera: cameraTrack }
3598
+ * }
3599
+ * })
3840
3600
  */
3841
- public muteAudio() {
3842
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3843
- return Promise.reject(new UserNotJoinedError());
3844
- }
3845
-
3846
- // @ts-ignore
3847
- if (!this.mediaId) {
3848
- // Happens when addMedia and mute are triggered in succession
3849
- return Promise.reject(new NoMediaEstablishedYetError());
3850
- }
3601
+ public joinWithMedia(
3602
+ options: {
3603
+ joinOptions?: any;
3604
+ mediaOptions?: AddMediaOptions;
3605
+ } = {}
3606
+ ) {
3607
+ const {mediaOptions, joinOptions} = options;
3851
3608
 
3852
- if (!this.audio) {
3853
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
3854
- }
3609
+ return this.join(joinOptions)
3610
+ .then((joinResponse) =>
3611
+ this.addMedia(mediaOptions).then((mediaResponse) => ({
3612
+ join: joinResponse,
3613
+ media: mediaResponse,
3614
+ }))
3615
+ )
3616
+ .catch((error) => {
3617
+ LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
3855
3618
 
3856
- // First, stop sending the local audio media
3857
- return logRequest(
3858
- this.audio
3859
- .handleClientRequest(this, true)
3860
- .then(() => {
3861
- MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
3862
- Metrics.postEvent({
3863
- event: eventType.MUTED,
3864
- meeting: this,
3865
- data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.AUDIO},
3866
- });
3867
- })
3868
- .catch((error) => {
3869
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MUTE_AUDIO_FAILURE, {
3619
+ Metrics.sendBehavioralMetric(
3620
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
3621
+ {
3870
3622
  correlation_id: this.correlationId,
3871
3623
  locus_id: this.locusUrl.split('/').pop(),
3872
3624
  reason: error.message,
3873
3625
  stack: error.stack,
3874
- });
3626
+ },
3627
+ {
3628
+ type: error.name,
3629
+ }
3630
+ );
3875
3631
 
3876
- throw error;
3877
- }),
3878
- {
3879
- logText: `Meeting:index#muteAudio --> correlationId=${this.correlationId} muting audio`,
3880
- }
3881
- );
3882
- }
3883
-
3884
- /**
3885
- * Unmute meeting audio
3886
- * @returns {Promise} resolves data from muting audio {mute, self} or rejects if there is no audio set
3887
- * @public
3888
- * @memberof Meeting
3889
- */
3890
- public unmuteAudio() {
3891
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3892
- return Promise.reject(new UserNotJoinedError());
3893
- }
3894
-
3895
- // @ts-ignore
3896
- if (!this.mediaId) {
3897
- // Happens when addMedia and mute are triggered in succession
3898
- return Promise.reject(new NoMediaEstablishedYetError());
3899
- }
3900
-
3901
- if (!this.audio) {
3902
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
3903
- }
3904
-
3905
- // First, send the control to unmute the participant on the server
3906
- return logRequest(
3907
- this.audio
3908
- .handleClientRequest(this, false)
3909
- .then(() => {
3910
- MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
3911
- Metrics.postEvent({
3912
- event: eventType.UNMUTED,
3913
- meeting: this,
3914
- data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.AUDIO},
3915
- });
3916
- })
3917
- .catch((error) => {
3918
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UNMUTE_AUDIO_FAILURE, {
3919
- correlation_id: this.correlationId,
3920
- locus_id: this.locusUrl.split('/').pop(),
3921
- reason: error.message,
3922
- stack: error.stack,
3923
- });
3924
-
3925
- throw error;
3926
- }),
3927
- {
3928
- logText: `Meeting:index#unmuteAudio --> correlationId=${this.correlationId} unmuting audio`,
3929
- }
3930
- );
3931
- }
3932
-
3933
- /**
3934
- * Mute the video for a meeting
3935
- * @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
3936
- * @public
3937
- * @memberof Meeting
3938
- */
3939
- public muteVideo() {
3940
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3941
- return Promise.reject(new UserNotJoinedError());
3942
- }
3943
-
3944
- // @ts-ignore
3945
- if (!this.mediaId) {
3946
- // Happens when addMedia and mute are triggered in succession
3947
- return Promise.reject(new NoMediaEstablishedYetError());
3948
- }
3949
-
3950
- if (!this.video) {
3951
- return Promise.reject(new ParameterError('no video control associated to the meeting'));
3952
- }
3953
-
3954
- return logRequest(
3955
- this.video
3956
- .handleClientRequest(this, true)
3957
- .then(() => {
3958
- MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
3959
- Metrics.postEvent({
3960
- event: eventType.MUTED,
3961
- meeting: this,
3962
- data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.VIDEO},
3963
- });
3964
- })
3965
- .catch((error) => {
3966
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MUTE_VIDEO_FAILURE, {
3967
- correlation_id: this.correlationId,
3968
- locus_id: this.locusUrl.split('/').pop(),
3969
- reason: error.message,
3970
- stack: error.stack,
3971
- });
3972
-
3973
- throw error;
3974
- }),
3975
- {
3976
- logText: `Meeting:index#muteVideo --> correlationId=${this.correlationId} muting video`,
3977
- }
3978
- );
3979
- }
3980
-
3981
- /**
3982
- * Unmute meeting video
3983
- * @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
3984
- * @public
3985
- * @memberof Meeting
3986
- */
3987
- public unmuteVideo() {
3988
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3989
- return Promise.reject(new UserNotJoinedError());
3990
- }
3991
-
3992
- // @ts-ignore
3993
- if (!this.mediaId) {
3994
- // Happens when addMedia and mute are triggered in succession
3995
- return Promise.reject(new NoMediaEstablishedYetError());
3996
- }
3997
-
3998
- if (!this.video) {
3999
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
4000
- }
4001
-
4002
- return logRequest(
4003
- this.video
4004
- .handleClientRequest(this, false)
4005
- .then(() => {
4006
- MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
4007
- Metrics.postEvent({
4008
- event: eventType.UNMUTED,
4009
- meeting: this,
4010
- data: {trigger: trigger.USER_INTERACTION, mediaType: MetricsMediaType.VIDEO},
4011
- });
4012
- })
4013
- .catch((error) => {
4014
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UNMUTE_VIDEO_FAILURE, {
4015
- correlation_id: this.correlationId,
4016
- locus_id: this.locusUrl.split('/').pop(),
4017
- reason: error.message,
4018
- stack: error.stack,
4019
- });
4020
-
4021
- throw error;
4022
- }),
4023
- {
4024
- logText: `Meeting:index#unmuteVideo --> correlationId=${this.correlationId} unmuting video`,
4025
- }
4026
- );
4027
- }
4028
-
4029
- /**
4030
- * Shorthand function to join AND set up media
4031
- * @param {Object} options - options to join with media
4032
- * @param {JoinOptions} [options.joinOptions] - see #join()
4033
- * @param {MediaDirection} options.mediaSettings - see #addMedia()
4034
- * @param {AudioVideo} [options.audioVideoOptions] - see #getMediaStreams()
4035
- * @returns {Promise} -- {join: see join(), media: see addMedia(), local: see getMediaStreams()}
4036
- * @public
4037
- * @memberof Meeting
4038
- * @example
4039
- * joinWithMedia({
4040
- * joinOptions: {resourceId: 'resourceId' },
4041
- * mediaSettings: {
4042
- * sendAudio: true,
4043
- * sendVideo: true,
4044
- * sendShare: false,
4045
- * receiveVideo:true,
4046
- * receiveAudio: true,
4047
- * receiveShare: true
4048
- * }
4049
- * audioVideoOptions: {
4050
- * audio: 'audioDeviceId',
4051
- * video: 'videoDeviceId'
4052
- * }})
4053
- */
4054
- public joinWithMedia(
4055
- options: {
4056
- joinOptions?: any;
4057
- mediaSettings: any;
4058
- audioVideoOptions?: any;
4059
- } = {} as any
4060
- ) {
4061
- // TODO: add validations for parameters
4062
- const {mediaSettings, joinOptions, audioVideoOptions} = options;
4063
-
4064
- return this.join(joinOptions)
4065
- .then((joinResponse) =>
4066
- this.getMediaStreams(mediaSettings, audioVideoOptions).then(([localStream, localShare]) =>
4067
- this.addMedia({
4068
- mediaSettings,
4069
- localShare,
4070
- localStream,
4071
- }).then((mediaResponse) => ({
4072
- join: joinResponse,
4073
- media: mediaResponse,
4074
- local: [localStream, localShare],
4075
- }))
4076
- )
4077
- )
4078
- .catch((error) => {
4079
- LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
4080
-
4081
- Metrics.sendBehavioralMetric(
4082
- BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
4083
- {
4084
- correlation_id: this.correlationId,
4085
- locus_id: this.locusUrl.split('/').pop(),
4086
- reason: error.message,
4087
- stack: error.stack,
4088
- },
4089
- {
4090
- type: error.name,
4091
- }
4092
- );
4093
-
4094
- return Promise.reject(error);
4095
- });
3632
+ return Promise.reject(error);
3633
+ });
4096
3634
  }
4097
3635
 
4098
3636
  /**
@@ -4508,18 +4046,12 @@ export default class Meeting extends StatelessWebexPlugin {
4508
4046
  return Promise.reject(error);
4509
4047
  }
4510
4048
 
4511
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
4512
4049
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
4513
4050
  }
4514
4051
 
4515
4052
  if (typeof options.meetingQuality === 'object') {
4516
- if (
4517
- !QUALITY_LEVELS[options.meetingQuality.local] &&
4518
- !QUALITY_LEVELS[options.meetingQuality.remote]
4519
- ) {
4520
- const errorMessage = `Meeting:index#join --> ${
4521
- options.meetingQuality.local || options.meetingQuality.remote
4522
- } not defined`;
4053
+ if (!QUALITY_LEVELS[options.meetingQuality.remote]) {
4054
+ const errorMessage = `Meeting:index#join --> ${options.meetingQuality.remote} not defined`;
4523
4055
 
4524
4056
  LoggerProxy.logger.error(errorMessage);
4525
4057
 
@@ -4531,9 +4063,6 @@ export default class Meeting extends StatelessWebexPlugin {
4531
4063
  return Promise.reject(new Error(errorMessage));
4532
4064
  }
4533
4065
 
4534
- if (options.meetingQuality.local) {
4535
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
4536
- }
4537
4066
  if (options.meetingQuality.remote) {
4538
4067
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
4539
4068
  }
@@ -4840,14 +4369,10 @@ export default class Meeting extends StatelessWebexPlugin {
4840
4369
  },
4841
4370
  };
4842
4371
 
4843
- // clean up the local tracks
4844
- this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4845
-
4846
- // close the existing local tracks
4847
- await this.closeLocalStream();
4848
- await this.closeLocalShare();
4372
+ this.cleanupLocalTracks();
4849
4373
 
4850
- this.mediaProperties.unsetMediaTracks();
4374
+ this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4375
+ this.mediaProperties.unsetRemoteMedia();
4851
4376
 
4852
4377
  // when a move to is intiated by the client , Locus delets the existing media node from the server as soon the DX answers the meeting
4853
4378
  // once the DX answers we establish connection back the media server with only receiveShare enabled
@@ -4930,165 +4455,6 @@ export default class Meeting extends StatelessWebexPlugin {
4930
4455
  });
4931
4456
  }
4932
4457
 
4933
- /**
4934
- * Get local media streams based on options passed
4935
- *
4936
- * NOTE: this method can only be used with transcoded meetings, not with multistream meetings
4937
- *
4938
- * @param {MediaDirection} mediaDirection A configurable options object for joining a meeting
4939
- * @param {AudioVideo} [audioVideo] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
4940
- * @param {SharePreferences} [sharePreferences] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
4941
- * @returns {Promise} see #Media.getUserMedia
4942
- * @public
4943
- * @todo should be static, or moved so can be called outside of a meeting
4944
- * @memberof Meeting
4945
- */
4946
- getMediaStreams = (
4947
- mediaDirection: any,
4948
- // This return an OBJECT {video: {height, widght}}
4949
- // eslint-disable-next-line default-param-last
4950
- audioVideo: any = VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel],
4951
- sharePreferences?: any
4952
- ) => {
4953
- if (
4954
- mediaDirection &&
4955
- (mediaDirection.sendAudio || mediaDirection.sendVideo || mediaDirection.sendShare)
4956
- ) {
4957
- if (
4958
- mediaDirection &&
4959
- mediaDirection.sendAudio &&
4960
- mediaDirection.sendVideo &&
4961
- mediaDirection.sendShare &&
4962
- isBrowser('safari')
4963
- ) {
4964
- LoggerProxy.logger.warn(
4965
- 'Meeting:index#getMediaStreams --> Setting `sendShare` to FALSE, due to complications with Safari'
4966
- );
4967
-
4968
- mediaDirection.sendShare = false;
4969
-
4970
- LoggerProxy.logger.warn(
4971
- 'Meeting:index#getMediaStreams --> Enabling `sendShare` along with `sendAudio` & `sendVideo`, on Safari, causes a failure while setting up a screen share at the same time as the camera+mic stream'
4972
- );
4973
- LoggerProxy.logger.warn(
4974
- 'Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting'
4975
- );
4976
- }
4977
-
4978
- if (audioVideo && isString(audioVideo)) {
4979
- if (Object.keys(VIDEO_RESOLUTIONS).includes(audioVideo)) {
4980
- this.mediaProperties.setLocalQualityLevel(audioVideo);
4981
- audioVideo = {video: VIDEO_RESOLUTIONS[audioVideo].video};
4982
- } else {
4983
- throw new ParameterError(
4984
- `${audioVideo} not supported. Either pass level from pre-defined resolutions or pass complete audioVideo object`
4985
- );
4986
- }
4987
- }
4988
-
4989
- if (!audioVideo.video) {
4990
- audioVideo = {
4991
- ...audioVideo,
4992
- video: {
4993
- ...audioVideo.video,
4994
- ...VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel].video,
4995
- },
4996
- };
4997
- }
4998
- // extract deviceId if exists otherwise default to null.
4999
- const {deviceId: preferredVideoDevice} = (audioVideo && audioVideo.video) || {deviceId: null};
5000
- const lastVideoDeviceId = this.mediaProperties.getVideoDeviceId();
5001
-
5002
- if (preferredVideoDevice) {
5003
- // Store new preferred video input device
5004
- this.mediaProperties.setVideoDeviceId(preferredVideoDevice);
5005
- } else if (lastVideoDeviceId) {
5006
- // no new video preference specified so use last stored value,
5007
- // works with empty object {} or media constraint.
5008
- // eslint-disable-next-line no-param-reassign
5009
- audioVideo = {
5010
- ...audioVideo,
5011
- video: {
5012
- ...audioVideo.video,
5013
- deviceId: lastVideoDeviceId,
5014
- },
5015
- };
5016
- }
5017
-
5018
- return Media.getSupportedDevice({
5019
- sendAudio: mediaDirection.sendAudio,
5020
- sendVideo: mediaDirection.sendVideo,
5021
- })
5022
- .catch((error) =>
5023
- Promise.reject(
5024
- new MediaError(
5025
- 'Given constraints do not match permission set for either camera or microphone',
5026
- error
5027
- )
5028
- )
5029
- )
5030
- .then((devicePermissions) =>
5031
- Media.getUserMedia(
5032
- {
5033
- ...mediaDirection,
5034
- sendAudio: devicePermissions.sendAudio,
5035
- sendVideo: devicePermissions.sendVideo,
5036
- isSharing: this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE,
5037
- },
5038
- audioVideo,
5039
- sharePreferences,
5040
- // @ts-ignore - config coming from registerPlugin
5041
- this.config
5042
- ).catch((error) => {
5043
- // Whenever there is a failure when trying to access a user's device
5044
- // report it as an Behavioral metric
5045
- // This gives visibility into common errors and can help
5046
- // with further troubleshooting
5047
- const metricName = BEHAVIORAL_METRICS.GET_USER_MEDIA_FAILURE;
5048
- const data = {
5049
- correlation_id: this.correlationId,
5050
- locus_id: this.locusUrl?.split('/').pop(),
5051
- reason: error.message,
5052
- stack: error.stack,
5053
- };
5054
- const metadata = {
5055
- type: error.name,
5056
- };
5057
-
5058
- Metrics.sendBehavioralMetric(metricName, data, metadata);
5059
- throw new MediaError('Unable to retrieve media streams', error);
5060
- })
5061
- );
5062
- }
5063
-
5064
- return Promise.reject(
5065
- new MediaError('At least one of the mediaDirection value should be true')
5066
- );
5067
- };
5068
-
5069
- /**
5070
- * Checks if the machine has at least one audio or video device
5071
- * @param {Object} options
5072
- * @param {Boolean} options.sendAudio
5073
- * @param {Boolean} options.sendVideo
5074
- * @returns {Object}
5075
- * @memberof Meetings
5076
- */
5077
- getSupportedDevices = ({
5078
- sendAudio = true,
5079
- sendVideo = true,
5080
- }: {
5081
- sendAudio: boolean;
5082
- sendVideo: boolean;
5083
- }) => Media.getSupportedDevice({sendAudio, sendVideo});
5084
-
5085
- /**
5086
- * Get the devices from the Media module
5087
- * @returns {Promise} resolves to an array of DeviceInfo
5088
- * @memberof Meetings
5089
- */
5090
- getDevices = () => Media.getDevices();
5091
-
5092
4458
  /**
5093
4459
  * Handles ROAP_FAILURE event from the webrtc media connection
5094
4460
  *
@@ -5531,13 +4897,14 @@ export default class Meeting extends StatelessWebexPlugin {
5531
4897
  }
5532
4898
 
5533
4899
  /**
5534
- * Creates a webrtc media connection
4900
+ * Creates a webrtc media connection and publishes tracks to it
5535
4901
  *
5536
4902
  * @param {Object} turnServerInfo TURN server information
5537
4903
  * @param {BundlePolicy} [bundlePolicy] Bundle policy settings
5538
4904
  * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
5539
4905
  */
5540
- createMediaConnection(turnServerInfo, bundlePolicy) {
4906
+ private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
4907
+ // create the actual media connection
5541
4908
  const mc = Media.createMediaConnection(this.isMultistream, this.getMediaConnectionDebugId(), {
5542
4909
  mediaProperties: this.mediaProperties,
5543
4910
  remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
@@ -5552,6 +4919,17 @@ export default class Meeting extends StatelessWebexPlugin {
5552
4919
  this.mediaProperties.setMediaPeerConnection(mc);
5553
4920
  this.setupMediaConnectionListeners();
5554
4921
 
4922
+ // publish the tracks
4923
+ if (this.mediaProperties.audioTrack) {
4924
+ await this.publishTrack(this.mediaProperties.audioTrack);
4925
+ }
4926
+ if (this.mediaProperties.videoTrack) {
4927
+ await this.publishTrack(this.mediaProperties.videoTrack);
4928
+ }
4929
+ if (this.mediaProperties.shareTrack) {
4930
+ await this.publishTrack(this.mediaProperties.shareTrack);
4931
+ }
4932
+
5555
4933
  return mc;
5556
4934
  }
5557
4935
 
@@ -5579,24 +4957,21 @@ export default class Meeting extends StatelessWebexPlugin {
5579
4957
  }
5580
4958
 
5581
4959
  /**
5582
- * Specify joining via audio (option: pstn), video, screenshare
5583
- * @param {Object} options A configurable options object for joining a meeting
5584
- * @param {Object} options.resourceId pass the deviceId
5585
- * @param {MediaDirection} options.mediaSettings pass media options
5586
- * @param {MediaStream} options.localStream
5587
- * @param {MediaStream} options.localShare
5588
- * @param {BundlePolicy} options.bundlePolicy bundle policy for multistream meetings
5589
- * @param {RemoteMediaManagerConfig} options.remoteMediaManagerConfig only applies if multistream is enabled
4960
+ * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
4961
+ *
4962
+ * @param {AddMediaOptions} options
5590
4963
  * @returns {Promise}
5591
4964
  * @public
5592
4965
  * @memberof Meeting
5593
4966
  */
5594
- addMedia(options: any = {}) {
4967
+ addMedia(options: AddMediaOptions = {}) {
5595
4968
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5596
4969
 
5597
4970
  let turnDiscoverySkippedReason;
5598
4971
  let turnServerUsed = false;
5599
4972
 
4973
+ LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
4974
+
5600
4975
  if (this.meetingState !== FULL_STATE.ACTIVE) {
5601
4976
  return Promise.reject(new MeetingNotActiveError());
5602
4977
  }
@@ -5610,10 +4985,14 @@ export default class Meeting extends StatelessWebexPlugin {
5610
4985
  return Promise.reject(new UserInLobbyError());
5611
4986
  }
5612
4987
 
5613
- const {localStream, localShare, mediaSettings, remoteMediaManagerConfig, bundlePolicy} =
5614
- options;
5615
-
5616
- LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
4988
+ const {
4989
+ localTracks,
4990
+ audioEnabled = true,
4991
+ videoEnabled = true,
4992
+ receiveShare = true,
4993
+ remoteMediaManagerConfig,
4994
+ bundlePolicy,
4995
+ } = options;
5617
4996
 
5618
4997
  Metrics.postEvent({
5619
4998
  event: eventType.MEDIA_CAPABILITIES,
@@ -5638,34 +5017,61 @@ export default class Meeting extends StatelessWebexPlugin {
5638
5017
  },
5639
5018
  });
5640
5019
 
5641
- return MeetingUtil.validateOptions(options)
5642
- .then(() => {
5643
- this.locusMediaRequest = new LocusMediaRequest(
5644
- {
5645
- correlationId: this.correlationId,
5646
- device: {
5647
- url: this.deviceUrl,
5648
- // @ts-ignore
5649
- deviceType: this.config.deviceType,
5650
- },
5651
- preferTranscoding: !this.isMultistream,
5652
- },
5653
- {
5654
- // @ts-ignore
5655
- parent: this.webex,
5656
- }
5657
- );
5658
- })
5020
+ // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
5021
+ // to avoid doing an extra SDP exchange when they are published for the first time
5022
+ this.mediaProperties.setMediaDirection({
5023
+ sendAudio: audioEnabled,
5024
+ sendVideo: videoEnabled,
5025
+ sendShare: false,
5026
+ receiveAudio: audioEnabled,
5027
+ receiveVideo: videoEnabled,
5028
+ receiveShare,
5029
+ });
5030
+
5031
+ this.locusMediaRequest = new LocusMediaRequest(
5032
+ {
5033
+ correlationId: this.correlationId,
5034
+ device: {
5035
+ url: this.deviceUrl,
5036
+ // @ts-ignore
5037
+ deviceType: this.config.deviceType,
5038
+ },
5039
+ preferTranscoding: !this.isMultistream,
5040
+ },
5041
+ {
5042
+ // @ts-ignore
5043
+ parent: this.webex,
5044
+ }
5045
+ );
5046
+
5047
+ this.audio = createMuteState(AUDIO, this, audioEnabled);
5048
+ this.video = createMuteState(VIDEO, this, videoEnabled);
5049
+
5050
+ this.annotationInfo = localTracks?.annotationInfo;
5051
+
5052
+ const promises = [];
5053
+
5054
+ // setup all the references to local tracks in this.mediaProperties before creating media connection
5055
+ // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5056
+ if (localTracks?.microphone) {
5057
+ promises.push(this.setLocalAudioTrack(localTracks.microphone));
5058
+ }
5059
+ if (localTracks?.camera) {
5060
+ promises.push(this.setLocalVideoTrack(localTracks.camera));
5061
+ }
5062
+ if (localTracks?.screenShare?.video) {
5063
+ promises.push(this.setLocalShareTrack(localTracks.screenShare.video));
5064
+ }
5065
+
5066
+ return Promise.all(promises)
5659
5067
  .then(() => this.roap.doTurnDiscovery(this, false))
5660
- .then((turnDiscoveryObject) => {
5068
+ .then(async (turnDiscoveryObject) => {
5661
5069
  ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5662
5070
  turnServerUsed = !turnDiscoverySkippedReason;
5663
5071
 
5664
5072
  const {turnServerInfo} = turnDiscoveryObject;
5665
5073
 
5666
- this.preMedia(localStream, localShare, mediaSettings);
5667
-
5668
- const mc = this.createMediaConnection(turnServerInfo, bundlePolicy);
5074
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5669
5075
 
5670
5076
  if (this.isMultistream) {
5671
5077
  this.remoteMediaManager = new RemoteMediaManager(
@@ -5690,16 +5096,16 @@ export default class Meeting extends StatelessWebexPlugin {
5690
5096
  EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5691
5097
  );
5692
5098
 
5693
- return this.remoteMediaManager.start().then(() => mc.initiateOffer());
5099
+ await this.remoteMediaManager.start();
5694
5100
  }
5695
5101
 
5696
- return mc.initiateOffer();
5102
+ await mc.initiateOffer();
5697
5103
  })
5698
5104
  .then(() => {
5699
5105
  this.setMercuryListener();
5700
5106
  })
5701
5107
  .then(() =>
5702
- this.getDevices().then((devices) => {
5108
+ getDevices().then((devices) => {
5703
5109
  MeetingUtil.handleDeviceLogging(devices);
5704
5110
  })
5705
5111
  )
@@ -5739,7 +5145,7 @@ export default class Meeting extends StatelessWebexPlugin {
5739
5145
 
5740
5146
  // eslint-disable-next-line func-names
5741
5147
  // eslint-disable-next-line prefer-arrow-callback
5742
- if (this.type === _CALL_) {
5148
+ if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5743
5149
  resolve();
5744
5150
  }
5745
5151
  const joiningTimer = setInterval(() => {
@@ -5764,17 +5170,13 @@ export default class Meeting extends StatelessWebexPlugin {
5764
5170
  })
5765
5171
  )
5766
5172
  .then(() => {
5767
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
5768
- if (mediaSettings && mediaSettings.sendShare && localShare) {
5769
- if (this.state === MEETING_STATE.STATES.JOINED) {
5770
- return this.requestScreenShareFloor();
5771
- }
5772
-
5773
- // When the self state changes to JOINED then request the floor
5774
- this.floorGrantPending = true;
5173
+ if (localTracks?.screenShare?.video) {
5174
+ this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
5175
+ lambda: async () => {
5176
+ return this.requestScreenShareFloor();
5177
+ },
5178
+ });
5775
5179
  }
5776
-
5777
- return {};
5778
5180
  })
5779
5181
  .then(() => this.mediaProperties.getCurrentConnectionType())
5780
5182
  .then((connectionType) => {
@@ -5869,7 +5271,7 @@ export default class Meeting extends StatelessWebexPlugin {
5869
5271
  * @private
5870
5272
  * @memberof Meeting
5871
5273
  */
5872
- private enqueueMediaUpdate(mediaUpdateType: string, options: any) {
5274
+ private enqueueMediaUpdate(mediaUpdateType: string, options: any): Promise<void> {
5873
5275
  if (mediaUpdateType === MEDIA_UPDATE_TYPE.LAMBDA && typeof options?.lambda !== 'function') {
5874
5276
  return Promise.reject(
5875
5277
  new Error('lambda must be specified when enqueuing MEDIA_UPDATE_TYPE.LAMBDA')
@@ -5932,21 +5334,17 @@ export default class Meeting extends StatelessWebexPlugin {
5932
5334
  LoggerProxy.logger.log(
5933
5335
  `Meeting:index#processNextQueuedMediaUpdate --> performing delayed media update type=${mediaUpdateType}`
5934
5336
  );
5337
+ let mediaUpdate = Promise.resolve();
5338
+
5935
5339
  switch (mediaUpdateType) {
5936
- case MEDIA_UPDATE_TYPE.ALL:
5937
- this.updateMedia(options).then(pendingPromiseResolve, pendingPromiseReject);
5938
- break;
5939
- case MEDIA_UPDATE_TYPE.AUDIO:
5940
- this.updateAudio(options).then(pendingPromiseResolve, pendingPromiseReject);
5941
- break;
5942
- case MEDIA_UPDATE_TYPE.VIDEO:
5943
- this.updateVideo(options).then(pendingPromiseResolve, pendingPromiseReject);
5944
- break;
5945
- case MEDIA_UPDATE_TYPE.SHARE:
5946
- this.updateShare(options).then(pendingPromiseResolve, pendingPromiseReject);
5340
+ case MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION:
5341
+ mediaUpdate = this.updateTranscodedMediaConnection();
5947
5342
  break;
5948
5343
  case MEDIA_UPDATE_TYPE.LAMBDA:
5949
- options.lambda().then(pendingPromiseResolve, pendingPromiseReject);
5344
+ mediaUpdate = options.lambda();
5345
+ break;
5346
+ case MEDIA_UPDATE_TYPE.UPDATE_MEDIA:
5347
+ mediaUpdate = this.updateMedia(options);
5950
5348
  break;
5951
5349
  default:
5952
5350
  LoggerProxy.logger.error(
@@ -5954,384 +5352,84 @@ export default class Meeting extends StatelessWebexPlugin {
5954
5352
  );
5955
5353
  break;
5956
5354
  }
5355
+
5356
+ mediaUpdate
5357
+ .then(pendingPromiseResolve, pendingPromiseReject)
5358
+ .then(() => this.processNextQueuedMediaUpdate());
5957
5359
  }
5958
5360
  };
5959
5361
 
5960
5362
  /**
5961
- * A confluence of updateAudio, updateVideo, and updateShare
5962
- * this function re-establishes all of the media streams with new options
5363
+ * Updates the media connection - it allows to enable/disable all audio/video/share in the meeting.
5364
+ * This does not affect the published tracks, so for example if a microphone track is published and
5365
+ * updateMedia({audioEnabled: false}) is called, the audio will not be sent or received anymore,
5366
+ * but the track's "published" state is not changed and when updateMedia({audioEnabled: true}) is called,
5367
+ * the sending of the audio from the same track will resume.
5368
+ *
5963
5369
  * @param {Object} options
5964
- * @param {MediaStream} options.localStream
5965
- * @param {MediaStream} options.localShare
5966
- * @param {MediaDirection} options.mediaSettings
5370
+ * @param {boolean} options.audioEnabled [optional] enables/disables receiving and sending of main audio in the meeting
5371
+ * @param {boolean} options.videoEnabled [optional] enables/disables receiving and sending of main video in the meeting
5372
+ * @param {boolean} options.shareEnabled [optional] enables/disables receiving and sending of screen share in the meeting
5967
5373
  * @returns {Promise}
5968
5374
  * @public
5969
5375
  * @memberof Meeting
5970
5376
  */
5971
- public updateMedia(
5972
- options: {
5973
- localStream?: MediaStream;
5974
- localShare?: MediaStream;
5975
- mediaSettings?: any;
5976
- } = {} as any
5977
- ) {
5978
- const LOG_HEADER = 'Meeting:index#updateMedia -->';
5377
+ public async updateMedia(options: {
5378
+ audioEnabled?: boolean;
5379
+ videoEnabled?: boolean;
5380
+ receiveShare?: boolean;
5381
+ }) {
5382
+ this.checkMediaConnection();
5979
5383
 
5980
- if (!this.canUpdateMedia()) {
5981
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.ALL, options);
5982
- }
5384
+ const {audioEnabled, videoEnabled, receiveShare} = options;
5983
5385
 
5984
- if (this.isMultistream) {
5985
- const audioEnabled = options.mediaSettings?.sendAudio || options.mediaSettings?.receiveAudio;
5986
-
5987
- return this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5988
- }
5989
-
5990
- const {localStream, localShare, mediaSettings} = options;
5991
-
5992
- const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
5993
-
5994
- if (!this.mediaProperties.webrtcMediaConnection) {
5995
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5996
- }
5997
-
5998
- return MeetingUtil.validateOptions(options)
5999
- .then(() => this.preMedia(localStream, localShare, mediaSettings))
6000
- .then(() =>
6001
- this.mediaProperties.webrtcMediaConnection
6002
- .update({
6003
- localTracks: {
6004
- audio: this.mediaProperties.mediaDirection.sendAudio
6005
- ? this.mediaProperties.audioTrack.underlyingTrack
6006
- : null,
6007
- video: this.mediaProperties.mediaDirection.sendVideo
6008
- ? this.mediaProperties.videoTrack.underlyingTrack
6009
- : null,
6010
- screenShareVideo: this.mediaProperties.mediaDirection.sendShare
6011
- ? this.mediaProperties.shareTrack.underlyingTrack
6012
- : null,
6013
- },
6014
- direction: {
6015
- audio: Media.getDirection(
6016
- this.mediaProperties.mediaDirection.receiveAudio,
6017
- this.mediaProperties.mediaDirection.sendAudio
6018
- ),
6019
- video: Media.getDirection(
6020
- this.mediaProperties.mediaDirection.receiveVideo,
6021
- this.mediaProperties.mediaDirection.sendVideo
6022
- ),
6023
- screenShareVideo: Media.getDirection(
6024
- this.mediaProperties.mediaDirection.receiveShare,
6025
- this.mediaProperties.mediaDirection.sendShare
6026
- ),
6027
- },
6028
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6029
- })
6030
- .then(() => {
6031
- LoggerProxy.logger.info(`${LOG_HEADER} webrtcMediaConnection.update done`);
6032
- })
6033
- .catch((error) => {
6034
- LoggerProxy.logger.error(`${LOG_HEADER} Error updatedMedia, `, error);
6035
-
6036
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
6037
- correlation_id: this.correlationId,
6038
- locus_id: this.locusUrl.split('/').pop(),
6039
- reason: error.message,
6040
- stack: error.stack,
6041
- });
6042
-
6043
- throw error;
6044
- })
6045
- // todo: the following code used to be called always after sending the roap message with the new SDP
6046
- // now it's called independently from the roap message (so might be before it), check if that's OK
6047
- // if not, ensure it's called after (now it's called after roap message is sent out, but we're not
6048
- // waiting for sendRoapMediaRequest() to be resolved)
6049
- .then(() =>
6050
- this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
6051
- lambda: () => {
6052
- return Promise.resolve()
6053
- .then(() =>
6054
- this.checkForStopShare(mediaSettings.sendShare, previousSendShareStatus)
6055
- )
6056
- .then((startShare) => {
6057
- // This is a special case if we do an /floor grant followed by /media
6058
- // we actually get a OFFER from the server and a GLAR condition happens
6059
- if (startShare) {
6060
- // We are assuming that the clients are connected when doing an update
6061
- return this.requestScreenShareFloor();
6062
- }
6063
-
6064
- return Promise.resolve();
6065
- });
6066
- },
6067
- })
6068
- )
6069
- );
6070
- }
5386
+ LoggerProxy.logger.log(
5387
+ `Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
5388
+ );
6071
5389
 
6072
- /**
6073
- * Update the main audio track with new parameters
6074
- *
6075
- * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
6076
- *
6077
- * @param {Object} options
6078
- * @param {boolean} options.sendAudio
6079
- * @param {boolean} options.receiveAudio
6080
- * @param {MediaStream} options.stream Stream that contains the audio track to update
6081
- * @returns {Promise}
6082
- * @public
6083
- * @memberof Meeting
6084
- */
6085
- public async updateAudio(options: {
6086
- sendAudio: boolean;
6087
- receiveAudio: boolean;
6088
- stream: MediaStream;
6089
- }) {
6090
- if (this.isMultistream) {
6091
- throw new Error(
6092
- 'updateAudio() is not supported with multistream, use publishTracks/unpublishTracks instead'
6093
- );
6094
- }
6095
5390
  if (!this.canUpdateMedia()) {
6096
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.AUDIO, options);
5391
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
6097
5392
  }
6098
- const {sendAudio, receiveAudio, stream} = options;
6099
- const track = MeetingUtil.getTrack(stream).audioTrack;
6100
-
6101
- if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
6102
- return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
6103
- }
6104
-
6105
- if (!this.mediaProperties.webrtcMediaConnection) {
6106
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
6107
- }
6108
-
6109
- return MeetingUtil.validateOptions({sendAudio, localStream: stream})
6110
- .then(() =>
6111
- this.mediaProperties.webrtcMediaConnection.update({
6112
- localTracks: {audio: track},
6113
- direction: {
6114
- audio: Media.getDirection(receiveAudio, sendAudio),
6115
- video: Media.getDirection(
6116
- this.mediaProperties.mediaDirection.receiveVideo,
6117
- this.mediaProperties.mediaDirection.sendVideo
6118
- ),
6119
- screenShareVideo: Media.getDirection(
6120
- this.mediaProperties.mediaDirection.receiveShare,
6121
- this.mediaProperties.mediaDirection.sendShare
6122
- ),
6123
- },
6124
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6125
- })
6126
- )
6127
- .then(() => {
6128
- this.setLocalAudioTrack(track);
6129
- // todo: maybe this.mediaProperties.mediaDirection could be removed? it's duplicating stuff from webrtcMediaConnection
6130
- this.mediaProperties.mediaDirection.sendAudio = sendAudio;
6131
- this.mediaProperties.mediaDirection.receiveAudio = receiveAudio;
6132
-
6133
- // audio state could be undefined if you have not sent audio before
6134
- this.audio =
6135
- this.audio || createMuteState(AUDIO, this, this.mediaProperties.mediaDirection, true);
6136
- });
6137
- }
6138
5393
 
6139
- /**
6140
- * Update the main video track with new parameters
6141
- *
6142
- * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
6143
- *
6144
- * @param {Object} options
6145
- * @param {boolean} options.sendVideo
6146
- * @param {boolean} options.receiveVideo
6147
- * @param {MediaStream} options.stream Stream that contains the video track to update
6148
- * @returns {Promise}
6149
- * @public
6150
- * @memberof Meeting
6151
- */
6152
- public updateVideo(options: {sendVideo: boolean; receiveVideo: boolean; stream: MediaStream}) {
6153
5394
  if (this.isMultistream) {
6154
- throw new Error(
6155
- 'updateVideo() is not supported with multistream, use publishTracks/unpublishTracks instead'
6156
- );
6157
- }
6158
- if (!this.canUpdateMedia()) {
6159
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.VIDEO, options);
6160
- }
6161
- const {sendVideo, receiveVideo, stream} = options;
6162
- const track = MeetingUtil.getTrack(stream).videoTrack;
5395
+ if (videoEnabled !== undefined) {
5396
+ throw new Error(
5397
+ 'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
5398
+ );
5399
+ }
6163
5400
 
6164
- if (typeof sendVideo !== 'boolean' || typeof receiveVideo !== 'boolean') {
6165
- return Promise.reject(new ParameterError('Pass sendVideo and receiveVideo parameter'));
5401
+ if (receiveShare !== undefined) {
5402
+ throw new Error(
5403
+ 'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5404
+ );
5405
+ }
6166
5406
  }
6167
5407
 
6168
- if (!this.mediaProperties.webrtcMediaConnection) {
6169
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5408
+ if (audioEnabled !== undefined) {
5409
+ this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
5410
+ this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
5411
+ this.audio.enable(this, audioEnabled);
6170
5412
  }
6171
5413
 
6172
- return MeetingUtil.validateOptions({sendVideo, localStream: stream})
6173
- .then(() =>
6174
- this.mediaProperties.webrtcMediaConnection.update({
6175
- localTracks: {video: track},
6176
- direction: {
6177
- audio: Media.getDirection(
6178
- this.mediaProperties.mediaDirection.receiveAudio,
6179
- this.mediaProperties.mediaDirection.sendAudio
6180
- ),
6181
- video: Media.getDirection(receiveVideo, sendVideo),
6182
- screenShareVideo: Media.getDirection(
6183
- this.mediaProperties.mediaDirection.receiveShare,
6184
- this.mediaProperties.mediaDirection.sendShare
6185
- ),
6186
- },
6187
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6188
- })
6189
- )
6190
- .then(() => {
6191
- this.setLocalVideoTrack(track);
6192
- this.mediaProperties.mediaDirection.sendVideo = sendVideo;
6193
- this.mediaProperties.mediaDirection.receiveVideo = receiveVideo;
6194
-
6195
- // video state could be undefined if you have not sent video before
6196
- this.video =
6197
- this.video || createMuteState(VIDEO, this, this.mediaProperties.mediaDirection, true);
6198
- });
6199
- }
6200
-
6201
- /**
6202
- * Internal function when stopping a share stream, cleanup
6203
- * @param {boolean} sendShare
6204
- * @param {boolean} previousShareStatus
6205
- * @returns {Promise}
6206
- * @private
6207
- * @memberof Meeting
6208
- */
6209
- private checkForStopShare(sendShare: boolean, previousShareStatus: boolean) {
6210
- if (sendShare && !previousShareStatus) {
6211
- // When user starts sharing
6212
- return Promise.resolve(true);
5414
+ if (videoEnabled !== undefined) {
5415
+ this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
5416
+ this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
5417
+ this.video.enable(this, videoEnabled);
6213
5418
  }
6214
5419
 
6215
- if (!sendShare && previousShareStatus) {
6216
- // When user stops sharing
6217
- return this.releaseScreenShareFloor().then(() => Promise.resolve(false));
5420
+ if (receiveShare !== undefined) {
5421
+ this.mediaProperties.mediaDirection.receiveShare = receiveShare;
6218
5422
  }
6219
5423
 
6220
- return Promise.resolve();
6221
- }
6222
-
6223
- /**
6224
- * Update the share streams, can be used to start sharing
6225
- *
6226
- * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
6227
- *
6228
- * @param {Object} options
6229
- * @param {boolean} options.sendShare
6230
- * @param {boolean} options.receiveShare
6231
- * @returns {Promise}
6232
- * @public
6233
- * @memberof Meeting
6234
- */
6235
- public updateShare(options: {
6236
- sendShare?: boolean;
6237
- receiveShare?: boolean;
6238
- stream?: any;
6239
- skipSignalingCheck?: boolean;
6240
- }) {
6241
5424
  if (this.isMultistream) {
6242
- throw new Error(
6243
- 'updateShare() is not supported with multistream, use publishTracks/unpublishTracks instead'
6244
- );
6245
- }
6246
- if (!options.skipSignalingCheck && !this.canUpdateMedia()) {
6247
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE, options);
6248
- }
6249
- const {sendShare, receiveShare, stream} = options;
6250
- const track = MeetingUtil.getTrack(stream).videoTrack;
6251
-
6252
- if (typeof sendShare !== 'boolean' || typeof receiveShare !== 'boolean') {
6253
- return Promise.reject(new ParameterError('Pass sendShare and receiveShare parameter'));
6254
- }
6255
-
6256
- if (!this.mediaProperties.webrtcMediaConnection) {
6257
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5425
+ if (audioEnabled !== undefined) {
5426
+ await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5427
+ }
5428
+ } else {
5429
+ await this.updateTranscodedMediaConnection();
6258
5430
  }
6259
5431
 
6260
- const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
6261
-
6262
- this.setLocalShareTrack(track);
6263
-
6264
- return MeetingUtil.validateOptions({sendShare, localShare: stream})
6265
- .then(() => this.checkForStopShare(sendShare, previousSendShareStatus))
6266
- .then((startShare) =>
6267
- this.mediaProperties.webrtcMediaConnection
6268
- .update({
6269
- localTracks: {screenShareVideo: track},
6270
- direction: {
6271
- audio: Media.getDirection(
6272
- this.mediaProperties.mediaDirection.receiveAudio,
6273
- this.mediaProperties.mediaDirection.sendAudio
6274
- ),
6275
- video: Media.getDirection(
6276
- this.mediaProperties.mediaDirection.receiveVideo,
6277
- this.mediaProperties.mediaDirection.sendVideo
6278
- ),
6279
- screenShareVideo: Media.getDirection(receiveShare, sendShare),
6280
- },
6281
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6282
- })
6283
- .then(() =>
6284
- this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
6285
- lambda: async () => {
6286
- if (startShare) {
6287
- return this.requestScreenShareFloor();
6288
- }
6289
-
6290
- return undefined;
6291
- },
6292
- })
6293
- )
6294
- )
6295
- .then(() => {
6296
- this.mediaProperties.mediaDirection.sendShare = sendShare;
6297
- this.mediaProperties.mediaDirection.receiveShare = receiveShare;
6298
- })
6299
- .catch((error) => {
6300
- this.unsetLocalShareTrack();
6301
- throw error;
6302
- });
6303
- }
6304
-
6305
- /**
6306
- * Do all the attach media pre set up before executing the actual attach
6307
- * @param {MediaStream} localStream
6308
- * @param {MediaStream} localShare
6309
- * @param {MediaDirection} mediaSettings
6310
- * @returns {undefined}
6311
- * @private
6312
- * @memberof Meeting
6313
- */
6314
- private preMedia(localStream: MediaStream, localShare: MediaStream, mediaSettings: any) {
6315
- // eslint-disable-next-line no-warning-comments
6316
- // TODO wire into default config. There's currently an issue with the stateless plugin or how we register
6317
- // @ts-ignore - config coming from registerPlugin
6318
- this.mediaProperties.setMediaDirection(Object.assign(this.config.mediaSettings, mediaSettings));
6319
-
6320
- // for multistream, this.audio and this.video are created when publishTracks() is called
6321
- if (!this.isMultistream) {
6322
- this.audio =
6323
- this.audio || createMuteState(AUDIO, this, this.mediaProperties.mediaDirection, true);
6324
- this.video =
6325
- this.video || createMuteState(VIDEO, this, this.mediaProperties.mediaDirection, true);
6326
- }
6327
- // Validation is already done in addMedia so no need to check if the lenght is greater then 0
6328
- this.setLocalTracks(localStream);
6329
- if (this.isMultistream && localShare) {
6330
- throw new Error(
6331
- 'calling addMedia() with localShare stream is not supported when using multistream'
6332
- );
6333
- }
6334
- this.setLocalShareTrack(MeetingUtil.getTrack(localShare).videoTrack);
5432
+ return undefined;
6335
5433
  }
6336
5434
 
6337
5435
  /**
@@ -6573,56 +5671,68 @@ export default class Meeting extends StatelessWebexPlugin {
6573
5671
  * @memberof Meeting
6574
5672
  */
6575
5673
  private requestScreenShareFloor() {
6576
- const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5674
+ if (!this.mediaProperties.shareTrack || !this.mediaProperties.mediaDirection.sendShare) {
5675
+ LoggerProxy.logger.log(
5676
+ `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareTrack=${
5677
+ this.mediaProperties.shareTrack ? 'yes' : 'no'
5678
+ }, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
5679
+ );
6577
5680
 
6578
- if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
6579
- Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5681
+ return Promise.resolve({});
5682
+ }
5683
+ if (this.state === MEETING_STATE.STATES.JOINED) {
5684
+ const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
6580
5685
 
6581
- return this.meetingRequest
6582
- .changeMeetingFloor({
6583
- disposition: FLOOR_ACTION.GRANTED,
6584
- personUrl: this.locusInfo.self.url,
6585
- deviceUrl: this.deviceUrl,
6586
- uri: content.url,
6587
- resourceUrl: this.resourceUrl,
6588
- annotationInfo: this.annotationInfo,
6589
- })
6590
- .then(() => {
6591
- this.isSharing = true;
5686
+ if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5687
+ Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
6592
5688
 
6593
- return Promise.resolve();
6594
- })
6595
- .catch((error) => {
6596
- LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5689
+ return this.meetingRequest
5690
+ .changeMeetingFloor({
5691
+ disposition: FLOOR_ACTION.GRANTED,
5692
+ personUrl: this.locusInfo.self.url,
5693
+ deviceUrl: this.deviceUrl,
5694
+ uri: content.url,
5695
+ resourceUrl: this.resourceUrl,
5696
+ annotationInfo: this.annotationInfo,
5697
+ })
5698
+ .then(() => {
5699
+ this.isSharing = true;
6597
5700
 
6598
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
6599
- correlation_id: this.correlationId,
6600
- locus_id: this.locusUrl.split('/').pop(),
6601
- reason: error.message,
6602
- stack: error.stack,
5701
+ return Promise.resolve();
5702
+ })
5703
+ .catch((error) => {
5704
+ LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5705
+
5706
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
5707
+ correlation_id: this.correlationId,
5708
+ locus_id: this.locusUrl.split('/').pop(),
5709
+ reason: error.message,
5710
+ stack: error.stack,
5711
+ });
5712
+
5713
+ return Promise.reject(error);
6603
5714
  });
5715
+ }
6604
5716
 
6605
- return Promise.reject(error);
6606
- });
5717
+ return Promise.reject(new ParameterError('Cannot share without content.'));
6607
5718
  }
5719
+ this.floorGrantPending = true;
6608
5720
 
6609
- return Promise.reject(new ParameterError('Cannot share without content.'));
5721
+ return Promise.resolve({});
6610
5722
  }
6611
5723
 
6612
5724
  /**
6613
- * Stops the screen share
6614
- * @returns {Promise} see #updateShare
6615
- * @public
6616
- * @memberof Meeting
5725
+ * Requests screen share floor if such request is pending.
5726
+ * It should be called whenever meeting state changes to JOINED
5727
+ *
5728
+ * @returns {void}
6617
5729
  */
6618
- // Internal only, temporarily allows optional params
6619
- // eslint-disable-next-line valid-jsdoc
6620
- public stopShare(options = {}) {
6621
- return this.updateShare({
6622
- sendShare: false,
6623
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
6624
- ...options,
6625
- });
5730
+ private requestScreenShareFloorIfPending() {
5731
+ if (this.floorGrantPending && this.state === MEETING_STATE.STATES.JOINED) {
5732
+ this.requestScreenShareFloor().then(() => {
5733
+ this.floorGrantPending = false;
5734
+ });
5735
+ }
6626
5736
  }
6627
5737
 
6628
5738
  /**
@@ -6634,10 +5744,10 @@ export default class Meeting extends StatelessWebexPlugin {
6634
5744
  private releaseScreenShareFloor() {
6635
5745
  const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
6636
5746
 
6637
- if (content && this.mediaProperties.mediaDirection.sendShare) {
5747
+ if (content) {
6638
5748
  Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
6639
5749
 
6640
- if (content.floor.beneficiary.id !== this.selfId) {
5750
+ if (content.floor?.beneficiary.id !== this.selfId) {
6641
5751
  // remote participant started sharing and caused our sharing to stop, we don't want to send any floor action request in that case
6642
5752
  this.isSharing = false;
6643
5753
 
@@ -6669,7 +5779,10 @@ export default class Meeting extends StatelessWebexPlugin {
6669
5779
  });
6670
5780
  }
6671
5781
 
6672
- return Promise.reject(new ParameterError('Cannot stop share without content'));
5782
+ // according to Locus there is no content, so we don't need to release the floor (it's probably already been released)
5783
+ this.isSharing = false;
5784
+
5785
+ return Promise.resolve();
6673
5786
  }
6674
5787
 
6675
5788
  /**
@@ -6933,62 +6046,6 @@ export default class Meeting extends StatelessWebexPlugin {
6933
6046
  });
6934
6047
  }
6935
6048
 
6936
- /**
6937
- * Sets the quality of the local video stream
6938
- * @param {String} level {LOW|MEDIUM|HIGH}
6939
- * @returns {Promise<MediaStream>} localStream
6940
- */
6941
- setLocalVideoQuality(level: string) {
6942
- LoggerProxy.logger.log(`Meeting:index#setLocalVideoQuality --> Setting quality to ${level}`);
6943
-
6944
- if (!VIDEO_RESOLUTIONS[level]) {
6945
- return this.rejectWithErrorLog(`Meeting:index#setLocalVideoQuality --> ${level} not defined`);
6946
- }
6947
-
6948
- if (!this.mediaProperties.mediaDirection.sendVideo) {
6949
- return this.rejectWithErrorLog(
6950
- 'Meeting:index#setLocalVideoQuality --> unable to change video quality, sendVideo is disabled'
6951
- );
6952
- }
6953
-
6954
- // If level is already the same, don't do anything
6955
- if (level === this.mediaProperties.localQualityLevel) {
6956
- LoggerProxy.logger.warn(
6957
- `Meeting:index#setLocalQualityLevel --> Quality already set to ${level}`
6958
- );
6959
-
6960
- return Promise.resolve();
6961
- }
6962
-
6963
- // Set the quality level in properties
6964
- this.mediaProperties.setLocalQualityLevel(level);
6965
-
6966
- const mediaDirection = {
6967
- sendAudio: this.mediaProperties.mediaDirection.sendAudio,
6968
- sendVideo: this.mediaProperties.mediaDirection.sendVideo,
6969
- sendShare: this.mediaProperties.mediaDirection.sendShare,
6970
- };
6971
-
6972
- // When changing local video quality level
6973
- // Need to stop current track first as chrome doesn't support resolution upscaling(for eg. changing 480p to 720p)
6974
- // Without feeding it a new track
6975
- // open bug link: https://bugs.chromium.org/p/chromium/issues/detail?id=943469
6976
- if (isBrowser('chrome') && this.mediaProperties.videoTrack)
6977
- Media.stopTracks(this.mediaProperties.videoTrack);
6978
-
6979
- return this.getMediaStreams(mediaDirection, VIDEO_RESOLUTIONS[level]).then(
6980
- async ([localStream]) => {
6981
- await this.updateVideo({
6982
- sendVideo: true,
6983
- receiveVideo: true,
6984
- stream: localStream,
6985
- });
6986
-
6987
- return localStream;
6988
- }
6989
- );
6990
- }
6991
-
6992
6049
  /**
6993
6050
  * Sets the quality level of the remote incoming media
6994
6051
  * @param {String} level {LOW|MEDIUM|HIGH}
@@ -7024,129 +6081,7 @@ export default class Meeting extends StatelessWebexPlugin {
7024
6081
  // Set the quality level in properties
7025
6082
  this.mediaProperties.setRemoteQualityLevel(level);
7026
6083
 
7027
- return this.updateMedia({mediaSettings: this.mediaProperties.mediaDirection});
7028
- }
7029
-
7030
- /**
7031
- * This is deprecated, please use setLocalVideoQuality for setting local and setRemoteQualityLevel for remote
7032
- * @param {String} level {LOW|MEDIUM|HIGH}
7033
- * @returns {Promise}
7034
- * @deprecated After FHD support
7035
- */
7036
- setMeetingQuality(level: string) {
7037
- LoggerProxy.logger.log(`Meeting:index#setMeetingQuality --> Setting quality to ${level}`);
7038
-
7039
- if (!QUALITY_LEVELS[level]) {
7040
- return this.rejectWithErrorLog(`Meeting:index#setMeetingQuality --> ${level} not defined`);
7041
- }
7042
-
7043
- const previousLevel = {
7044
- local: this.mediaProperties.localQualityLevel,
7045
- remote: this.mediaProperties.remoteQualityLevel,
7046
- };
7047
-
7048
- // If level is already the same, don't do anything
7049
- if (
7050
- level === this.mediaProperties.localQualityLevel &&
7051
- level === this.mediaProperties.remoteQualityLevel
7052
- ) {
7053
- LoggerProxy.logger.warn(
7054
- `Meeting:index#setMeetingQuality --> Quality already set to ${level}`
7055
- );
7056
-
7057
- return Promise.resolve();
7058
- }
7059
-
7060
- // Determine the direction of our current media
7061
- const {receiveAudio, receiveVideo, sendVideo} = this.mediaProperties.mediaDirection;
7062
-
7063
- return (sendVideo ? this.setLocalVideoQuality(level) : Promise.resolve())
7064
- .then(() =>
7065
- receiveAudio || receiveVideo ? this.setRemoteQualityLevel(level) : Promise.resolve()
7066
- )
7067
- .catch((error) => {
7068
- // From troubleshooting it seems that the stream itself doesn't change the max-fs if the peer connection isn't stable
7069
- this.mediaProperties.setLocalQualityLevel(previousLevel.local);
7070
- this.mediaProperties.setRemoteQualityLevel(previousLevel.remote);
7071
-
7072
- LoggerProxy.logger.error(`Meeting:index#setMeetingQuality --> ${error.message}`);
7073
-
7074
- Metrics.sendBehavioralMetric(
7075
- BEHAVIORAL_METRICS.SET_MEETING_QUALITY_FAILURE,
7076
- {
7077
- correlation_id: this.correlationId,
7078
- locus_id: this.locusUrl.split('/').pop(),
7079
- reason: error.message,
7080
- stack: error.stack,
7081
- },
7082
- {
7083
- type: error.name,
7084
- }
7085
- );
7086
-
7087
- return Promise.reject(error);
7088
- });
7089
- }
7090
-
7091
- /**
7092
- *
7093
- * NOTE: this method can only be used with transcoded meetings, for multistream use publishTrack()
7094
- *
7095
- * @param {Object} options parameter
7096
- * @param {Boolean} options.sendAudio send audio from the display share
7097
- * @param {Boolean} options.sendShare send video from the display share
7098
- * @param {Object} options.sharePreferences
7099
- * @param {MediaTrackConstraints} options.sharePreferences.shareConstraints constraints to apply to video
7100
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints}
7101
- * @param {Boolean} options.sharePreferences.highFrameRate if shareConstraints isn't provided, set default values based off of this boolean
7102
- * @returns {Promise}
7103
- */
7104
- shareScreen(
7105
- options: {
7106
- sendAudio: boolean;
7107
- sendShare: boolean;
7108
- sharePreferences: {shareConstraints: MediaTrackConstraints};
7109
- } = {} as any
7110
- ) {
7111
- LoggerProxy.logger.log('Meeting:index#shareScreen --> Getting local share');
7112
-
7113
- const shareConstraints = {
7114
- sendShare: true,
7115
- sendAudio: false,
7116
- ...options,
7117
- };
7118
-
7119
- // @ts-ignore - config coming from registerPlugin
7120
- return Media.getDisplayMedia(shareConstraints, this.config)
7121
- .then((shareStream) =>
7122
- this.updateShare({
7123
- sendShare: true,
7124
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
7125
- stream: shareStream,
7126
- })
7127
- )
7128
- .catch((error) => {
7129
- // Whenever there is a failure when trying to access a user's display
7130
- // report it as an Behavioral metric
7131
- // This gives visibility into common errors and can help
7132
- // with further troubleshooting
7133
-
7134
- // This metrics will get erros for getDisplayMedia and share errors for now
7135
- // TODO: The getDisplayMedia errors need to be moved inside `media.getDisplayMedia`
7136
- const metricName = BEHAVIORAL_METRICS.GET_DISPLAY_MEDIA_FAILURE;
7137
- const data = {
7138
- correlation_id: this.correlationId,
7139
- locus_id: this.locusUrl.split('/').pop(),
7140
- reason: error.message,
7141
- stack: error.stack,
7142
- };
7143
- const metadata = {
7144
- type: error.name,
7145
- };
7146
-
7147
- Metrics.sendBehavioralMetric(metricName, data, metadata);
7148
- throw new MediaError('Unable to retrieve display media stream', error);
7149
- });
6084
+ return this.updateTranscodedMediaConnection();
7150
6085
  }
7151
6086
 
7152
6087
  /**
@@ -7159,34 +6094,15 @@ export default class Meeting extends StatelessWebexPlugin {
7159
6094
  private handleShareTrackEnded = async () => {
7160
6095
  if (this.wirelessShare) {
7161
6096
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
7162
- } else if (this.isMultistream) {
6097
+ } else {
7163
6098
  try {
7164
- if (this.mediaProperties.mediaDirection.sendShare) {
7165
- await this.releaseScreenShareFloor();
7166
- }
6099
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo: screen share audio (SPARK-399690)
7167
6100
  } catch (error) {
7168
6101
  LoggerProxy.logger.log(
7169
6102
  'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
7170
6103
  error
7171
6104
  );
7172
- } finally {
7173
- // todo: once SPARK-399695 is done, we will be able to just call this.setLocalShareTrack(null); here instead of the next 2 lines:
7174
- this.mediaProperties.shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
7175
- this.mediaProperties.setLocalShareTrack(null);
7176
-
7177
- this.mediaProperties.mediaDirection.sendShare = false;
7178
6105
  }
7179
- } else {
7180
- // Skip checking for a stable peerConnection
7181
- // to allow immediately stopping screenshare
7182
- this.stopShare({
7183
- skipSignalingCheck: true,
7184
- }).catch((error) => {
7185
- LoggerProxy.logger.log(
7186
- 'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
7187
- error
7188
- );
7189
- });
7190
6106
  }
7191
6107
 
7192
6108
  Trigger.trigger(
@@ -7197,7 +6113,7 @@ export default class Meeting extends StatelessWebexPlugin {
7197
6113
  },
7198
6114
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
7199
6115
  {
7200
- type: EVENT_TYPES.LOCAL_SHARE,
6116
+ reason: SHARE_STOPPED_REASON.TRACK_ENDED,
7201
6117
  }
7202
6118
  );
7203
6119
  };
@@ -7235,8 +6151,8 @@ export default class Meeting extends StatelessWebexPlugin {
7235
6151
  * @returns {undefined}
7236
6152
  */
7237
6153
  private handleMediaLogging(mediaProperties: {
7238
- audioTrack: LocalMicrophoneTrack | null;
7239
- videoTrack: LocalCameraTrack | null;
6154
+ audioTrack?: LocalMicrophoneTrack;
6155
+ videoTrack?: LocalCameraTrack;
7240
6156
  }) {
7241
6157
  MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
7242
6158
  MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
@@ -7661,7 +6577,7 @@ export default class Meeting extends StatelessWebexPlugin {
7661
6577
  if (this.mediaProperties?.webrtcMediaConnection) {
7662
6578
  return;
7663
6579
  }
7664
- throw new Error('Webrtc media connection is missing, call addMedia() first');
6580
+ throw new NoMediaEstablishedYetError();
7665
6581
  }
7666
6582
 
7667
6583
  /**
@@ -7690,90 +6606,150 @@ export default class Meeting extends StatelessWebexPlugin {
7690
6606
  }
7691
6607
  }
7692
6608
 
7693
- /**
7694
- * Publishes specified local tracks in the meeting
6609
+ /** Updates the tracks being sent on the transcoded media connection
7695
6610
  *
7696
- * @param {Object} tracks
7697
- * @returns {Promise}
6611
+ * @returns {Promise<void>}
7698
6612
  */
7699
- async publishTracks(tracks: {
7700
- microphone?: LocalMicrophoneTrack;
7701
- camera?: LocalCameraTrack;
7702
- screenShare: {
7703
- audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
7704
- video?: LocalDisplayTrack;
7705
- };
7706
- annotationInfo?: AnnotationInfo;
7707
- }): Promise<void> {
7708
- this.checkMediaConnection();
6613
+ private updateTranscodedMediaConnection(): Promise<void> {
6614
+ const LOG_HEADER = 'Meeting:index#updateTranscodedMediaConnection -->';
7709
6615
 
7710
- if (!this.isMultistream) {
7711
- throw new Error('publishTracks() only supported with multistream');
7712
- }
7713
- this.annotationInfo = tracks.annotationInfo;
7714
- if (tracks.screenShare?.video) {
7715
- const oldTrack = this.mediaProperties.shareTrack;
7716
- const localDisplayTrack = tracks.screenShare?.video;
6616
+ LoggerProxy.logger.info(`${LOG_HEADER} starting`);
7717
6617
 
7718
- oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
6618
+ if (!this.canUpdateMedia()) {
6619
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION, {});
6620
+ }
7719
6621
 
7720
- // we are starting a screen share
7721
- this.mediaProperties.setLocalShareTrack(localDisplayTrack);
6622
+ return this.mediaProperties.webrtcMediaConnection
6623
+ .update({
6624
+ localTracks: {
6625
+ audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
6626
+ video: this.mediaProperties.videoTrack?.underlyingTrack || null,
6627
+ screenShareVideo: this.mediaProperties.shareTrack?.underlyingTrack || null,
6628
+ },
6629
+ direction: {
6630
+ audio: Media.getDirection(
6631
+ true,
6632
+ this.mediaProperties.mediaDirection.receiveAudio,
6633
+ this.mediaProperties.mediaDirection.sendAudio
6634
+ ),
6635
+ video: Media.getDirection(
6636
+ true,
6637
+ this.mediaProperties.mediaDirection.receiveVideo,
6638
+ this.mediaProperties.mediaDirection.sendVideo
6639
+ ),
6640
+ screenShareVideo: Media.getDirection(
6641
+ false,
6642
+ this.mediaProperties.mediaDirection.receiveShare,
6643
+ this.mediaProperties.mediaDirection.sendShare
6644
+ ),
6645
+ },
6646
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6647
+ })
6648
+ .then(() => {
6649
+ LoggerProxy.logger.info(`${LOG_HEADER} done`);
6650
+ })
6651
+ .catch((error) => {
6652
+ LoggerProxy.logger.error(`${LOG_HEADER} Error: `, error);
7722
6653
 
7723
- localDisplayTrack.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
6654
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
6655
+ correlation_id: this.correlationId,
6656
+ locus_id: this.locusUrl.split('/').pop(),
6657
+ reason: error.message,
6658
+ stack: error.stack,
6659
+ });
7724
6660
 
7725
- await this.requestScreenShareFloor();
7726
- this.mediaProperties.mediaDirection.sendShare = true;
6661
+ throw error;
6662
+ });
6663
+ }
7727
6664
 
7728
- await this.mediaProperties.webrtcMediaConnection.publishTrack(
7729
- this.mediaProperties.shareTrack
7730
- );
6665
+ /**
6666
+ * Publishes a track.
6667
+ *
6668
+ * @param {LocalTrack} track to publish
6669
+ * @returns {Promise}
6670
+ */
6671
+ private async publishTrack(track?: LocalTrack) {
6672
+ if (!track) {
6673
+ return;
7731
6674
  }
7732
6675
 
7733
- if (tracks.microphone) {
7734
- const oldTrack = this.mediaProperties.audioTrack;
7735
- const localTrack = tracks.microphone;
6676
+ if (this.mediaProperties.webrtcMediaConnection) {
6677
+ if (this.isMultistream) {
6678
+ await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
6679
+ } else {
6680
+ track.setPublished(true); // for multistream, this call is done by WCME
6681
+ }
6682
+ }
6683
+ }
7736
6684
 
7737
- oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
6685
+ /**
6686
+ * Un-publishes a track.
6687
+ *
6688
+ * @param {LocalTrack} track to unpublish
6689
+ * @returns {Promise}
6690
+ */
6691
+ private async unpublishTrack(track?: LocalTrack) {
6692
+ if (!track) {
6693
+ return;
6694
+ }
7738
6695
 
7739
- this.mediaProperties.setLocalAudioTrack(localTrack);
7740
- this.mediaProperties.mediaDirection.sendAudio = true;
6696
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
6697
+ await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
6698
+ } else {
6699
+ track.setPublished(false); // for multistream, this call is done by WCME
6700
+ }
6701
+ }
7741
6702
 
7742
- // audio mute state could be undefined if you have not sent audio before
7743
- if (!this.audio) {
7744
- this.audio = createMuteState(AUDIO, this, this.mediaProperties.mediaDirection, false);
7745
- } else {
7746
- this.audio.handleLocalTrackChange(this);
7747
- }
6703
+ /**
6704
+ * Publishes specified local tracks in the meeting
6705
+ *
6706
+ * @param {Object} tracks
6707
+ * @returns {Promise}
6708
+ */
6709
+ async publishTracks(tracks: LocalTracks): Promise<void> {
6710
+ this.checkMediaConnection();
7748
6711
 
7749
- localTrack.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
6712
+ this.annotationInfo = tracks.annotationInfo;
7750
6713
 
7751
- await this.mediaProperties.webrtcMediaConnection.publishTrack(
7752
- this.mediaProperties.audioTrack
7753
- );
6714
+ if (
6715
+ !tracks.microphone &&
6716
+ !tracks.camera &&
6717
+ !tracks.screenShare?.audio &&
6718
+ !tracks.screenShare?.video
6719
+ ) {
6720
+ // nothing to do
6721
+ return;
7754
6722
  }
7755
6723
 
7756
- if (tracks.camera) {
7757
- const oldTrack = this.mediaProperties.videoTrack;
7758
- const localTrack = tracks.camera;
6724
+ let floorRequestNeeded = false;
7759
6725
 
7760
- oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
6726
+ if (tracks.screenShare?.video) {
6727
+ await this.setLocalShareTrack(tracks.screenShare?.video);
7761
6728
 
7762
- this.mediaProperties.setLocalVideoTrack(localTrack);
7763
- this.mediaProperties.mediaDirection.sendVideo = true;
6729
+ floorRequestNeeded = true;
6730
+ }
7764
6731
 
7765
- // video state could be undefined if you have not sent video before
7766
- if (!this.video) {
7767
- this.video = createMuteState(VIDEO, this, this.mediaProperties.mediaDirection, false);
7768
- } else {
7769
- this.video.handleLocalTrackChange(this);
7770
- }
6732
+ if (tracks.microphone) {
6733
+ await this.setLocalAudioTrack(tracks.microphone);
6734
+ }
7771
6735
 
7772
- localTrack.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
6736
+ if (tracks.camera) {
6737
+ await this.setLocalVideoTrack(tracks.camera);
6738
+ }
7773
6739
 
7774
- await this.mediaProperties.webrtcMediaConnection.publishTrack(
7775
- this.mediaProperties.videoTrack
7776
- );
6740
+ if (!this.isMultistream) {
6741
+ await this.updateTranscodedMediaConnection();
6742
+ }
6743
+
6744
+ if (floorRequestNeeded) {
6745
+ // we're sending the http request to Locus to request the screen share floor
6746
+ // only after the SDP update, because that's how it's always been done for transcoded meetings
6747
+ // and also if sharing from the start, we need confluence to have been created
6748
+ await this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
6749
+ lambda: async () => {
6750
+ return this.requestScreenShareFloor();
6751
+ },
6752
+ });
7777
6753
  }
7778
6754
  }
7779
6755
 
@@ -7786,43 +6762,31 @@ export default class Meeting extends StatelessWebexPlugin {
7786
6762
  async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
7787
6763
  this.checkMediaConnection();
7788
6764
 
7789
- if (!this.isMultistream) {
7790
- throw new Error('unpublishTracks() is only supported with multistream');
7791
- }
7792
-
7793
- const unpublishPromises = [];
6765
+ const promises = [];
7794
6766
 
7795
6767
  for (const track of tracks.filter((t) => !!t)) {
7796
6768
  if (track === this.mediaProperties.shareTrack) {
7797
- this.mediaProperties.setLocalShareTrack(null);
7798
-
7799
- track.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
7800
-
7801
- this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
7802
- this.mediaProperties.mediaDirection.sendShare = false;
7803
-
7804
- unpublishPromises.push(this.mediaProperties.webrtcMediaConnection.unpublishTrack(track));
6769
+ try {
6770
+ this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
6771
+ } catch (e) {
6772
+ // nothing to do here, error is logged already inside releaseScreenShareFloor()
6773
+ }
6774
+ promises.push(this.setLocalShareTrack(undefined));
7805
6775
  }
7806
6776
 
7807
6777
  if (track === this.mediaProperties.audioTrack) {
7808
- this.mediaProperties.setLocalAudioTrack(null);
7809
- this.mediaProperties.mediaDirection.sendAudio = false;
7810
-
7811
- track.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
7812
-
7813
- unpublishPromises.push(this.mediaProperties.webrtcMediaConnection.unpublishTrack(track));
6778
+ promises.push(this.setLocalAudioTrack(undefined));
7814
6779
  }
7815
6780
 
7816
6781
  if (track === this.mediaProperties.videoTrack) {
7817
- this.mediaProperties.setLocalVideoTrack(null);
7818
- this.mediaProperties.mediaDirection.sendVideo = false;
7819
-
7820
- track.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
7821
-
7822
- unpublishPromises.push(this.mediaProperties.webrtcMediaConnection.unpublishTrack(track));
6782
+ promises.push(this.setLocalVideoTrack(undefined));
7823
6783
  }
7824
6784
  }
7825
6785
 
7826
- await Promise.all(unpublishPromises);
6786
+ if (!this.isMultistream) {
6787
+ promises.push(this.updateTranscodedMediaConnection());
6788
+ }
6789
+
6790
+ await Promise.all(promises);
7827
6791
  }
7828
6792
  }