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

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 (60) hide show
  1. package/dist/annotation/index.js +0 -2
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/webex-errors.js +3 -2
  6. package/dist/common/errors/webex-errors.js.map +1 -1
  7. package/dist/config.js +1 -7
  8. package/dist/config.js.map +1 -1
  9. package/dist/constants.js +7 -15
  10. package/dist/constants.js.map +1 -1
  11. package/dist/index.js +6 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/media/index.js +5 -56
  14. package/dist/media/index.js.map +1 -1
  15. package/dist/media/properties.js +15 -93
  16. package/dist/media/properties.js.map +1 -1
  17. package/dist/meeting/index.js +1112 -1873
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +88 -184
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/util.js +1 -23
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meetings/index.js +1 -2
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/reconnection-manager/index.js +153 -134
  26. package/dist/reconnection-manager/index.js.map +1 -1
  27. package/dist/roap/index.js +8 -7
  28. package/dist/roap/index.js.map +1 -1
  29. package/dist/types/common/errors/webex-errors.d.ts +1 -1
  30. package/dist/types/config.d.ts +0 -6
  31. package/dist/types/constants.d.ts +1 -18
  32. package/dist/types/index.d.ts +1 -1
  33. package/dist/types/media/properties.d.ts +16 -38
  34. package/dist/types/meeting/index.d.ts +97 -353
  35. package/dist/types/meeting/muteState.d.ts +36 -38
  36. package/dist/types/meeting/util.d.ts +2 -4
  37. package/package.json +19 -19
  38. package/src/annotation/index.ts +0 -2
  39. package/src/common/errors/webex-errors.ts +6 -2
  40. package/src/config.ts +0 -6
  41. package/src/constants.ts +1 -14
  42. package/src/index.ts +1 -0
  43. package/src/media/index.ts +10 -53
  44. package/src/media/properties.ts +32 -92
  45. package/src/meeting/index.ts +544 -1567
  46. package/src/meeting/muteState.ts +87 -178
  47. package/src/meeting/util.ts +3 -24
  48. package/src/meetings/index.ts +0 -1
  49. package/src/reconnection-manager/index.ts +4 -9
  50. package/src/roap/index.ts +13 -14
  51. package/test/integration/spec/converged-space-meetings.js +59 -3
  52. package/test/integration/spec/journey.js +330 -256
  53. package/test/integration/spec/space-meeting.js +75 -3
  54. package/test/unit/spec/annotation/index.ts +4 -4
  55. package/test/unit/spec/meeting/index.js +811 -1367
  56. package/test/unit/spec/meeting/muteState.js +238 -394
  57. package/test/unit/spec/meeting/utils.js +2 -9
  58. package/test/unit/spec/multistream/receiveSlot.ts +1 -1
  59. package/test/unit/spec/roap/index.ts +2 -2
  60. 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
 
@@ -2200,6 +2200,29 @@ export default class Meeting extends StatelessWebexPlugin {
2200
2200
  });
2201
2201
  }
2202
2202
 
2203
+ /**
2204
+ * Trigger annotation info update event
2205
+ @returns {undefined}
2206
+ @param {object} contentShare
2207
+ @param {object} previousContentShare
2208
+ */
2209
+ private triggerAnnotationInfoEvent(contentShare, previousContentShare) {
2210
+ if (
2211
+ contentShare?.annotation &&
2212
+ !isEqual(contentShare?.annotation, previousContentShare?.annotation)
2213
+ ) {
2214
+ Trigger.trigger(
2215
+ this,
2216
+ {
2217
+ file: 'meeting/index',
2218
+ function: 'triggerAnnotationInfoEvent',
2219
+ },
2220
+ EVENT_TRIGGERS.MEETING_UPDATE_ANNOTATION_INFO,
2221
+ contentShare.annotation
2222
+ );
2223
+ }
2224
+ }
2225
+
2203
2226
  /**
2204
2227
  * Set up the locus info media shares listener
2205
2228
  * update content and whiteboard sharing id value for members, and updates the member
@@ -2215,17 +2238,7 @@ export default class Meeting extends StatelessWebexPlugin {
2215
2238
  const previousContentShare = payload.previous?.content;
2216
2239
  const previousWhiteboardShare = payload.previous?.whiteboard;
2217
2240
 
2218
- if (!isEqual(contentShare?.annotation, previousContentShare?.annotation)) {
2219
- Trigger.trigger(
2220
- this,
2221
- {
2222
- file: 'meetings/index',
2223
- function: 'remoteShare',
2224
- },
2225
- EVENT_TRIGGERS.MEETING_UPDATE_ANNOTATION_INFO,
2226
- contentShare.annotation
2227
- );
2228
- }
2241
+ this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2229
2242
 
2230
2243
  if (
2231
2244
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
@@ -2254,29 +2267,8 @@ export default class Meeting extends StatelessWebexPlugin {
2254
2267
  this.selfId === contentShare.beneficiaryId &&
2255
2268
  contentShare.disposition === FLOOR_ACTION.GRANTED
2256
2269
  ) {
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
- }
2270
+ // CONTENT - sharing content local
2271
+ newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2280
2272
  }
2281
2273
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
2282
2274
  // There is no concept of local/remote share for whiteboard
@@ -2372,14 +2364,7 @@ export default class Meeting extends StatelessWebexPlugin {
2372
2364
  this.mediaProperties.mediaDirection?.sendShare &&
2373
2365
  oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
2374
2366
  ) {
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
- }
2367
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2383
2368
  }
2384
2369
  } finally {
2385
2370
  sendStartedSharingRemote();
@@ -2997,13 +2982,13 @@ export default class Meeting extends StatelessWebexPlugin {
2997
2982
  });
2998
2983
  }
2999
2984
  });
3000
- this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
2985
+ this.locusInfo.on(EVENTS.DESTROY_MEETING, async (payload) => {
3001
2986
  // if self state is NOT left
3002
2987
 
3003
2988
  // TODO: Handle sharing and wireless sharing when meeting end
3004
2989
  if (this.wirelessShare) {
3005
2990
  if (this.mediaProperties.shareTrack) {
3006
- this.setLocalShareTrack(null);
2991
+ await this.setLocalShareTrack(undefined);
3007
2992
  }
3008
2993
  }
3009
2994
  // when multiple WEB deviceType join with same user
@@ -3017,18 +3002,18 @@ export default class Meeting extends StatelessWebexPlugin {
3017
3002
  if (payload.shouldLeave) {
3018
3003
  // TODO: We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
3019
3004
 
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
- });
3005
+ try {
3006
+ await this.leave({reason: payload.reason});
3007
+
3008
+ LoggerProxy.logger.warn(
3009
+ 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
3010
+ );
3011
+ } catch (error) {
3012
+ // @ts-ignore
3013
+ LoggerProxy.logger.error(
3014
+ `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
3015
+ );
3016
+ }
3032
3017
  } else {
3033
3018
  LoggerProxy.logger.info(
3034
3019
  'Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON',
@@ -3179,66 +3164,6 @@ export default class Meeting extends StatelessWebexPlugin {
3179
3164
  return this.members;
3180
3165
  }
3181
3166
 
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
3167
  /**
3243
3168
  * Sets the meeting info on the class instance
3244
3169
  * @param {Object} meetingInfo
@@ -3369,21 +3294,6 @@ export default class Meeting extends StatelessWebexPlugin {
3369
3294
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
3370
3295
  }
3371
3296
 
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
3297
  /**
3388
3298
  * Removes remote audio, video and share tracks from class instance's mediaProperties
3389
3299
  * @returns {undefined}
@@ -3468,274 +3378,124 @@ export default class Meeting extends StatelessWebexPlugin {
3468
3378
  }
3469
3379
 
3470
3380
  /**
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
3381
+ * Stores the reference to a new microphone track, sets up the required event listeners
3382
+ * on it, cleans up previous track, etc.
3383
+ *
3384
+ * @param {LocalMicrophoneTrack | null} localTrack local microphone track
3385
+ * @returns {Promise<void>}
3501
3386
  */
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
- }
3387
+ private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
3388
+ const oldTrack = this.mediaProperties.audioTrack;
3506
3389
 
3507
- if (rawAudioTrack) {
3508
- const settings = rawAudioTrack.getSettings();
3390
+ oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3391
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3509
3392
 
3510
- const localMicrophoneTrack = new LocalMicrophoneTrack(
3511
- MediaUtil.createMediaStream([rawAudioTrack])
3512
- );
3393
+ // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
3394
+ this.mediaProperties.setLocalAudioTrack(localTrack);
3513
3395
 
3514
- this.mediaProperties.setMediaSettings('audio', {
3515
- echoCancellation: settings.echoCancellation,
3516
- noiseSuppression: settings.noiseSuppression,
3517
- });
3396
+ this.audio.handleLocalTrackChange(this);
3518
3397
 
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
- }
3398
+ localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3399
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3528
3400
 
3529
- if (emitEvent) {
3530
- this.sendLocalMediaReadyEvent();
3401
+ if (!this.isMultistream || !localTrack) {
3402
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3403
+ await this.unpublishTrack(oldTrack);
3531
3404
  }
3405
+ await this.publishTrack(this.mediaProperties.audioTrack);
3532
3406
  }
3533
3407
 
3534
3408
  /**
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
3409
+ * Stores the reference to a new camera track, sets up the required event listeners
3410
+ * on it, cleans up previous track, etc.
3411
+ *
3412
+ * @param {LocalCameraTrack | null} localTrack local camera track
3413
+ * @returns {Promise<void>}
3541
3414
  */
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();
3415
+ private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
3416
+ const oldTrack = this.mediaProperties.videoTrack;
3549
3417
 
3550
- const {localQualityLevel} = this.mediaProperties;
3418
+ oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3419
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3551
3420
 
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
- }
3561
-
3562
- this.mediaProperties.setLocalVideoTrack(localCameraTrack);
3563
- if (this.video) this.video.applyClientStateLocally(this);
3564
-
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
- }
3421
+ // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
3422
+ this.mediaProperties.setLocalVideoTrack(localTrack);
3602
3423
 
3603
- const {audioTrack, videoTrack} = MeetingUtil.getTrack(localStream);
3424
+ this.video.handleLocalTrackChange(this);
3604
3425
 
3605
- this.setLocalAudioTrack(audioTrack, false);
3606
- this.setLocalVideoTrack(videoTrack, false);
3426
+ localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3427
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3607
3428
 
3608
- this.sendLocalMediaReadyEvent();
3429
+ if (!this.isMultistream || !localTrack) {
3430
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3431
+ await this.unpublishTrack(oldTrack);
3609
3432
  }
3433
+ await this.publishTrack(this.mediaProperties.videoTrack);
3610
3434
  }
3611
3435
 
3612
3436
  /**
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
3437
+ * Stores the reference to a new screen share track, sets up the required event listeners
3438
+ * on it, cleans up previous track, etc.
3439
+ * It also sends the floor grant/release request.
3440
+ *
3441
+ * @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
3442
+ * @returns {Promise<void>}
3618
3443
  */
3619
- public setLocalShareTrack(rawLocalShareTrack: MediaStreamTrack | null) {
3620
- if (rawLocalShareTrack) {
3621
- const settings = rawLocalShareTrack.getSettings();
3444
+ private async setLocalShareTrack(localDisplayTrack?: LocalDisplayTrack) {
3445
+ const oldTrack = this.mediaProperties.shareTrack;
3622
3446
 
3623
- const localDisplayTrack = new LocalDisplayTrack(
3624
- MediaUtil.createMediaStream([rawLocalShareTrack])
3625
- );
3447
+ oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3448
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3626
3449
 
3627
- this.mediaProperties.setLocalShareTrack(localDisplayTrack);
3450
+ this.mediaProperties.setLocalShareTrack(localDisplayTrack);
3628
3451
 
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
- );
3452
+ localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3453
+ localDisplayTrack?.on(
3454
+ LocalTrackEvents.UnderlyingTrackChange,
3455
+ this.underlyingLocalTrackChangeHandler
3456
+ );
3643
3457
 
3644
- localDisplayTrack.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3458
+ this.mediaProperties.mediaDirection.sendShare = !!localDisplayTrack;
3645
3459
 
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);
3460
+ if (!this.isMultistream || !localDisplayTrack) {
3461
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3462
+ await this.unpublishTrack(oldTrack);
3662
3463
  }
3464
+ await this.publishTrack(this.mediaProperties.shareTrack);
3663
3465
  }
3664
3466
 
3665
3467
  /**
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
3468
+ * Removes references to local tracks. This function should be called
3469
+ * on cleanup when we leave the meeting etc.
3470
+ *
3471
+ * @internal
3472
+ * @returns {void}
3671
3473
  */
3672
- public closeLocalStream() {
3673
- const {audioTrack, videoTrack} = this.mediaProperties;
3474
+ public cleanupLocalTracks() {
3475
+ const {audioTrack, videoTrack, shareTrack} = this.mediaProperties;
3674
3476
 
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
- }
3477
+ audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3478
+ audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3693
3479
 
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;
3480
+ videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3481
+ videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3703
3482
 
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
- }
3483
+ shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3484
+ shareTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3720
3485
 
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
- }
3486
+ this.mediaProperties.setLocalAudioTrack(undefined);
3487
+ this.mediaProperties.setLocalVideoTrack(undefined);
3488
+ this.mediaProperties.setLocalShareTrack(undefined);
3730
3489
 
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();
3490
+ this.mediaProperties.mediaDirection.sendAudio = false;
3491
+ this.mediaProperties.mediaDirection.sendVideo = false;
3492
+ this.mediaProperties.mediaDirection.sendShare = false;
3493
+
3494
+ // WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
3495
+ // (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
3496
+ audioTrack?.setPublished(false);
3497
+ videoTrack?.setPublished(false);
3498
+ shareTrack?.setPublished(false);
3739
3499
  }
3740
3500
 
3741
3501
  /**
@@ -3802,6 +3562,9 @@ export default class Meeting extends StatelessWebexPlugin {
3802
3562
  this.mediaProperties.webrtcMediaConnection.close();
3803
3563
  }
3804
3564
 
3565
+ this.audio = null;
3566
+ this.video = null;
3567
+
3805
3568
  return Promise.resolve();
3806
3569
  }
3807
3570
 
@@ -3833,250 +3596,38 @@ export default class Meeting extends StatelessWebexPlugin {
3833
3596
  }
3834
3597
 
3835
3598
  /**
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
3599
+ * Shorthand function to join AND set up media
3600
+ * @param {Object} options - options to join with media
3601
+ * @param {JoinOptions} [options.joinOptions] - see #join()
3602
+ * @param {MediaDirection} [options.mediaOptions] - see #addMedia()
3603
+ * @returns {Promise} -- {join: see join(), media: see addMedia()}
3838
3604
  * @public
3839
3605
  * @memberof Meeting
3606
+ * @example
3607
+ * joinWithMedia({
3608
+ * joinOptions: {resourceId: 'resourceId' },
3609
+ * mediaOptions: {
3610
+ * localTracks: { microphone: microphoneTrack, camera: cameraTrack }
3611
+ * }
3612
+ * })
3840
3613
  */
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
- }
3614
+ public joinWithMedia(
3615
+ options: {
3616
+ joinOptions?: any;
3617
+ mediaOptions?: AddMediaOptions;
3618
+ } = {}
3619
+ ) {
3620
+ const {mediaOptions, joinOptions} = options;
3851
3621
 
3852
- if (!this.audio) {
3853
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
3854
- }
3855
-
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, {
3870
- correlation_id: this.correlationId,
3871
- locus_id: this.locusUrl.split('/').pop(),
3872
- reason: error.message,
3873
- stack: error.stack,
3874
- });
3875
-
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);
3622
+ return this.join(joinOptions)
3623
+ .then((joinResponse) =>
3624
+ this.addMedia(mediaOptions).then((mediaResponse) => ({
3625
+ join: joinResponse,
3626
+ media: mediaResponse,
3627
+ }))
3628
+ )
3629
+ .catch((error) => {
3630
+ LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
4080
3631
 
4081
3632
  Metrics.sendBehavioralMetric(
4082
3633
  BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
@@ -4508,18 +4059,12 @@ export default class Meeting extends StatelessWebexPlugin {
4508
4059
  return Promise.reject(error);
4509
4060
  }
4510
4061
 
4511
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
4512
4062
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
4513
4063
  }
4514
4064
 
4515
4065
  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`;
4066
+ if (!QUALITY_LEVELS[options.meetingQuality.remote]) {
4067
+ const errorMessage = `Meeting:index#join --> ${options.meetingQuality.remote} not defined`;
4523
4068
 
4524
4069
  LoggerProxy.logger.error(errorMessage);
4525
4070
 
@@ -4531,9 +4076,6 @@ export default class Meeting extends StatelessWebexPlugin {
4531
4076
  return Promise.reject(new Error(errorMessage));
4532
4077
  }
4533
4078
 
4534
- if (options.meetingQuality.local) {
4535
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
4536
- }
4537
4079
  if (options.meetingQuality.remote) {
4538
4080
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
4539
4081
  }
@@ -4840,14 +4382,10 @@ export default class Meeting extends StatelessWebexPlugin {
4840
4382
  },
4841
4383
  };
4842
4384
 
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();
4385
+ this.cleanupLocalTracks();
4849
4386
 
4850
- this.mediaProperties.unsetMediaTracks();
4387
+ this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4388
+ this.mediaProperties.unsetRemoteMedia();
4851
4389
 
4852
4390
  // 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
4391
  // once the DX answers we establish connection back the media server with only receiveShare enabled
@@ -4930,165 +4468,6 @@ export default class Meeting extends StatelessWebexPlugin {
4930
4468
  });
4931
4469
  }
4932
4470
 
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
4471
  /**
5093
4472
  * Handles ROAP_FAILURE event from the webrtc media connection
5094
4473
  *
@@ -5531,13 +4910,14 @@ export default class Meeting extends StatelessWebexPlugin {
5531
4910
  }
5532
4911
 
5533
4912
  /**
5534
- * Creates a webrtc media connection
4913
+ * Creates a webrtc media connection and publishes tracks to it
5535
4914
  *
5536
4915
  * @param {Object} turnServerInfo TURN server information
5537
4916
  * @param {BundlePolicy} [bundlePolicy] Bundle policy settings
5538
4917
  * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
5539
4918
  */
5540
- createMediaConnection(turnServerInfo, bundlePolicy) {
4919
+ private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
4920
+ // create the actual media connection
5541
4921
  const mc = Media.createMediaConnection(this.isMultistream, this.getMediaConnectionDebugId(), {
5542
4922
  mediaProperties: this.mediaProperties,
5543
4923
  remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
@@ -5552,6 +4932,17 @@ export default class Meeting extends StatelessWebexPlugin {
5552
4932
  this.mediaProperties.setMediaPeerConnection(mc);
5553
4933
  this.setupMediaConnectionListeners();
5554
4934
 
4935
+ // publish the tracks
4936
+ if (this.mediaProperties.audioTrack) {
4937
+ await this.publishTrack(this.mediaProperties.audioTrack);
4938
+ }
4939
+ if (this.mediaProperties.videoTrack) {
4940
+ await this.publishTrack(this.mediaProperties.videoTrack);
4941
+ }
4942
+ if (this.mediaProperties.shareTrack) {
4943
+ await this.publishTrack(this.mediaProperties.shareTrack);
4944
+ }
4945
+
5555
4946
  return mc;
5556
4947
  }
5557
4948
 
@@ -5579,24 +4970,21 @@ export default class Meeting extends StatelessWebexPlugin {
5579
4970
  }
5580
4971
 
5581
4972
  /**
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
4973
+ * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
4974
+ *
4975
+ * @param {AddMediaOptions} options
5590
4976
  * @returns {Promise}
5591
4977
  * @public
5592
4978
  * @memberof Meeting
5593
4979
  */
5594
- addMedia(options: any = {}) {
4980
+ addMedia(options: AddMediaOptions = {}) {
5595
4981
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5596
4982
 
5597
4983
  let turnDiscoverySkippedReason;
5598
4984
  let turnServerUsed = false;
5599
4985
 
4986
+ LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
4987
+
5600
4988
  if (this.meetingState !== FULL_STATE.ACTIVE) {
5601
4989
  return Promise.reject(new MeetingNotActiveError());
5602
4990
  }
@@ -5610,10 +4998,14 @@ export default class Meeting extends StatelessWebexPlugin {
5610
4998
  return Promise.reject(new UserInLobbyError());
5611
4999
  }
5612
5000
 
5613
- const {localStream, localShare, mediaSettings, remoteMediaManagerConfig, bundlePolicy} =
5614
- options;
5615
-
5616
- LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
5001
+ const {
5002
+ localTracks,
5003
+ audioEnabled = true,
5004
+ videoEnabled = true,
5005
+ receiveShare = true,
5006
+ remoteMediaManagerConfig,
5007
+ bundlePolicy,
5008
+ } = options;
5617
5009
 
5618
5010
  Metrics.postEvent({
5619
5011
  event: eventType.MEDIA_CAPABILITIES,
@@ -5638,34 +5030,61 @@ export default class Meeting extends StatelessWebexPlugin {
5638
5030
  },
5639
5031
  });
5640
5032
 
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
- })
5033
+ // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
5034
+ // to avoid doing an extra SDP exchange when they are published for the first time
5035
+ this.mediaProperties.setMediaDirection({
5036
+ sendAudio: audioEnabled,
5037
+ sendVideo: videoEnabled,
5038
+ sendShare: false,
5039
+ receiveAudio: audioEnabled,
5040
+ receiveVideo: videoEnabled,
5041
+ receiveShare,
5042
+ });
5043
+
5044
+ this.locusMediaRequest = new LocusMediaRequest(
5045
+ {
5046
+ correlationId: this.correlationId,
5047
+ device: {
5048
+ url: this.deviceUrl,
5049
+ // @ts-ignore
5050
+ deviceType: this.config.deviceType,
5051
+ },
5052
+ preferTranscoding: !this.isMultistream,
5053
+ },
5054
+ {
5055
+ // @ts-ignore
5056
+ parent: this.webex,
5057
+ }
5058
+ );
5059
+
5060
+ this.audio = createMuteState(AUDIO, this, audioEnabled);
5061
+ this.video = createMuteState(VIDEO, this, videoEnabled);
5062
+
5063
+ this.annotationInfo = localTracks?.annotationInfo;
5064
+
5065
+ const promises = [];
5066
+
5067
+ // setup all the references to local tracks in this.mediaProperties before creating media connection
5068
+ // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5069
+ if (localTracks?.microphone) {
5070
+ promises.push(this.setLocalAudioTrack(localTracks.microphone));
5071
+ }
5072
+ if (localTracks?.camera) {
5073
+ promises.push(this.setLocalVideoTrack(localTracks.camera));
5074
+ }
5075
+ if (localTracks?.screenShare?.video) {
5076
+ promises.push(this.setLocalShareTrack(localTracks.screenShare.video));
5077
+ }
5078
+
5079
+ return Promise.all(promises)
5659
5080
  .then(() => this.roap.doTurnDiscovery(this, false))
5660
- .then((turnDiscoveryObject) => {
5081
+ .then(async (turnDiscoveryObject) => {
5661
5082
  ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5662
5083
  turnServerUsed = !turnDiscoverySkippedReason;
5663
5084
 
5664
5085
  const {turnServerInfo} = turnDiscoveryObject;
5665
5086
 
5666
- this.preMedia(localStream, localShare, mediaSettings);
5667
-
5668
- const mc = this.createMediaConnection(turnServerInfo, bundlePolicy);
5087
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5669
5088
 
5670
5089
  if (this.isMultistream) {
5671
5090
  this.remoteMediaManager = new RemoteMediaManager(
@@ -5690,16 +5109,16 @@ export default class Meeting extends StatelessWebexPlugin {
5690
5109
  EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5691
5110
  );
5692
5111
 
5693
- return this.remoteMediaManager.start().then(() => mc.initiateOffer());
5112
+ await this.remoteMediaManager.start();
5694
5113
  }
5695
5114
 
5696
- return mc.initiateOffer();
5115
+ await mc.initiateOffer();
5697
5116
  })
5698
5117
  .then(() => {
5699
5118
  this.setMercuryListener();
5700
5119
  })
5701
5120
  .then(() =>
5702
- this.getDevices().then((devices) => {
5121
+ getDevices().then((devices) => {
5703
5122
  MeetingUtil.handleDeviceLogging(devices);
5704
5123
  })
5705
5124
  )
@@ -5739,7 +5158,7 @@ export default class Meeting extends StatelessWebexPlugin {
5739
5158
 
5740
5159
  // eslint-disable-next-line func-names
5741
5160
  // eslint-disable-next-line prefer-arrow-callback
5742
- if (this.type === _CALL_) {
5161
+ if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5743
5162
  resolve();
5744
5163
  }
5745
5164
  const joiningTimer = setInterval(() => {
@@ -5764,17 +5183,13 @@ export default class Meeting extends StatelessWebexPlugin {
5764
5183
  })
5765
5184
  )
5766
5185
  .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;
5186
+ if (localTracks?.screenShare?.video) {
5187
+ this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
5188
+ lambda: async () => {
5189
+ return this.requestScreenShareFloor();
5190
+ },
5191
+ });
5775
5192
  }
5776
-
5777
- return {};
5778
5193
  })
5779
5194
  .then(() => this.mediaProperties.getCurrentConnectionType())
5780
5195
  .then((connectionType) => {
@@ -5869,7 +5284,7 @@ export default class Meeting extends StatelessWebexPlugin {
5869
5284
  * @private
5870
5285
  * @memberof Meeting
5871
5286
  */
5872
- private enqueueMediaUpdate(mediaUpdateType: string, options: any) {
5287
+ private enqueueMediaUpdate(mediaUpdateType: string, options: any): Promise<void> {
5873
5288
  if (mediaUpdateType === MEDIA_UPDATE_TYPE.LAMBDA && typeof options?.lambda !== 'function') {
5874
5289
  return Promise.reject(
5875
5290
  new Error('lambda must be specified when enqueuing MEDIA_UPDATE_TYPE.LAMBDA')
@@ -5932,21 +5347,17 @@ export default class Meeting extends StatelessWebexPlugin {
5932
5347
  LoggerProxy.logger.log(
5933
5348
  `Meeting:index#processNextQueuedMediaUpdate --> performing delayed media update type=${mediaUpdateType}`
5934
5349
  );
5350
+ let mediaUpdate = Promise.resolve();
5351
+
5935
5352
  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);
5353
+ case MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION:
5354
+ mediaUpdate = this.updateTranscodedMediaConnection();
5947
5355
  break;
5948
5356
  case MEDIA_UPDATE_TYPE.LAMBDA:
5949
- options.lambda().then(pendingPromiseResolve, pendingPromiseReject);
5357
+ mediaUpdate = options.lambda();
5358
+ break;
5359
+ case MEDIA_UPDATE_TYPE.UPDATE_MEDIA:
5360
+ mediaUpdate = this.updateMedia(options);
5950
5361
  break;
5951
5362
  default:
5952
5363
  LoggerProxy.logger.error(
@@ -5954,384 +5365,84 @@ export default class Meeting extends StatelessWebexPlugin {
5954
5365
  );
5955
5366
  break;
5956
5367
  }
5368
+
5369
+ mediaUpdate
5370
+ .then(pendingPromiseResolve, pendingPromiseReject)
5371
+ .then(() => this.processNextQueuedMediaUpdate());
5957
5372
  }
5958
5373
  };
5959
5374
 
5960
5375
  /**
5961
- * A confluence of updateAudio, updateVideo, and updateShare
5962
- * this function re-establishes all of the media streams with new options
5376
+ * Updates the media connection - it allows to enable/disable all audio/video/share in the meeting.
5377
+ * This does not affect the published tracks, so for example if a microphone track is published and
5378
+ * updateMedia({audioEnabled: false}) is called, the audio will not be sent or received anymore,
5379
+ * but the track's "published" state is not changed and when updateMedia({audioEnabled: true}) is called,
5380
+ * the sending of the audio from the same track will resume.
5381
+ *
5963
5382
  * @param {Object} options
5964
- * @param {MediaStream} options.localStream
5965
- * @param {MediaStream} options.localShare
5966
- * @param {MediaDirection} options.mediaSettings
5383
+ * @param {boolean} options.audioEnabled [optional] enables/disables receiving and sending of main audio in the meeting
5384
+ * @param {boolean} options.videoEnabled [optional] enables/disables receiving and sending of main video in the meeting
5385
+ * @param {boolean} options.shareEnabled [optional] enables/disables receiving and sending of screen share in the meeting
5967
5386
  * @returns {Promise}
5968
5387
  * @public
5969
5388
  * @memberof Meeting
5970
5389
  */
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 -->';
5390
+ public async updateMedia(options: {
5391
+ audioEnabled?: boolean;
5392
+ videoEnabled?: boolean;
5393
+ receiveShare?: boolean;
5394
+ }) {
5395
+ this.checkMediaConnection();
5979
5396
 
5980
- if (!this.canUpdateMedia()) {
5981
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.ALL, options);
5982
- }
5397
+ const {audioEnabled, videoEnabled, receiveShare} = options;
5983
5398
 
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
- }
5399
+ LoggerProxy.logger.log(
5400
+ `Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
5401
+ );
6071
5402
 
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
5403
  if (!this.canUpdateMedia()) {
6096
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.AUDIO, options);
6097
- }
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'));
5404
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
6103
5405
  }
6104
5406
 
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
-
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
5407
  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;
5408
+ if (videoEnabled !== undefined) {
5409
+ throw new Error(
5410
+ 'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
5411
+ );
5412
+ }
6163
5413
 
6164
- if (typeof sendVideo !== 'boolean' || typeof receiveVideo !== 'boolean') {
6165
- return Promise.reject(new ParameterError('Pass sendVideo and receiveVideo parameter'));
5414
+ if (receiveShare !== undefined) {
5415
+ throw new Error(
5416
+ 'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5417
+ );
5418
+ }
6166
5419
  }
6167
5420
 
6168
- if (!this.mediaProperties.webrtcMediaConnection) {
6169
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5421
+ if (audioEnabled !== undefined) {
5422
+ this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
5423
+ this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
5424
+ this.audio.enable(this, audioEnabled);
6170
5425
  }
6171
5426
 
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);
5427
+ if (videoEnabled !== undefined) {
5428
+ this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
5429
+ this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
5430
+ this.video.enable(this, videoEnabled);
6213
5431
  }
6214
5432
 
6215
- if (!sendShare && previousShareStatus) {
6216
- // When user stops sharing
6217
- return this.releaseScreenShareFloor().then(() => Promise.resolve(false));
5433
+ if (receiveShare !== undefined) {
5434
+ this.mediaProperties.mediaDirection.receiveShare = receiveShare;
6218
5435
  }
6219
5436
 
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
5437
  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'));
5438
+ if (audioEnabled !== undefined) {
5439
+ await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5440
+ }
5441
+ } else {
5442
+ await this.updateTranscodedMediaConnection();
6258
5443
  }
6259
5444
 
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);
5445
+ return undefined;
6335
5446
  }
6336
5447
 
6337
5448
  /**
@@ -6573,56 +5684,68 @@ export default class Meeting extends StatelessWebexPlugin {
6573
5684
  * @memberof Meeting
6574
5685
  */
6575
5686
  private requestScreenShareFloor() {
6576
- const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5687
+ if (!this.mediaProperties.shareTrack || !this.mediaProperties.mediaDirection.sendShare) {
5688
+ LoggerProxy.logger.log(
5689
+ `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareTrack=${
5690
+ this.mediaProperties.shareTrack ? 'yes' : 'no'
5691
+ }, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
5692
+ );
6577
5693
 
6578
- if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
6579
- Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5694
+ return Promise.resolve({});
5695
+ }
5696
+ if (this.state === MEETING_STATE.STATES.JOINED) {
5697
+ const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
6580
5698
 
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;
5699
+ if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5700
+ Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
6592
5701
 
6593
- return Promise.resolve();
6594
- })
6595
- .catch((error) => {
6596
- LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5702
+ return this.meetingRequest
5703
+ .changeMeetingFloor({
5704
+ disposition: FLOOR_ACTION.GRANTED,
5705
+ personUrl: this.locusInfo.self.url,
5706
+ deviceUrl: this.deviceUrl,
5707
+ uri: content.url,
5708
+ resourceUrl: this.resourceUrl,
5709
+ annotationInfo: this.annotationInfo,
5710
+ })
5711
+ .then(() => {
5712
+ this.isSharing = true;
6597
5713
 
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,
5714
+ return Promise.resolve();
5715
+ })
5716
+ .catch((error) => {
5717
+ LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5718
+
5719
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
5720
+ correlation_id: this.correlationId,
5721
+ locus_id: this.locusUrl.split('/').pop(),
5722
+ reason: error.message,
5723
+ stack: error.stack,
5724
+ });
5725
+
5726
+ return Promise.reject(error);
6603
5727
  });
5728
+ }
6604
5729
 
6605
- return Promise.reject(error);
6606
- });
5730
+ return Promise.reject(new ParameterError('Cannot share without content.'));
6607
5731
  }
5732
+ this.floorGrantPending = true;
6608
5733
 
6609
- return Promise.reject(new ParameterError('Cannot share without content.'));
5734
+ return Promise.resolve({});
6610
5735
  }
6611
5736
 
6612
5737
  /**
6613
- * Stops the screen share
6614
- * @returns {Promise} see #updateShare
6615
- * @public
6616
- * @memberof Meeting
5738
+ * Requests screen share floor if such request is pending.
5739
+ * It should be called whenever meeting state changes to JOINED
5740
+ *
5741
+ * @returns {void}
6617
5742
  */
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
- });
5743
+ private requestScreenShareFloorIfPending() {
5744
+ if (this.floorGrantPending && this.state === MEETING_STATE.STATES.JOINED) {
5745
+ this.requestScreenShareFloor().then(() => {
5746
+ this.floorGrantPending = false;
5747
+ });
5748
+ }
6626
5749
  }
6627
5750
 
6628
5751
  /**
@@ -6634,10 +5757,10 @@ export default class Meeting extends StatelessWebexPlugin {
6634
5757
  private releaseScreenShareFloor() {
6635
5758
  const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
6636
5759
 
6637
- if (content && this.mediaProperties.mediaDirection.sendShare) {
5760
+ if (content) {
6638
5761
  Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
6639
5762
 
6640
- if (content.floor.beneficiary.id !== this.selfId) {
5763
+ if (content.floor?.beneficiary.id !== this.selfId) {
6641
5764
  // remote participant started sharing and caused our sharing to stop, we don't want to send any floor action request in that case
6642
5765
  this.isSharing = false;
6643
5766
 
@@ -6669,7 +5792,10 @@ export default class Meeting extends StatelessWebexPlugin {
6669
5792
  });
6670
5793
  }
6671
5794
 
6672
- return Promise.reject(new ParameterError('Cannot stop share without content'));
5795
+ // according to Locus there is no content, so we don't need to release the floor (it's probably already been released)
5796
+ this.isSharing = false;
5797
+
5798
+ return Promise.resolve();
6673
5799
  }
6674
5800
 
6675
5801
  /**
@@ -6933,62 +6059,6 @@ export default class Meeting extends StatelessWebexPlugin {
6933
6059
  });
6934
6060
  }
6935
6061
 
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
6062
  /**
6993
6063
  * Sets the quality level of the remote incoming media
6994
6064
  * @param {String} level {LOW|MEDIUM|HIGH}
@@ -7024,129 +6094,7 @@ export default class Meeting extends StatelessWebexPlugin {
7024
6094
  // Set the quality level in properties
7025
6095
  this.mediaProperties.setRemoteQualityLevel(level);
7026
6096
 
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
- });
6097
+ return this.updateTranscodedMediaConnection();
7150
6098
  }
7151
6099
 
7152
6100
  /**
@@ -7159,34 +6107,15 @@ export default class Meeting extends StatelessWebexPlugin {
7159
6107
  private handleShareTrackEnded = async () => {
7160
6108
  if (this.wirelessShare) {
7161
6109
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
7162
- } else if (this.isMultistream) {
6110
+ } else {
7163
6111
  try {
7164
- if (this.mediaProperties.mediaDirection.sendShare) {
7165
- await this.releaseScreenShareFloor();
7166
- }
6112
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo: screen share audio (SPARK-399690)
7167
6113
  } catch (error) {
7168
6114
  LoggerProxy.logger.log(
7169
6115
  'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
7170
6116
  error
7171
6117
  );
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
6118
  }
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
6119
  }
7191
6120
 
7192
6121
  Trigger.trigger(
@@ -7197,7 +6126,7 @@ export default class Meeting extends StatelessWebexPlugin {
7197
6126
  },
7198
6127
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
7199
6128
  {
7200
- type: EVENT_TYPES.LOCAL_SHARE,
6129
+ reason: SHARE_STOPPED_REASON.TRACK_ENDED,
7201
6130
  }
7202
6131
  );
7203
6132
  };
@@ -7235,8 +6164,8 @@ export default class Meeting extends StatelessWebexPlugin {
7235
6164
  * @returns {undefined}
7236
6165
  */
7237
6166
  private handleMediaLogging(mediaProperties: {
7238
- audioTrack: LocalMicrophoneTrack | null;
7239
- videoTrack: LocalCameraTrack | null;
6167
+ audioTrack?: LocalMicrophoneTrack;
6168
+ videoTrack?: LocalCameraTrack;
7240
6169
  }) {
7241
6170
  MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
7242
6171
  MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
@@ -7661,7 +6590,7 @@ export default class Meeting extends StatelessWebexPlugin {
7661
6590
  if (this.mediaProperties?.webrtcMediaConnection) {
7662
6591
  return;
7663
6592
  }
7664
- throw new Error('Webrtc media connection is missing, call addMedia() first');
6593
+ throw new NoMediaEstablishedYetError();
7665
6594
  }
7666
6595
 
7667
6596
  /**
@@ -7690,90 +6619,150 @@ export default class Meeting extends StatelessWebexPlugin {
7690
6619
  }
7691
6620
  }
7692
6621
 
7693
- /**
7694
- * Publishes specified local tracks in the meeting
6622
+ /** Updates the tracks being sent on the transcoded media connection
7695
6623
  *
7696
- * @param {Object} tracks
7697
- * @returns {Promise}
6624
+ * @returns {Promise<void>}
7698
6625
  */
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();
6626
+ private updateTranscodedMediaConnection(): Promise<void> {
6627
+ const LOG_HEADER = 'Meeting:index#updateTranscodedMediaConnection -->';
7709
6628
 
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;
6629
+ LoggerProxy.logger.info(`${LOG_HEADER} starting`);
7717
6630
 
7718
- oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
6631
+ if (!this.canUpdateMedia()) {
6632
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION, {});
6633
+ }
7719
6634
 
7720
- // we are starting a screen share
7721
- this.mediaProperties.setLocalShareTrack(localDisplayTrack);
6635
+ return this.mediaProperties.webrtcMediaConnection
6636
+ .update({
6637
+ localTracks: {
6638
+ audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
6639
+ video: this.mediaProperties.videoTrack?.underlyingTrack || null,
6640
+ screenShareVideo: this.mediaProperties.shareTrack?.underlyingTrack || null,
6641
+ },
6642
+ direction: {
6643
+ audio: Media.getDirection(
6644
+ true,
6645
+ this.mediaProperties.mediaDirection.receiveAudio,
6646
+ this.mediaProperties.mediaDirection.sendAudio
6647
+ ),
6648
+ video: Media.getDirection(
6649
+ true,
6650
+ this.mediaProperties.mediaDirection.receiveVideo,
6651
+ this.mediaProperties.mediaDirection.sendVideo
6652
+ ),
6653
+ screenShareVideo: Media.getDirection(
6654
+ false,
6655
+ this.mediaProperties.mediaDirection.receiveShare,
6656
+ this.mediaProperties.mediaDirection.sendShare
6657
+ ),
6658
+ },
6659
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6660
+ })
6661
+ .then(() => {
6662
+ LoggerProxy.logger.info(`${LOG_HEADER} done`);
6663
+ })
6664
+ .catch((error) => {
6665
+ LoggerProxy.logger.error(`${LOG_HEADER} Error: `, error);
7722
6666
 
7723
- localDisplayTrack.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
6667
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
6668
+ correlation_id: this.correlationId,
6669
+ locus_id: this.locusUrl.split('/').pop(),
6670
+ reason: error.message,
6671
+ stack: error.stack,
6672
+ });
7724
6673
 
7725
- await this.requestScreenShareFloor();
7726
- this.mediaProperties.mediaDirection.sendShare = true;
6674
+ throw error;
6675
+ });
6676
+ }
7727
6677
 
7728
- await this.mediaProperties.webrtcMediaConnection.publishTrack(
7729
- this.mediaProperties.shareTrack
7730
- );
6678
+ /**
6679
+ * Publishes a track.
6680
+ *
6681
+ * @param {LocalTrack} track to publish
6682
+ * @returns {Promise}
6683
+ */
6684
+ private async publishTrack(track?: LocalTrack) {
6685
+ if (!track) {
6686
+ return;
7731
6687
  }
7732
6688
 
7733
- if (tracks.microphone) {
7734
- const oldTrack = this.mediaProperties.audioTrack;
7735
- const localTrack = tracks.microphone;
6689
+ if (this.mediaProperties.webrtcMediaConnection) {
6690
+ if (this.isMultistream) {
6691
+ await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
6692
+ } else {
6693
+ track.setPublished(true); // for multistream, this call is done by WCME
6694
+ }
6695
+ }
6696
+ }
7736
6697
 
7737
- oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
6698
+ /**
6699
+ * Un-publishes a track.
6700
+ *
6701
+ * @param {LocalTrack} track to unpublish
6702
+ * @returns {Promise}
6703
+ */
6704
+ private async unpublishTrack(track?: LocalTrack) {
6705
+ if (!track) {
6706
+ return;
6707
+ }
7738
6708
 
7739
- this.mediaProperties.setLocalAudioTrack(localTrack);
7740
- this.mediaProperties.mediaDirection.sendAudio = true;
6709
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
6710
+ await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
6711
+ } else {
6712
+ track.setPublished(false); // for multistream, this call is done by WCME
6713
+ }
6714
+ }
7741
6715
 
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
- }
6716
+ /**
6717
+ * Publishes specified local tracks in the meeting
6718
+ *
6719
+ * @param {Object} tracks
6720
+ * @returns {Promise}
6721
+ */
6722
+ async publishTracks(tracks: LocalTracks): Promise<void> {
6723
+ this.checkMediaConnection();
7748
6724
 
7749
- localTrack.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
6725
+ this.annotationInfo = tracks.annotationInfo;
7750
6726
 
7751
- await this.mediaProperties.webrtcMediaConnection.publishTrack(
7752
- this.mediaProperties.audioTrack
7753
- );
6727
+ if (
6728
+ !tracks.microphone &&
6729
+ !tracks.camera &&
6730
+ !tracks.screenShare?.audio &&
6731
+ !tracks.screenShare?.video
6732
+ ) {
6733
+ // nothing to do
6734
+ return;
7754
6735
  }
7755
6736
 
7756
- if (tracks.camera) {
7757
- const oldTrack = this.mediaProperties.videoTrack;
7758
- const localTrack = tracks.camera;
6737
+ let floorRequestNeeded = false;
7759
6738
 
7760
- oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
6739
+ if (tracks.screenShare?.video) {
6740
+ await this.setLocalShareTrack(tracks.screenShare?.video);
7761
6741
 
7762
- this.mediaProperties.setLocalVideoTrack(localTrack);
7763
- this.mediaProperties.mediaDirection.sendVideo = true;
6742
+ floorRequestNeeded = true;
6743
+ }
7764
6744
 
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
- }
6745
+ if (tracks.microphone) {
6746
+ await this.setLocalAudioTrack(tracks.microphone);
6747
+ }
7771
6748
 
7772
- localTrack.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
6749
+ if (tracks.camera) {
6750
+ await this.setLocalVideoTrack(tracks.camera);
6751
+ }
7773
6752
 
7774
- await this.mediaProperties.webrtcMediaConnection.publishTrack(
7775
- this.mediaProperties.videoTrack
7776
- );
6753
+ if (!this.isMultistream) {
6754
+ await this.updateTranscodedMediaConnection();
6755
+ }
6756
+
6757
+ if (floorRequestNeeded) {
6758
+ // we're sending the http request to Locus to request the screen share floor
6759
+ // only after the SDP update, because that's how it's always been done for transcoded meetings
6760
+ // and also if sharing from the start, we need confluence to have been created
6761
+ await this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
6762
+ lambda: async () => {
6763
+ return this.requestScreenShareFloor();
6764
+ },
6765
+ });
7777
6766
  }
7778
6767
  }
7779
6768
 
@@ -7786,43 +6775,31 @@ export default class Meeting extends StatelessWebexPlugin {
7786
6775
  async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
7787
6776
  this.checkMediaConnection();
7788
6777
 
7789
- if (!this.isMultistream) {
7790
- throw new Error('unpublishTracks() is only supported with multistream');
7791
- }
7792
-
7793
- const unpublishPromises = [];
6778
+ const promises = [];
7794
6779
 
7795
6780
  for (const track of tracks.filter((t) => !!t)) {
7796
6781
  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));
6782
+ try {
6783
+ this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
6784
+ } catch (e) {
6785
+ // nothing to do here, error is logged already inside releaseScreenShareFloor()
6786
+ }
6787
+ promises.push(this.setLocalShareTrack(undefined));
7805
6788
  }
7806
6789
 
7807
6790
  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));
6791
+ promises.push(this.setLocalAudioTrack(undefined));
7814
6792
  }
7815
6793
 
7816
6794
  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));
6795
+ promises.push(this.setLocalVideoTrack(undefined));
7823
6796
  }
7824
6797
  }
7825
6798
 
7826
- await Promise.all(unpublishPromises);
6799
+ if (!this.isMultistream) {
6800
+ promises.push(this.updateTranscodedMediaConnection());
6801
+ }
6802
+
6803
+ await Promise.all(promises);
7827
6804
  }
7828
6805
  }