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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/annotation/index.js +0 -2
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/webex-errors.js +3 -2
  6. package/dist/common/errors/webex-errors.js.map +1 -1
  7. package/dist/config.js +1 -7
  8. package/dist/config.js.map +1 -1
  9. package/dist/constants.js +7 -15
  10. package/dist/constants.js.map +1 -1
  11. package/dist/index.js +6 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/media/index.js +5 -56
  14. package/dist/media/index.js.map +1 -1
  15. package/dist/media/properties.js +15 -93
  16. package/dist/media/properties.js.map +1 -1
  17. package/dist/meeting/index.js +1112 -1873
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +88 -184
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/util.js +1 -23
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meetings/index.js +1 -2
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/reconnection-manager/index.js +153 -134
  26. package/dist/reconnection-manager/index.js.map +1 -1
  27. package/dist/roap/index.js +8 -7
  28. package/dist/roap/index.js.map +1 -1
  29. package/dist/types/common/errors/webex-errors.d.ts +1 -1
  30. package/dist/types/config.d.ts +0 -6
  31. package/dist/types/constants.d.ts +1 -18
  32. package/dist/types/index.d.ts +1 -1
  33. package/dist/types/media/properties.d.ts +16 -38
  34. package/dist/types/meeting/index.d.ts +97 -353
  35. package/dist/types/meeting/muteState.d.ts +36 -38
  36. package/dist/types/meeting/util.d.ts +2 -4
  37. package/package.json +19 -19
  38. package/src/annotation/index.ts +0 -2
  39. package/src/common/errors/webex-errors.ts +6 -2
  40. package/src/config.ts +0 -6
  41. package/src/constants.ts +1 -14
  42. package/src/index.ts +1 -0
  43. package/src/media/index.ts +10 -53
  44. package/src/media/properties.ts +32 -92
  45. package/src/meeting/index.ts +544 -1567
  46. package/src/meeting/muteState.ts +87 -178
  47. package/src/meeting/util.ts +3 -24
  48. package/src/meetings/index.ts +0 -1
  49. package/src/reconnection-manager/index.ts +4 -9
  50. package/src/roap/index.ts +13 -14
  51. package/test/integration/spec/converged-space-meetings.js +59 -3
  52. package/test/integration/spec/journey.js +330 -256
  53. package/test/integration/spec/space-meeting.js +75 -3
  54. package/test/unit/spec/annotation/index.ts +4 -4
  55. package/test/unit/spec/meeting/index.js +811 -1367
  56. package/test/unit/spec/meeting/muteState.js +238 -394
  57. package/test/unit/spec/meeting/utils.js +2 -9
  58. package/test/unit/spec/multistream/receiveSlot.ts +1 -1
  59. package/test/unit/spec/roap/index.ts +2 -2
  60. package/test/utils/integrationTestUtils.js +5 -23
@@ -1,31 +1,18 @@
1
1
  import {ServerMuteReason} from '@webex/media-helpers';
2
2
  import LoggerProxy from '../common/logs/logger-proxy';
3
3
  import ParameterError from '../common/errors/parameter';
4
- import PermissionError from '../common/errors/permission';
5
4
  import MeetingUtil from './util';
6
5
  import {AUDIO, VIDEO} from '../constants';
7
- /* Certain aspects of server interaction for video muting are not implemented as we currently don't support remote muting of video.
8
- If we ever need to support it, search for REMOTE_MUTE_VIDEO_MISSING_IMPLEMENTATION string to find the places that need updating
9
- */
10
6
 
11
7
  // eslint-disable-next-line import/prefer-default-export
12
- export const createMuteState = (type, meeting, mediaDirection, sdkOwnsLocalTrack: boolean) => {
13
- // todo: remove mediaDirection argument (SPARK-399695)
8
+ export const createMuteState = (type, meeting, enabled: boolean) => {
14
9
  // todo: remove the meeting argument (SPARK-399695)
15
- if (type === AUDIO && !mediaDirection.sendAudio) {
16
- return null;
17
- }
18
- if (type === VIDEO && !mediaDirection.sendVideo) {
19
- return null;
20
- }
21
10
 
22
11
  LoggerProxy.logger.info(
23
12
  `Meeting:muteState#createMuteState --> ${type}: creating MuteState for meeting id ${meeting?.id}`
24
13
  );
25
14
 
26
- const muteState = new MuteState(type, meeting, sdkOwnsLocalTrack);
27
-
28
- muteState.init(meeting);
15
+ const muteState = new MuteState(type, meeting, enabled);
29
16
 
30
17
  return muteState;
31
18
  };
@@ -38,11 +25,16 @@ export const createMuteState = (type, meeting, mediaDirection, sdkOwnsLocalTrack
38
25
  This class is exported only for unit tests. It should never be instantiated directly with new MuteState(), instead createMuteState() should be called
39
26
  */
40
27
  export class MuteState {
41
- pendingPromiseReject: any;
42
- pendingPromiseResolve: any;
43
- state: any;
28
+ state: {
29
+ client: {
30
+ enabled: boolean; // indicates if audio/video is enabled at all or not
31
+ localMute: boolean;
32
+ };
33
+ server: {localMute: boolean; remoteMute: boolean; unmuteAllowed: boolean};
34
+ syncToServerInProgress: boolean;
35
+ };
36
+
44
37
  type: any;
45
- sdkOwnsLocalTrack: boolean; // todo: remove this when doing SPARK-399695
46
38
  ignoreMuteStateChange: boolean;
47
39
 
48
40
  /**
@@ -50,18 +42,18 @@ export class MuteState {
50
42
  *
51
43
  * @param {String} type - audio or video
52
44
  * @param {Object} meeting - the meeting object (used for reading current remote mute status)
53
- * @param {boolean} sdkOwnsLocalTrack - if false, then client app owns the local track (for now that's the case only for multistream meetings)
45
+ * @param {boolean} enabled - whether the client audio/video is enabled at all
54
46
  */
55
- constructor(type: string, meeting: any, sdkOwnsLocalTrack: boolean) {
47
+ constructor(type: string, meeting: any, enabled: boolean) {
56
48
  if (type !== AUDIO && type !== VIDEO) {
57
49
  throw new ParameterError('Mute state is designed for handling audio or video only');
58
50
  }
59
51
  this.type = type;
60
- this.sdkOwnsLocalTrack = sdkOwnsLocalTrack;
61
52
  this.ignoreMuteStateChange = false;
62
53
  this.state = {
63
54
  client: {
64
- localMute: false,
55
+ enabled,
56
+ localMute: true,
65
57
  },
66
58
  server: {
67
59
  localMute: true,
@@ -71,9 +63,6 @@ export class MuteState {
71
63
  },
72
64
  syncToServerInProgress: false,
73
65
  };
74
- // these 2 hold the resolve, reject methods for the promise we returned to the client in last handleClientRequest() call
75
- this.pendingPromiseResolve = null;
76
- this.pendingPromiseReject = null;
77
66
  }
78
67
 
79
68
  /**
@@ -83,42 +72,30 @@ export class MuteState {
83
72
  * @returns {void}
84
73
  */
85
74
  public init(meeting: any) {
86
- if (!this.sdkOwnsLocalTrack) {
87
- this.applyUnmuteAllowedToTrack(meeting);
75
+ this.applyUnmuteAllowedToTrack(meeting);
88
76
 
89
- // if we are remotely muted, we need to apply that to the local track now (mute on-entry)
90
- if (this.state.server.remoteMute) {
91
- this.muteLocalTrack(meeting, this.state.server.remoteMute, 'remotelyMuted');
92
- }
77
+ // if we are remotely muted, we need to apply that to the local track now (mute on-entry)
78
+ if (this.state.server.remoteMute) {
79
+ this.muteLocalTrack(meeting, this.state.server.remoteMute, 'remotelyMuted');
80
+ }
93
81
 
94
- const initialMute =
95
- this.type === AUDIO
96
- ? meeting.mediaProperties.audioTrack?.muted
97
- : meeting.mediaProperties.videoTrack?.muted;
82
+ const initialMute =
83
+ this.type === AUDIO
84
+ ? meeting.mediaProperties.audioTrack?.muted
85
+ : meeting.mediaProperties.videoTrack?.muted;
98
86
 
99
- LoggerProxy.logger.info(
100
- `Meeting:muteState#init --> ${this.type}: local track initial mute state: ${initialMute}`
101
- );
102
-
103
- if (initialMute !== undefined) {
104
- this.state.client.localMute = initialMute;
87
+ LoggerProxy.logger.info(
88
+ `Meeting:muteState#init --> ${this.type}: local track initial mute state: ${initialMute}`
89
+ );
105
90
 
106
- this.applyClientStateToServer(meeting);
107
- }
91
+ if (initialMute !== undefined) {
92
+ this.state.client.localMute = initialMute;
108
93
  } else {
109
- // in the mode where sdkOwnsLocalTrack is false (transcoded meetings),
110
- // SDK API currently doesn't allow to start with audio/video muted,
111
- // so we need to apply the initial local mute state (false) to server
112
- this.state.syncToServerInProgress = true;
113
- this.sendLocalMuteRequestToServer(meeting)
114
- .then(() => {
115
- this.state.syncToServerInProgress = false;
116
- })
117
- .catch(() => {
118
- this.state.syncToServerInProgress = false;
119
- // not much we can do here...
120
- });
94
+ // there is no track, so it's like we are locally muted
95
+ // (this is important especially for transcoded meetings, in which the SDP m-line direction always stays "sendrecv")
96
+ this.state.client.localMute = true;
121
97
  }
98
+ this.applyClientStateToServer(meeting);
122
99
  }
123
100
 
124
101
  /**
@@ -134,6 +111,19 @@ export class MuteState {
134
111
  return this.init(meeting);
135
112
  }
136
113
 
114
+ /**
115
+ * Enables/disables audio/video
116
+ *
117
+ * @param {Object} meeting - the meeting object
118
+ * @param {boolean} enable
119
+ * @returns {void}
120
+ */
121
+ public enable(meeting: any, enable: boolean) {
122
+ this.state.client.enabled = enable;
123
+
124
+ this.applyClientStateToServer(meeting);
125
+ }
126
+
137
127
  /**
138
128
  * Mutes/unmutes local track
139
129
  *
@@ -152,49 +142,6 @@ export class MuteState {
152
142
  this.ignoreMuteStateChange = false;
153
143
  }
154
144
 
155
- /**
156
- * Handles mute/unmute request from the client/user. Returns a promise that's resolved once the server update is completed or
157
- * at the point that this request becomese superseded by another client request.
158
- *
159
- * The client doesn't have to wait for the returned promise to resolve before calling handleClientRequest() again. If
160
- * handleClientRequest() is called again before the previous one resolved, the MuteState class will make sure that eventually
161
- * the server state will match the last requested state from the client.
162
- *
163
- * @public
164
- * @memberof MuteState
165
- * @param {Object} [meeting] the meeting object
166
- * @param {Boolean} [mute] true for muting, false for unmuting request
167
- * @returns {Promise}
168
- */
169
- public handleClientRequest(meeting: object, mute?: boolean) {
170
- // todo: this whole method will be removed in SPARK-399695
171
- LoggerProxy.logger.info(
172
- `Meeting:muteState#handleClientRequest --> ${this.type}: user requesting new mute state: ${mute}`
173
- );
174
-
175
- if (!mute && !this.state.server.unmuteAllowed) {
176
- return Promise.reject(
177
- new PermissionError('User is not allowed to unmute self (hard mute feature is being used)')
178
- );
179
- }
180
-
181
- // we don't check if we're already in the same state, because even if we were, we would still have to apply the mute state locally,
182
- // because the client may have changed the audio/video tracks
183
- this.state.client.localMute = mute;
184
-
185
- this.applyClientStateLocally(meeting);
186
-
187
- return new Promise((resolve, reject) => {
188
- if (this.pendingPromiseResolve) {
189
- // resolve the last promise we returned to the client as the client has issued a new request that has superseded the previous one
190
- this.pendingPromiseResolve();
191
- }
192
- this.pendingPromiseResolve = resolve;
193
- this.pendingPromiseReject = reject;
194
- this.applyClientStateToServer(meeting);
195
- });
196
- }
197
-
198
145
  /**
199
146
  * This method should be called when the local track mute state is changed
200
147
  * @public
@@ -211,12 +158,6 @@ export class MuteState {
211
158
  `Meeting:muteState#handleLocalTrackMuteStateChange --> ${this.type}: local track new mute state: ${mute}`
212
159
  );
213
160
 
214
- if (this.pendingPromiseReject) {
215
- LoggerProxy.logger.error(
216
- `Meeting:muteState#handleLocalTrackMuteStateChange --> ${this.type}: Local track mute state change handler called while a client request is handled - this should never happen!, mute state: ${mute}`
217
- );
218
- }
219
-
220
161
  this.state.client.localMute = mute;
221
162
 
222
163
  this.applyClientStateToServer(meeting);
@@ -232,15 +173,16 @@ export class MuteState {
232
173
  * @returns {void}
233
174
  */
234
175
  public applyClientStateLocally(meeting?: any, reason?: ServerMuteReason) {
235
- if (this.sdkOwnsLocalTrack) {
236
- if (this.type === AUDIO) {
237
- meeting.mediaProperties.audioTrack?.setMuted(this.state.client.localMute);
238
- } else {
239
- meeting.mediaProperties.videoTrack?.setMuted(this.state.client.localMute);
240
- }
241
- } else {
242
- this.muteLocalTrack(meeting, this.state.client.localMute, reason);
243
- }
176
+ this.muteLocalTrack(meeting, this.state.client.localMute, reason);
177
+ }
178
+
179
+ /** Returns true if client is locally muted - it takes into account not just the client local mute state,
180
+ * but also whether audio/video is enabled at all
181
+ *
182
+ * @returns {boolean}
183
+ */
184
+ private getClientLocalMuteState() {
185
+ return this.state.client.enabled ? this.state.client.localMute : true;
244
186
  }
245
187
 
246
188
  /**
@@ -251,7 +193,7 @@ export class MuteState {
251
193
  * @memberof MuteState
252
194
  * @returns {void}
253
195
  */
254
- private applyClientStateToServer(meeting?: object) {
196
+ private applyClientStateToServer(meeting?: any) {
255
197
  if (this.state.syncToServerInProgress) {
256
198
  LoggerProxy.logger.info(
257
199
  `Meeting:muteState#applyClientStateToServer --> ${this.type}: request to server in progress, we need to wait for it to complete`
@@ -260,11 +202,12 @@ export class MuteState {
260
202
  return;
261
203
  }
262
204
 
263
- const localMuteRequiresSync = this.state.client.localMute !== this.state.server.localMute;
264
- const remoteMuteRequiresSync = !this.state.client.localMute && this.state.server.remoteMute;
205
+ const localMuteState = this.getClientLocalMuteState();
206
+ const localMuteRequiresSync = localMuteState !== this.state.server.localMute;
207
+ const remoteMuteRequiresSync = !localMuteState && this.state.server.remoteMute;
265
208
 
266
209
  LoggerProxy.logger.info(
267
- `Meeting:muteState#applyClientStateToServer --> ${this.type}: localMuteRequiresSync: ${localMuteRequiresSync} (${this.state.client.localMute} ?= ${this.state.server.localMute})`
210
+ `Meeting:muteState#applyClientStateToServer --> ${this.type}: localMuteRequiresSync: ${localMuteRequiresSync} (${localMuteState} ?= ${this.state.server.localMute})`
268
211
  );
269
212
  LoggerProxy.logger.info(
270
213
  `Meeting:muteState#applyClientStateToServer --> ${this.type}: remoteMuteRequiresSync: ${remoteMuteRequiresSync}`
@@ -275,12 +218,6 @@ export class MuteState {
275
218
  `Meeting:muteState#applyClientStateToServer --> ${this.type}: client state already matching server state, nothing to do`
276
219
  );
277
220
 
278
- if (this.pendingPromiseResolve) {
279
- this.pendingPromiseResolve();
280
- }
281
- this.pendingPromiseResolve = null;
282
- this.pendingPromiseReject = null;
283
-
284
221
  return;
285
222
  }
286
223
 
@@ -308,11 +245,9 @@ export class MuteState {
308
245
  .catch((e) => {
309
246
  this.state.syncToServerInProgress = false;
310
247
 
311
- if (this.pendingPromiseReject) {
312
- this.pendingPromiseReject(e);
313
- }
314
- this.pendingPromiseResolve = null;
315
- this.pendingPromiseReject = null;
248
+ LoggerProxy.logger.warn(
249
+ `Meeting:muteState#applyClientStateToServer --> ${this.type}: error: ${e}`
250
+ );
316
251
 
317
252
  this.applyServerMuteToLocalTrack(meeting, 'clientRequestFailed');
318
253
  });
@@ -327,8 +262,8 @@ export class MuteState {
327
262
  * @returns {Promise}
328
263
  */
329
264
  private sendLocalMuteRequestToServer(meeting?: any) {
330
- const audioMuted = this.type === AUDIO ? this.state.client.localMute : undefined;
331
- const videoMuted = this.type === VIDEO ? this.state.client.localMute : undefined;
265
+ const audioMuted = this.type === AUDIO ? this.getClientLocalMuteState() : undefined;
266
+ const videoMuted = this.type === VIDEO ? this.getClientLocalMuteState() : undefined;
332
267
 
333
268
  LoggerProxy.logger.info(
334
269
  `Meeting:muteState#sendLocalMuteRequestToServer --> ${this.type}: sending local mute (audio=${audioMuted}, video=${videoMuted}) to server`
@@ -366,7 +301,7 @@ export class MuteState {
366
301
  * @returns {Promise}
367
302
  */
368
303
  private sendRemoteMuteRequestToServer(meeting?: any) {
369
- const remoteMute = this.state.client.localMute;
304
+ const remoteMute = this.getClientLocalMuteState();
370
305
 
371
306
  LoggerProxy.logger.info(
372
307
  `Meeting:muteState#sendRemoteMuteRequestToServer --> ${this.type}: sending remote mute:${remoteMute} to server`
@@ -396,12 +331,10 @@ export class MuteState {
396
331
  * @returns {void}
397
332
  */
398
333
  private applyServerMuteToLocalTrack(meeting: any, serverMuteReason: ServerMuteReason) {
399
- if (!this.sdkOwnsLocalTrack) {
400
- const muted = this.state.server.localMute || this.state.server.remoteMute;
334
+ const muted = this.state.server.localMute || this.state.server.remoteMute;
401
335
 
402
- // update the local track mute state, but not this.state.client.localMute
403
- this.muteLocalTrack(meeting, muted, serverMuteReason);
404
- }
336
+ // update the local track mute state, but not this.state.client.localMute
337
+ this.muteLocalTrack(meeting, muted, serverMuteReason);
405
338
  }
406
339
 
407
340
  /** Applies the current value for unmute allowed to the underlying track
@@ -410,12 +343,10 @@ export class MuteState {
410
343
  * @returns {void}
411
344
  */
412
345
  private applyUnmuteAllowedToTrack(meeting: any) {
413
- if (!this.sdkOwnsLocalTrack) {
414
- if (this.type === AUDIO) {
415
- meeting.mediaProperties.audioTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
416
- } else {
417
- meeting.mediaProperties.videoTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
418
- }
346
+ if (this.type === AUDIO) {
347
+ meeting.mediaProperties.audioTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
348
+ } else {
349
+ meeting.mediaProperties.videoTrack?.setUnmuteAllowed(this.state.server.unmuteAllowed);
419
350
  }
420
351
  }
421
352
 
@@ -452,28 +383,27 @@ export class MuteState {
452
383
  * @returns {undefined}
453
384
  */
454
385
  public handleServerLocalUnmuteRequired(meeting?: object) {
455
- LoggerProxy.logger.info(
456
- `Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received -> doing local unmute`
457
- );
386
+ if (!this.state.client.enabled) {
387
+ LoggerProxy.logger.warn(
388
+ `Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received while ${this.type} is disabled -> local unmute will not result in ${this.type} being sent`
389
+ );
390
+ } else {
391
+ LoggerProxy.logger.info(
392
+ `Meeting:muteState#handleServerLocalUnmuteRequired --> ${this.type}: localAudioUnmuteRequired received -> doing local unmute`
393
+ );
394
+ }
458
395
 
459
396
  // todo: I'm seeing "you can now unmute yourself " popup when this happens - but same thing happens on web.w.c so we can ignore for now
460
397
  this.state.server.remoteMute = false;
461
398
  this.state.client.localMute = false;
462
399
 
463
- if (this.pendingPromiseReject) {
464
- this.pendingPromiseReject(
465
- new Error('Server requested local unmute - this overrides any client request in progress')
466
- );
467
- this.pendingPromiseResolve = null;
468
- this.pendingPromiseReject = null;
469
- }
470
-
471
400
  this.applyClientStateLocally(meeting, 'localUnmuteRequired');
472
401
  this.applyClientStateToServer(meeting);
473
402
  }
474
403
 
475
404
  /**
476
- * Returns true if the user is locally or remotely muted
405
+ * Returns true if the user is locally or remotely muted.
406
+ * It only checks the mute status, ignoring the fact whether audio/video is enabled.
477
407
  *
478
408
  * @public
479
409
  * @memberof MuteState
@@ -508,34 +438,13 @@ export class MuteState {
508
438
  }
509
439
 
510
440
  /**
511
- * Returns true if the user is locally muted
441
+ * Returns true if the user is locally muted or audio/video is disabled
512
442
  *
513
443
  * @public
514
444
  * @memberof MuteState
515
445
  * @returns {Boolean}
516
446
  */
517
447
  public isLocallyMuted() {
518
- return this.state.client.localMute || this.state.server.localMute;
519
- }
520
-
521
- /**
522
- * Returns true if the user is muted as a result of the client request (and not remotely muted)
523
- *
524
- * @public
525
- * @memberof MuteState
526
- * @returns {Boolean}
527
- */
528
- public isSelf() {
529
- return this.state.client.localMute && !this.state.server.remoteMute;
530
- }
531
-
532
- // defined for backwards compatibility with the old AudioStateMachine/VideoStateMachine classes
533
- get muted() {
534
- return this.isMuted();
535
- }
536
-
537
- // defined for backwards compatibility with the old AudioStateMachine/VideoStateMachine classes
538
- get self() {
539
- return this.isSelf();
448
+ return this.getClientLocalMuteState();
540
449
  }
541
450
  }
@@ -136,13 +136,10 @@ const MeetingUtil = {
136
136
  : Promise.resolve();
137
137
 
138
138
  return stopStatsAnalyzer
139
- .then(() => meeting.closeLocalStream())
140
- .then(() => meeting.closeLocalShare())
141
139
  .then(() => meeting.closeRemoteTracks())
142
140
  .then(() => meeting.closePeerConnections())
143
141
  .then(() => {
144
- meeting.unsetLocalVideoTrack();
145
- meeting.unsetLocalShareTrack();
142
+ meeting.cleanupLocalTracks();
146
143
  meeting.unsetRemoteTracks();
147
144
  meeting.unsetPeerConnections();
148
145
  meeting.reconnectionManager.cleanUp();
@@ -278,24 +275,6 @@ const MeetingUtil = {
278
275
  });
279
276
  },
280
277
 
281
- validateOptions: (options) => {
282
- const {sendVideo, sendAudio, sendShare, localStream, localShare} = options;
283
-
284
- if (sendVideo && !MeetingUtil.getTrack(localStream).videoTrack) {
285
- return Promise.reject(new ParameterError('please pass valid video streams'));
286
- }
287
-
288
- if (sendAudio && !MeetingUtil.getTrack(localStream).audioTrack) {
289
- return Promise.reject(new ParameterError('please pass valid audio streams'));
290
- }
291
-
292
- if (sendShare && !MeetingUtil.getTrack(localShare).videoTrack) {
293
- return Promise.reject(new ParameterError('please pass valid share streams'));
294
- }
295
-
296
- return Promise.resolve();
297
- },
298
-
299
278
  getTrack: (stream) => {
300
279
  let audioTrack = null;
301
280
  let videoTrack = null;
@@ -395,7 +374,7 @@ const MeetingUtil = {
395
374
  return Promise.reject(new PermissionError('Unlock not allowed, due to joined property.'));
396
375
  },
397
376
 
398
- handleAudioLogging: (audioTrack: LocalMicrophoneTrack | null) => {
377
+ handleAudioLogging: (audioTrack?: LocalMicrophoneTrack) => {
399
378
  const LOG_HEADER = 'MeetingUtil#handleAudioLogging -->';
400
379
 
401
380
  if (audioTrack) {
@@ -407,7 +386,7 @@ const MeetingUtil = {
407
386
  }
408
387
  },
409
388
 
410
- handleVideoLogging: (videoTrack: LocalCameraTrack | null) => {
389
+ handleVideoLogging: (videoTrack?: LocalCameraTrack) => {
411
390
  const LOG_HEADER = 'MeetingUtil#handleVideoLogging -->';
412
391
 
413
392
  if (videoTrack) {
@@ -240,7 +240,6 @@ export default class Meetings extends WebexPlugin {
240
240
  */
241
241
  this.media = {
242
242
  getUserMedia: Media.getUserMedia,
243
- getSupportedDevice: Media.getSupportedDevice,
244
243
  };
245
244
 
246
245
  this.onReady();
@@ -239,13 +239,8 @@ export default class ReconnectionManager {
239
239
  * @private
240
240
  * @memberof ReconnectionManager
241
241
  */
242
- private stopLocalShareTrack(reason: string) {
243
- this.meeting.setLocalShareTrack(null);
244
- this.meeting.isSharing = false;
245
- if (this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
246
- this.meeting.shareStatus = SHARE_STATUS.NO_SHARE;
247
- }
248
- this.meeting.mediaProperties.mediaDirection.sendShare = false;
242
+ private async stopLocalShareTrack(reason: string) {
243
+ await this.meeting.unpublishTracks([this.meeting.mediaProperties.shareTrack]); // todo screen share audio SPARK-399690
249
244
  Trigger.trigger(
250
245
  this.meeting,
251
246
  {
@@ -416,7 +411,7 @@ export default class ReconnectionManager {
416
411
  const wasSharing = this.meeting.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE;
417
412
 
418
413
  if (wasSharing) {
419
- this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEDIA_RECONNECTION);
414
+ await this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEDIA_RECONNECTION);
420
415
  }
421
416
 
422
417
  if (networkDisconnect) {
@@ -507,7 +502,7 @@ export default class ReconnectionManager {
507
502
  LoggerProxy.logger.info('ReconnectionManager:index#rejoinMeeting --> meeting rejoined');
508
503
 
509
504
  if (wasSharing) {
510
- this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEETING_REJOIN);
505
+ await this.stopLocalShareTrack(SHARE_STOPPED_REASON.MEETING_REJOIN);
511
506
  }
512
507
  } catch (joinError) {
513
508
  this.rejoinAttempts += 1;
package/src/roap/index.ts CHANGED
@@ -190,27 +190,26 @@ export default class Roap extends StatelessWebexPlugin {
190
190
  // When reconnecting, it's important that the first roap message being sent out has empty media id.
191
191
  // Normally this is the roap offer, but when TURN discovery is enabled,
192
192
  // then this is the TURN discovery request message
193
- return this.turnDiscovery
194
- .isSkipped(meeting)
195
- .then((isTurnDiscoverySkipped) => {
196
- const sendEmptyMediaId = reconnect && isTurnDiscoverySkipped;
193
+ return this.turnDiscovery.isSkipped(meeting).then((isTurnDiscoverySkipped) => {
194
+ const sendEmptyMediaId = reconnect && isTurnDiscoverySkipped;
197
195
 
198
- return this.roapRequest.sendRoap({
196
+ return this.roapRequest
197
+ .sendRoap({
199
198
  roapMessage,
200
199
  locusSelfUrl: meeting.selfUrl,
201
200
  mediaId: sendEmptyMediaId ? '' : meeting.mediaId,
202
201
  meetingId: meeting.id,
202
+ preferTranscoding: !meeting.isMultistream,
203
203
  locusMediaRequest: meeting.locusMediaRequest,
204
- });
205
- })
206
-
207
- .then(({locus, mediaConnections}) => {
208
- if (mediaConnections) {
209
- meeting.updateMediaConnections(mediaConnections);
210
- }
204
+ })
205
+ .then(({locus, mediaConnections}) => {
206
+ if (mediaConnections) {
207
+ meeting.updateMediaConnections(mediaConnections);
208
+ }
211
209
 
212
- return locus;
213
- });
210
+ return locus;
211
+ });
212
+ });
214
213
  }
215
214
 
216
215
  /**
@@ -3,6 +3,7 @@ import 'jsdom-global/register';
3
3
  import {assert} from '@webex/test-helper-chai';
4
4
  import {skipInNode} from '@webex/test-helper-mocha';
5
5
  import BrowserDetection from '@webex/plugin-meetings/dist/common/browser-detection';
6
+ import {createCameraTrack, createMicrophoneTrack} from '@webex/plugin-meetings';
6
7
 
7
8
  import {MEDIA_SERVERS} from '../../utils/constants';
8
9
  import testUtils from '../../utils/testUtils';
@@ -11,6 +12,21 @@ import webexTestUsers from '../../utils/webex-test-users';
11
12
 
12
13
  config();
13
14
 
15
+ const localTracks = {
16
+ alice: {
17
+ microphone: undefined,
18
+ camera: undefined,
19
+ },
20
+ bob: {
21
+ microphone: undefined,
22
+ camera: undefined,
23
+ },
24
+ chris: {
25
+ microphone: undefined,
26
+ camera: undefined,
27
+ },
28
+ };
29
+
14
30
  skipInNode(describe)('plugin-meetings', () => {
15
31
  const {isBrowser} = BrowserDetection();
16
32
 
@@ -136,6 +152,17 @@ skipInNode(describe)('plugin-meetings', () => {
136
152
  assert.exists(chris.meeting.joinedWith);
137
153
  });
138
154
 
155
+ it('users "alice", "bob", and "chris" create local tracks', async () => {
156
+ localTracks.alice.microphone = await createMicrophoneTrack();
157
+ localTracks.alice.camera = await createCameraTrack();
158
+
159
+ localTracks.bob.microphone = await createMicrophoneTrack();
160
+ localTracks.bob.camera = await createCameraTrack();
161
+
162
+ localTracks.chris.microphone = await createMicrophoneTrack();
163
+ localTracks.chris.camera = await createCameraTrack();
164
+ });
165
+
139
166
  it('users "alice", "bob", and "chris" add media', async () => {
140
167
  mediaReadyListener = testUtils.waitForEvents([
141
168
  {scope: alice.meeting, event: 'media:negotiated'},
@@ -143,9 +170,9 @@ skipInNode(describe)('plugin-meetings', () => {
143
170
  {scope: chris.meeting, event: 'media:negotiated'},
144
171
  ]);
145
172
 
146
- const addMediaAlice = integrationTestUtils.addMedia(alice, {multistream: true});
147
- const addMediaBob = integrationTestUtils.addMedia(bob, {multistream: true});
148
- const addMediaChris = integrationTestUtils.addMedia(chris, {multistream: true});
173
+ const addMediaAlice = integrationTestUtils.addMedia(alice, {multistream: true, microphone: localTracks.alice.microphone, camera: localTracks.alice.camera});
174
+ const addMediaBob = integrationTestUtils.addMedia(bob, {multistream: true, microphone: localTracks.bob.microphone, camera: localTracks.bob.camera});
175
+ const addMediaChris = integrationTestUtils.addMedia(chris, {multistream: true, microphone: localTracks.chris.microphone, camera: localTracks.chris.camera});
149
176
 
150
177
  await addMediaAlice;
151
178
  await addMediaBob;
@@ -172,6 +199,35 @@ skipInNode(describe)('plugin-meetings', () => {
172
199
  assert.equal(bob.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
173
200
  assert.equal(chris.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
174
201
  });
202
+
203
+ it('users "alice", "bob", and "chris" stop their local tracks', () => {
204
+ if (localTracks.alice.microphone) {
205
+ localTracks.alice.microphone.stop();
206
+ localTracks.alice.microphone = undefined;
207
+ }
208
+ if (localTracks.alice.camera) {
209
+ localTracks.alice.camera.stop();
210
+ localTracks.alice.camera = undefined;
211
+ }
212
+
213
+ if (localTracks.bob.microphone) {
214
+ localTracks.bob.microphone.stop();
215
+ localTracks.bob.microphone = undefined;
216
+ }
217
+ if (localTracks.bob.camera) {
218
+ localTracks.bob.camera.stop();
219
+ localTracks.bob.camera = undefined;
220
+ }
221
+
222
+ if (localTracks.chris.microphone) {
223
+ localTracks.chris.microphone.stop();
224
+ localTracks.chris.microphone = undefined;
225
+ }
226
+ if (localTracks.chris.camera) {
227
+ localTracks.chris.camera.stop();
228
+ localTracks.chris.camera = undefined;
229
+ }
230
+ });
175
231
  });
176
232
  }
177
233
  });