@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +92 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/meeting/index.js +2 -1
- package/dist/meeting/index.js.map +1 -1
- package/dist/meetings/util.js +17 -0
- package/dist/meetings/util.js.map +1 -1
- package/dist/members/index.js +1 -0
- package/dist/members/index.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/multistream/receiveSlotManager.js +15 -0
- package/dist/multistream/receiveSlotManager.js.map +1 -1
- package/dist/types/multistream/receiveSlot.d.ts +3 -1
- package/dist/types/multistream/receiveSlotManager.d.ts +5 -0
- package/package.json +18 -18
- package/src/breakouts/index.ts +73 -1
- package/src/meeting/index.ts +1 -0
- package/src/meetings/util.ts +24 -0
- package/src/members/index.ts +2 -0
- package/src/multistream/receiveSlot.ts +8 -1
- package/src/multistream/receiveSlotManager.ts +12 -0
- package/test/integration/spec/converged-space-meetings.js +176 -0
- package/test/unit/spec/breakouts/index.ts +76 -0
- package/test/unit/spec/multistream/receiveSlot.ts +36 -0
- package/test/unit/spec/multistream/receiveSlotManager.ts +28 -0
- package/test/utils/constants.js +9 -0
- package/test/utils/testUtils.js +13 -6
- package/test/utils/webex-config.js +4 -0
- package/test/utils/webex-test-users.js +6 -3
package/src/breakouts/index.ts
CHANGED
|
@@ -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;
|
package/src/meeting/index.ts
CHANGED
|
@@ -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
|
|
package/src/meetings/util.ts
CHANGED
|
@@ -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
|
|
package/src/members/index.ts
CHANGED
|
@@ -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
|
+
};
|
package/test/utils/testUtils.js
CHANGED
|
@@ -195,12 +195,19 @@ const delayedTest = (callback, timeout) =>
|
|
|
195
195
|
}, timeout);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
const addMedia = (user) => {
|
|
199
|
-
const mediaReadyPromises =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|