@webex/plugin-meetings 3.0.0-beta.26 → 3.0.0-beta.28

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.
@@ -5,7 +5,7 @@ import {WebexPlugin} from '@webex/webex-core';
5
5
  import {debounce, forEach} from 'lodash';
6
6
  import LoggerProxy from '../common/logs/logger-proxy';
7
7
 
8
- import {BREAKOUTS, MEETINGS} from '../constants';
8
+ import {BREAKOUTS, MEETINGS, HTTP_VERBS} from '../constants';
9
9
 
10
10
  import Breakout from './breakout';
11
11
  import BreakoutCollection from './collection';
@@ -32,6 +32,7 @@ const Breakouts = WebexPlugin.extend({
32
32
  status: 'string', // only present when in a breakout session
33
33
  url: 'string', // appears from the moment you enable breakouts
34
34
  locusUrl: 'string', // the current locus url
35
+ breakoutServiceUrl: 'string', // the current breakout resouce url
35
36
  },
36
37
 
37
38
  children: {
@@ -89,6 +90,15 @@ const Breakouts = WebexPlugin.extend({
89
90
  this.set('locusUrl', locusUrl);
90
91
  },
91
92
 
93
+ /**
94
+ * Update the current breakout resouce url
95
+ * @param {string} breakoutServiceUrl
96
+ * @returns {void}
97
+ */
98
+ breakoutServiceUrlUpdate(breakoutServiceUrl) {
99
+ this.set('breakoutServiceUrl', `${breakoutServiceUrl}/breakout/`);
100
+ },
101
+
92
102
  /**
93
103
  * The initial roster lists need to be queried because you don't
94
104
  * get a breakout.roster event when you join the meeting
@@ -184,6 +194,8 @@ const Breakouts = WebexPlugin.extend({
184
194
  [BREAKOUTS.SESSION_STATES.ASSIGNED_CURRENT]: false,
185
195
  [BREAKOUTS.SESSION_STATES.REQUESTED]: false,
186
196
  });
197
+
198
+ this.set('enableBreakoutSession', params.enableBreakoutSession);
187
199
  },
188
200
 
189
201
  /**
@@ -220,6 +232,66 @@ const Breakouts = WebexPlugin.extend({
220
232
 
221
233
  this.breakouts.set(Object.values(breakouts));
222
234
  },
235
+
236
+ /**
237
+ * Make enable breakout resource
238
+ * @returns {Promise}
239
+ */
240
+ enableBreakouts() {
241
+ if (this.breakoutServiceUrl) {
242
+ // @ts-ignore
243
+ return this.webex
244
+ .request({
245
+ method: HTTP_VERBS.POST,
246
+ uri: this.breakoutServiceUrl,
247
+ body: {
248
+ locusUrl: this.locusUrl,
249
+ },
250
+ })
251
+ .catch((err) => {
252
+ LoggerProxy.logger.error(
253
+ `Meeting:request#touchBreakout --> Error provisioning error ${err}`
254
+ );
255
+ throw err;
256
+ });
257
+ }
258
+
259
+ return Promise.reject(new Error(`enableBreakouts: the breakoutServiceUrl is empty`));
260
+ },
261
+
262
+ /**
263
+ * Make the meeting enbale or disable breakout session
264
+ * @param {boolean} enable
265
+ * @returns {Promise}
266
+ */
267
+ async toggleBreakout(enable) {
268
+ if (this.enableBreakoutSession === undefined) {
269
+ const info = await this.enableBreakouts();
270
+ if (!enable) {
271
+ // if enable is false, updateBreakout set the param then set enableBreakoutSession as false
272
+ this.updateBreakout(info.body);
273
+ await this.doToggleBreakout(enable);
274
+ }
275
+ } else {
276
+ await this.doToggleBreakout(enable);
277
+ }
278
+ },
279
+
280
+ /**
281
+ * do toggle meeting breakout session enable or disable
282
+ * @param {boolean} enable
283
+ * @returns {Promise}
284
+ */
285
+ doToggleBreakout(enable) {
286
+ // @ts-ignore
287
+ return this.webex.request({
288
+ method: HTTP_VERBS.PUT,
289
+ uri: this.url,
290
+ body: {
291
+ enableBreakoutSession: enable,
292
+ },
293
+ });
294
+ },
223
295
  });
224
296
 
225
297
  export default Breakouts;
@@ -2219,6 +2219,7 @@ export default class Meeting extends StatelessWebexPlugin {
2219
2219
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_SERVICES, (payload) => {
2220
2220
  this.recordingController.setServiceUrl(payload?.services?.record?.url);
2221
2221
  this.recordingController.setSessionId(this.locusInfo?.fullState?.sessionId);
2222
+ this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2222
2223
  });
2223
2224
  }
2224
2225
 
@@ -58,12 +58,36 @@ MeetingsUtil.handleRoapMercury = (envelope, meetingCollection) => {
58
58
  errorCause,
59
59
  };
60
60
 
61
+ const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
62
+
61
63
  meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
64
+
65
+ if (mediaServer) {
66
+ meeting.mediaProperties.webrtcMediaConnection.mediaServer = mediaServer;
67
+ }
62
68
  }
63
69
  }
64
70
  }
65
71
  };
66
72
 
73
+ MeetingsUtil.getMediaServer = (sdp) => {
74
+ let mediaServer;
75
+
76
+ // Attempt to collect the media server from the roap message.
77
+ try {
78
+ mediaServer = sdp
79
+ .split('\r\n')
80
+ .find((line) => line.startsWith('o='))
81
+ .split(' ')
82
+ .shift()
83
+ .replace('o=', '');
84
+ } catch {
85
+ mediaServer = undefined;
86
+ }
87
+
88
+ return mediaServer;
89
+ };
90
+
67
91
  MeetingsUtil.checkForCorrelationId = (deviceUrl, locus) => {
68
92
  let devices = [];
69
93
 
@@ -296,6 +296,8 @@ export default class Members extends StatelessWebexPlugin {
296
296
  const delta = this.handleLocusInfoUpdatedParticipants(payload);
297
297
  const full = this.handleMembersUpdate(delta); // SDK should propagate the full list for both delta and non delta updates
298
298
 
299
+ this.receiveSlotManager.updateMemberIds();
300
+
299
301
  Trigger.trigger(
300
302
  this,
301
303
  {
@@ -91,7 +91,7 @@ export class ReceiveSlot extends EventsScope {
91
91
  /**
92
92
  * registers event handlers with the underlying ReceiveSlot
93
93
  */
94
- setupEventListeners() {
94
+ private setupEventListeners() {
95
95
  const scope = {
96
96
  file: 'meeting/receiveSlot',
97
97
  function: 'setupEventListeners',
@@ -116,6 +116,13 @@ export class ReceiveSlot extends EventsScope {
116
116
  );
117
117
  }
118
118
 
119
+ /** Tries to find the member id for this receive slot if it hasn't got one */
120
+ public findMemberId() {
121
+ if (this.#memberId === undefined && this.#csi) {
122
+ this.#memberId = this.findMemberIdCallback(this.#csi);
123
+ }
124
+ }
125
+
119
126
  /**
120
127
  * The MediaStream object associated with this slot.
121
128
  *
@@ -141,4 +141,16 @@ export class ReceiveSlotManager {
141
141
  numFreeSlots,
142
142
  };
143
143
  }
144
+
145
+ /**
146
+ * Tries to find the member id on all allocated receive slots
147
+ * This function should be called when new members are added to the meeting.
148
+ */
149
+ updateMemberIds() {
150
+ Object.keys(this.allocatedSlots).forEach((key) => {
151
+ this.allocatedSlots[key].forEach((slot: ReceiveSlot) => {
152
+ slot.findMemberId();
153
+ });
154
+ });
155
+ }
144
156
  }
@@ -0,0 +1,176 @@
1
+ import { config } from 'dotenv';
2
+ import 'jsdom-global/register';
3
+ import {assert} from '@webex/test-helper-chai';
4
+ import {skipInNode} from '@webex/test-helper-mocha';
5
+ import BrowserDetection from '@webex/plugin-meetings/dist/common/browser-detection';
6
+
7
+ import {MEDIA_SERVERS} from '../../utils/constants';
8
+ import testUtils from '../../utils/testUtils';
9
+ import webexTestUsers from '../../utils/webex-test-users';
10
+
11
+ config();
12
+
13
+ skipInNode(describe)('plugin-meetings', () => {
14
+ const {isBrowser} = BrowserDetection();
15
+
16
+ // `addMedia()` fails on FF, this needs to be debuged and fixed in a later change
17
+ if (!isBrowser('firefox')) {
18
+ describe('converged-space-meeting', () => {
19
+ let shouldSkip = false;
20
+ let users, alice, bob, chris;
21
+ let meeting = null;
22
+ let space = null;
23
+ let mediaReadyListener = null;
24
+
25
+ before('setup users', async () => {
26
+ const userSet = await webexTestUsers.generateTestUsers({
27
+ count: 3,
28
+ whistler: process.env.WHISTLER || process.env.JENKINS,
29
+ config
30
+ });
31
+
32
+ users = userSet;
33
+ alice = users[0];
34
+ bob = users[1];
35
+ chris = users[2];
36
+ alice.name = 'alice';
37
+ bob.name = 'bob';
38
+ chris.name = 'chris';
39
+
40
+ const aliceSync = testUtils.syncAndEndMeeting(alice);
41
+ const bobSync = testUtils.syncAndEndMeeting(bob);
42
+ const chrisSync = testUtils.syncAndEndMeeting(chris);
43
+
44
+ await aliceSync;
45
+ await bobSync;
46
+ await chrisSync;
47
+ });
48
+
49
+ // Skip a test in this series if one failed.
50
+ // This beforeEach() instance function must use the `function` declaration to preserve the
51
+ // `this` context. `() => {}` will not generate the correct `this` context
52
+ beforeEach('check if should skip test', function() {
53
+ if (shouldSkip) {
54
+ this.skip();
55
+ }
56
+ });
57
+
58
+ // Store to the describe scope if a test has failed for skipping.
59
+ // This beforeEach() instance function must use the `function` declaration to preserve the
60
+ // `this` context. `() => {}` will not generate the correct `this` context
61
+ afterEach('check if test failed', function() {
62
+ if (this.currentTest.state === 'failed') {
63
+ shouldSkip = true;
64
+ }
65
+ });
66
+
67
+ it('user "alice" starts a space', async () => {
68
+ const conversation = await alice.webex.internal.conversation.create({
69
+ participants: [bob, chris],
70
+ });
71
+
72
+ assert.lengthOf(conversation.participants.items, 3);
73
+ assert.lengthOf(conversation.activities.items, 1);
74
+
75
+ space = conversation;
76
+
77
+ const destinationWithType = await alice.webex.meetings.meetingInfo.fetchMeetingInfo(space.url, 'CONVERSATION_URL');
78
+ const destinationWithoutType = await alice.webex.meetings.meetingInfo.fetchMeetingInfo(space.url);
79
+
80
+ assert.exists(destinationWithoutType);
81
+ assert.exists(destinationWithType);
82
+ assert.exists(destinationWithoutType.body.meetingNumber);
83
+ assert.exists(destinationWithType.body.meetingNumber);
84
+ });
85
+
86
+ it('user "alice" starts a meeting', async () => {
87
+ const wait = testUtils.waitForEvents([{
88
+ scope: alice.webex.meetings,
89
+ event: 'meeting:added',
90
+ user: alice,
91
+ }]);
92
+
93
+ const createdMeeting = await testUtils.delayedPromise(alice.webex.meetings.create(space.url));
94
+
95
+ await wait;
96
+
97
+ assert.exists(createdMeeting);
98
+
99
+ meeting = createdMeeting;
100
+ });
101
+
102
+ it('user "alice" joins the meeting', async () => {
103
+ const wait = testUtils.waitForEvents([
104
+ {scope: bob.webex.meetings, event: 'meeting:added', user: bob},
105
+ {scope: chris.webex.meetings, event: 'meeting:added', user: chris},
106
+ ]);
107
+
108
+ await testUtils.delayedPromise(alice.meeting.join({enableMultistream: true}));
109
+
110
+ await wait;
111
+
112
+ assert.isTrue(!!alice.webex.meetings.meetingCollection.meetings[meeting.id].joinedWith);
113
+ });
114
+
115
+ it('users "bob" and "chris" join the meeting', async () => {
116
+ await testUtils.waitForStateChange(alice.meeting, 'JOINED');
117
+
118
+ const bobIdle = testUtils.waitForStateChange(bob.meeting, 'IDLE');
119
+ const chrisIdle = testUtils.waitForStateChange(chris.meeting, 'IDLE');
120
+
121
+ await bobIdle;
122
+ await chrisIdle;
123
+
124
+ const bobJoined = testUtils.waitForStateChange(bob.meeting, 'JOINED');
125
+ const chrisJoined = testUtils.waitForStateChange(chris.meeting, 'JOINED');
126
+ const bobJoin = bob.meeting.join({enableMultistream: true});
127
+ const chrisJoin = chris.meeting.join({enableMultistream: true});
128
+
129
+ await bobJoin;
130
+ await chrisJoin;
131
+ await bobJoined;
132
+ await chrisJoined;
133
+
134
+ assert.exists(bob.meeting.joinedWith);
135
+ assert.exists(chris.meeting.joinedWith);
136
+ });
137
+
138
+ it('users "alice", "bob", and "chris" add media', async () => {
139
+ mediaReadyListener = testUtils.waitForEvents([
140
+ {scope: alice.meeting, event: 'media:negotiated'},
141
+ {scope: bob.meeting, event: 'media:negotiated'},
142
+ {scope: chris.meeting, event: 'media:negotiated'},
143
+ ]);
144
+
145
+ const addMediaAlice = testUtils.addMedia(alice, {expectedMediaReadyTypes: ['local']});
146
+ const addMediaBob = testUtils.addMedia(bob, {expectedMediaReadyTypes: ['local']});
147
+ const addMediaChris = testUtils.addMedia(chris, {expectedMediaReadyTypes: ['local']});
148
+
149
+ await addMediaAlice;
150
+ await addMediaBob;
151
+ await addMediaChris;
152
+
153
+ assert.isTrue(alice.meeting.mediaProperties.mediaDirection.sendAudio);
154
+ assert.isTrue(alice.meeting.mediaProperties.mediaDirection.sendVideo);
155
+ assert.isTrue(alice.meeting.mediaProperties.mediaDirection.receiveAudio);
156
+ assert.isTrue(alice.meeting.mediaProperties.mediaDirection.receiveVideo);
157
+ assert.isTrue(bob.meeting.mediaProperties.mediaDirection.sendAudio);
158
+ assert.isTrue(bob.meeting.mediaProperties.mediaDirection.sendVideo);
159
+ assert.isTrue(bob.meeting.mediaProperties.mediaDirection.receiveAudio);
160
+ assert.isTrue(bob.meeting.mediaProperties.mediaDirection.receiveVideo);
161
+ assert.isTrue(chris.meeting.mediaProperties.mediaDirection.sendAudio);
162
+ assert.isTrue(chris.meeting.mediaProperties.mediaDirection.sendVideo);
163
+ assert.isTrue(chris.meeting.mediaProperties.mediaDirection.receiveAudio);
164
+ assert.isTrue(chris.meeting.mediaProperties.mediaDirection.receiveVideo);
165
+ });
166
+
167
+ it(`users "alice", "bob", and "chris" should be using the "${MEDIA_SERVERS.HOMER}" media server`, async () => {
168
+ await mediaReadyListener;
169
+
170
+ assert.equal(alice.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
171
+ assert.equal(bob.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
172
+ assert.equal(chris.meeting.mediaProperties.webrtcMediaConnection.mediaServer, MEDIA_SERVERS.HOMER);
173
+ });
174
+ });
175
+ }
176
+ });
@@ -20,6 +20,9 @@ describe('plugin-meetings', () => {
20
20
  webex.internal.llm.on = sinon.stub();
21
21
  webex.internal.mercury.on = sinon.stub();
22
22
  breakouts = new Breakouts({}, {parent: webex});
23
+ breakouts.locusUrl = 'locusUrl';
24
+ breakouts.breakoutServiceUrl = 'breakoutServiceUrl';
25
+ breakouts.url = 'url';
23
26
  webex.request = sinon.stub().returns(Promise.resolve('REQUEST_RETURN_VALUE'));
24
27
  });
25
28
 
@@ -289,5 +292,78 @@ describe('plugin-meetings', () => {
289
292
  assert.equal(breakouts.isInMainSession, true);
290
293
  });
291
294
  });
295
+
296
+ describe('#breakoutServiceUrlUpdate', () => {
297
+ it('sets the breakoutService url', () => {
298
+ breakouts.breakoutServiceUrlUpdate('newBreakoutServiceUrl');
299
+ assert.equal(breakouts.breakoutServiceUrl, 'newBreakoutServiceUrl/breakout/');
300
+ });
301
+ });
302
+
303
+ describe('#toggleBreakout', () => {
304
+ it('enableBreakoutSession is undefined, run enableBreakouts then toggleBreakout', async() => {
305
+ breakouts.enableBreakoutSession = undefined;
306
+ breakouts.enableBreakouts = sinon.stub().resolves(({body: {
307
+ sessionId: 'sessionId',
308
+ groupId: 'groupId',
309
+ name: 'name',
310
+ current: true,
311
+ sessionType: 'sessionType',
312
+ url: 'url'
313
+ }}))
314
+ breakouts.updateBreakout = sinon.stub().resolves();
315
+ breakouts.doToggleBreakout = sinon.stub().resolves();
316
+
317
+ await breakouts.toggleBreakout(false);
318
+ assert.calledOnceWithExactly(breakouts.enableBreakouts);
319
+ assert.calledOnceWithExactly(breakouts.updateBreakout, {
320
+ sessionId: 'sessionId',
321
+ groupId: 'groupId',
322
+ name: 'name',
323
+ current: true,
324
+ sessionType: 'sessionType',
325
+ url: 'url'
326
+ });
327
+ assert.calledOnceWithExactly(breakouts.doToggleBreakout, false);
328
+ });
329
+
330
+ it('enableBreakoutSession is exist, run toggleBreakout', async() => {
331
+ breakouts.enableBreakoutSession = true;
332
+ breakouts.doToggleBreakout = sinon.stub().resolves();
333
+ await breakouts.toggleBreakout(true);
334
+ assert.calledOnceWithExactly(breakouts.doToggleBreakout, true);
335
+ });
336
+ });
337
+
338
+ describe('enableBreakouts', () => {
339
+ it('makes the request as expected', async () => {
340
+ const result = await breakouts.enableBreakouts();
341
+ breakouts.set('breakoutServiceUrl', 'breakoutServiceUrl');
342
+ assert.calledOnceWithExactly(webex.request, {
343
+ method: 'POST',
344
+ uri: 'breakoutServiceUrl',
345
+ body: {
346
+ locusUrl: 'locusUrl'
347
+ }
348
+ });
349
+
350
+ assert.equal(result, 'REQUEST_RETURN_VALUE');
351
+ });
352
+ });
353
+
354
+ describe('doToggleBreakout', () => {
355
+ it('makes the request as expected', async () => {
356
+ const result = await breakouts.doToggleBreakout(true);
357
+ assert.calledOnceWithExactly(webex.request, {
358
+ method: 'PUT',
359
+ uri: 'url',
360
+ body: {
361
+ enableBreakoutSession: true
362
+ }
363
+ });
364
+
365
+ assert.equal(result, 'REQUEST_RETURN_VALUE');
366
+ });
367
+ });
292
368
  });
293
369
  });
@@ -101,4 +101,40 @@ describe('ReceiveSlot', () => {
101
101
  assert.strictEqual(receiveSlot.csi, undefined);
102
102
  assert.strictEqual(receiveSlot.sourceState, 'no source');
103
103
  });
104
+
105
+ describe('findMemberId()', () => {
106
+ it('doesn\'t do anything if csi is not set', () => {
107
+ // by default the receiveSlot does not have any csi or member id
108
+ receiveSlot.findMemberId();
109
+
110
+ assert.notCalled(findMemberIdCallbackStub);
111
+ });
112
+
113
+ it('finds a member id if member id is undefined and CSI is known', () => {
114
+ // setup receiveSlot to have a csi without a member id
115
+ const csi = 12345;
116
+ fakeWcmeSlot.emit(WcmeReceiveSlotEvents.SourceUpdate, 'live', csi);
117
+ findMemberIdCallbackStub.reset();
118
+
119
+ receiveSlot.findMemberId();
120
+
121
+ assert.calledOnce(findMemberIdCallbackStub);
122
+ assert.calledWith(findMemberIdCallbackStub, csi);
123
+ });
124
+
125
+ it('doesn\'t do anything if member id already set', () => {
126
+ // setup receiveSlot to have a csi and a member id
127
+ const csi = 12345;
128
+ const memberId = '12345678-1234-5678-9012-345678901234';
129
+
130
+ findMemberIdCallbackStub.returns(memberId);
131
+
132
+ fakeWcmeSlot.emit(WcmeReceiveSlotEvents.SourceUpdate, 'live', csi);
133
+ findMemberIdCallbackStub.reset();
134
+
135
+ receiveSlot.findMemberId();
136
+
137
+ assert.notCalled(findMemberIdCallbackStub);
138
+ });
139
+ });
104
140
  });
@@ -1,6 +1,7 @@
1
1
  import sinon from 'sinon';
2
2
  import {assert} from '@webex/test-helper-chai';
3
3
  import {MediaType} from '@webex/internal-media-core';
4
+ import {ReceiveSlot} from '@webex/plugin-meetings/src/multistream/receiveSlot';
4
5
  import {ReceiveSlotManager} from '@webex/plugin-meetings/src/multistream/receiveSlotManager';
5
6
  import * as ReceiveSlotModule from '@webex/plugin-meetings/src/multistream/receiveSlot';
6
7
 
@@ -30,6 +31,7 @@ describe('ReceiveSlotManager', () => {
30
31
  const fakeReceiveSlot = {
31
32
  id: `fake sdk receive slot ${fakeReceiveSlots.length + 1}`,
32
33
  mediaType,
34
+ findMemberId: sinon.stub(),
33
35
  };
34
36
 
35
37
  fakeReceiveSlots.push(fakeReceiveSlot);
@@ -170,4 +172,30 @@ describe('ReceiveSlotManager', () => {
170
172
  numFreeSlots: {'VIDEO-MAIN': 1},
171
173
  });
172
174
  });
175
+
176
+ describe('updateMemberIds', () => {
177
+
178
+ it('calls findMemberId() on all allocated receive slots', async () => {
179
+ const audioSlots: ReceiveSlot[] = [];
180
+ const videoSlots: ReceiveSlot[] = [];
181
+
182
+ // allocate a bunch of receive slots
183
+ audioSlots.push(await receiveSlotManager.allocateSlot(MediaType.AudioMain));
184
+ audioSlots.push(await receiveSlotManager.allocateSlot(MediaType.AudioMain));
185
+ videoSlots.push(await receiveSlotManager.allocateSlot(MediaType.VideoMain));
186
+ videoSlots.push(await receiveSlotManager.allocateSlot(MediaType.VideoMain));
187
+ videoSlots.push(await receiveSlotManager.allocateSlot(MediaType.VideoMain));
188
+
189
+ receiveSlotManager.updateMemberIds();
190
+
191
+ assert.strictEqual(audioSlots.length, 2);
192
+ assert.strictEqual(videoSlots.length, 3);
193
+
194
+ assert.strictEqual(fakeReceiveSlots.length, audioSlots.length + videoSlots.length);
195
+
196
+ fakeReceiveSlots.forEach(slot => {
197
+ assert.calledOnce(slot.findMemberId);
198
+ });
199
+ });
200
+ });
173
201
  });
@@ -0,0 +1,9 @@
1
+ // MOVE TO TEST CONSTANTS
2
+ export const MEDIA_SERVERS = {
3
+ // The homer media server for converged multistream meetings.
4
+ HOMER: 'homer',
5
+ // The linus media server
6
+ LINUS: 'linus',
7
+ // The calliope media server for transcoded meetings
8
+ CALLIOPE: 'calliope',
9
+ };
@@ -195,12 +195,19 @@ const delayedTest = (callback, timeout) =>
195
195
  }, timeout);
196
196
  });
197
197
 
198
- const addMedia = (user) => {
199
- const mediaReadyPromises = {
200
- local: new Defer(),
201
- remoteAudio: new Defer(),
202
- remoteVideo: new Defer(),
203
- };
198
+ const addMedia = (user, options = {}) => {
199
+ const mediaReadyPromises = Array.isArray(options.expectedMediaReadyTypes)
200
+ ? options.expectedMediaReadyTypes.reduce((output, expectedMediaReadyType) => {
201
+ if (typeof expectedMediaReadyType !== 'string') {
202
+ return output;
203
+ }
204
+
205
+ output[expectedMediaReadyType] = new Defer();
206
+
207
+ return output;
208
+ }, {})
209
+ : {local: new Defer(), remoteAudio: new Defer(), remoteVideo: new Defer()};
210
+
204
211
  const mediaReady = (media) => {
205
212
  if (!media) {
206
213
  return;
@@ -57,6 +57,10 @@ Config.webex = {
57
57
  enabled: true,
58
58
  },
59
59
  enableRtx: true,
60
+ experimental: {
61
+ enableMediaNegotiatedEvent: true,
62
+ enableUnifiedMeetings: true,
63
+ },
60
64
  },
61
65
  people: {
62
66
  showAllTypes: true,
@@ -17,9 +17,11 @@ require('@webex/plugin-people');
17
17
  require('@webex/plugin-rooms');
18
18
  require('@webex/plugin-meetings');
19
19
 
20
- const generateTestUsers = (options) =>
21
- testUser
22
- .create({count: options.count})
20
+ const generateTestUsers = (options = {}) => {
21
+ options.config = options.config || {};
22
+ options.config.orgId = options.config.orgId || process.env.WEBEX_CONVERGED_ORG_ID;
23
+
24
+ return testUser.create(options)
23
25
  .then(async (userSet) => {
24
26
  if (userSet.length !== options.count) {
25
27
  return Promise.reject(new Error('Test users not created'));
@@ -52,6 +54,7 @@ const generateTestUsers = (options) =>
52
54
  .catch((error) => {
53
55
  console.error('#generateTestUsers=>ERROR', error);
54
56
  });
57
+ };
55
58
 
56
59
  const reserveCMR = (user) =>
57
60
  user.webex