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

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