@webex/plugin-meetings 3.8.1-next.4 → 3.8.1-next.41
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/README.md +26 -13
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/constants.js +24 -2
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +38 -84
- package/dist/locus-info/index.js.map +1 -1
- package/dist/media/index.js +2 -2
- package/dist/media/index.js.map +1 -1
- package/dist/meeting/brbState.js +14 -12
- package/dist/meeting/brbState.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +6 -0
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +213 -77
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +19 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/type.js +7 -0
- package/dist/meeting/type.js.map +1 -0
- package/dist/meeting/util.js +11 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +35 -33
- package/dist/meetings/index.js.map +1 -1
- package/dist/members/index.js +11 -9
- package/dist/members/index.js.map +1 -1
- package/dist/members/request.js +3 -3
- package/dist/members/request.js.map +1 -1
- package/dist/members/util.js +18 -6
- package/dist/members/util.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +1 -1
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMedia.js +34 -5
- package/dist/multistream/remoteMedia.js.map +1 -1
- package/dist/multistream/remoteMediaGroup.js +42 -2
- package/dist/multistream/remoteMediaGroup.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +32 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/reachability/index.js +5 -10
- package/dist/reachability/index.js.map +1 -1
- package/dist/types/constants.d.ts +22 -0
- package/dist/types/locus-info/index.d.ts +0 -9
- package/dist/types/meeting/brbState.d.ts +0 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
- package/dist/types/meeting/index.d.ts +35 -18
- package/dist/types/meeting/request.d.ts +9 -1
- package/dist/types/meeting/request.type.d.ts +74 -0
- package/dist/types/meeting/type.d.ts +9 -0
- package/dist/types/meeting/util.d.ts +3 -0
- package/dist/types/members/index.d.ts +10 -7
- package/dist/types/members/request.d.ts +1 -1
- package/dist/types/members/util.d.ts +7 -3
- package/dist/types/multistream/remoteMedia.d.ts +20 -1
- package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
- package/dist/types/multistream/sendSlotManager.d.ts +16 -0
- package/dist/types/reachability/index.d.ts +2 -2
- package/dist/webinar/index.js +1 -1
- package/package.json +24 -25
- package/src/constants.ts +23 -2
- package/src/locus-info/index.ts +47 -82
- package/src/media/index.ts +2 -2
- package/src/meeting/brbState.ts +9 -7
- package/src/meeting/in-meeting-actions.ts +13 -0
- package/src/meeting/index.ts +168 -42
- package/src/meeting/request.ts +16 -0
- package/src/meeting/request.type.ts +64 -0
- package/src/meeting/type.ts +9 -0
- package/src/meeting/util.ts +13 -0
- package/src/meetings/index.ts +3 -2
- package/src/members/index.ts +13 -10
- package/src/members/request.ts +2 -2
- package/src/members/util.ts +16 -4
- package/src/multistream/mediaRequestManager.ts +7 -7
- package/src/multistream/remoteMedia.ts +34 -4
- package/src/multistream/remoteMediaGroup.ts +37 -2
- package/src/multistream/sendSlotManager.ts +34 -2
- package/src/reachability/index.ts +5 -13
- package/test/unit/spec/locus-info/index.js +177 -83
- package/test/unit/spec/media/index.ts +107 -0
- package/test/unit/spec/meeting/brbState.ts +9 -9
- package/test/unit/spec/meeting/in-meeting-actions.ts +6 -0
- package/test/unit/spec/meeting/index.js +694 -60
- package/test/unit/spec/meeting/request.js +71 -0
- package/test/unit/spec/meeting/utils.js +21 -0
- package/test/unit/spec/meetings/index.js +2 -0
- package/test/unit/spec/members/index.js +68 -9
- package/test/unit/spec/members/request.js +2 -2
- package/test/unit/spec/members/utils.js +27 -7
- package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
- package/test/unit/spec/multistream/remoteMedia.ts +66 -2
- package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
- package/test/unit/spec/reachability/index.ts +2 -6
@@ -15,7 +15,7 @@ import {cloneDeepWith, debounce, isEmpty} from 'lodash';
|
|
15
15
|
import LoggerProxy from '../common/logs/logger-proxy';
|
16
16
|
|
17
17
|
import {ReceiveSlot, ReceiveSlotEvents} from './receiveSlot';
|
18
|
-
import {
|
18
|
+
import {MAX_FS_VALUES} from './remoteMedia';
|
19
19
|
|
20
20
|
export interface ActiveSpeakerPolicyInfo {
|
21
21
|
policy: 'active-speaker';
|
@@ -123,12 +123,12 @@ export class MediaRequestManager {
|
|
123
123
|
|
124
124
|
private getDegradedClientRequests(clientRequests: ClientRequestsMap) {
|
125
125
|
const maxFsLimits = [
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
126
|
+
MAX_FS_VALUES['1080p'],
|
127
|
+
MAX_FS_VALUES['720p'],
|
128
|
+
MAX_FS_VALUES['540p'],
|
129
|
+
MAX_FS_VALUES['360p'],
|
130
|
+
MAX_FS_VALUES['180p'],
|
131
|
+
MAX_FS_VALUES['90p'],
|
132
132
|
];
|
133
133
|
|
134
134
|
// reduce max-fs until total macroblocks is below limit
|
@@ -19,17 +19,18 @@ export type RemoteVideoResolution =
|
|
19
19
|
| 'large' // 1080p or less
|
20
20
|
| 'best'; // highest possible resolution
|
21
21
|
|
22
|
-
const MAX_FS_VALUES = {
|
22
|
+
export const MAX_FS_VALUES = {
|
23
23
|
'90p': 60,
|
24
24
|
'180p': 240,
|
25
25
|
'360p': 920,
|
26
|
+
'540p': 2040,
|
26
27
|
'720p': 3600,
|
27
28
|
'1080p': 8192,
|
28
29
|
};
|
29
30
|
|
30
31
|
/**
|
31
32
|
* Converts pane size into h264 maxFs
|
32
|
-
* @param {
|
33
|
+
* @param {RemoteVideoResolution} paneSize
|
33
34
|
* @returns {number}
|
34
35
|
*/
|
35
36
|
export function getMaxFs(paneSize: RemoteVideoResolution): number {
|
@@ -89,6 +90,13 @@ export class RemoteMedia extends EventsScope {
|
|
89
90
|
|
90
91
|
public readonly id: RemoteMediaId;
|
91
92
|
|
93
|
+
/**
|
94
|
+
* The max frame size of the media request, used for logging and media requests.
|
95
|
+
* Set by setSizeHint() based on video element dimensions.
|
96
|
+
* When > 0, this value takes precedence over options.resolution in sendMediaRequest().
|
97
|
+
*/
|
98
|
+
private maxFrameSize = 0;
|
99
|
+
|
92
100
|
/**
|
93
101
|
* Constructs RemoteMedia instance
|
94
102
|
*
|
@@ -136,15 +144,34 @@ export class RemoteMedia extends EventsScope {
|
|
136
144
|
fs = MAX_FS_VALUES['180p'];
|
137
145
|
} else if (height < getThresholdHeight(360)) {
|
138
146
|
fs = MAX_FS_VALUES['360p'];
|
147
|
+
} else if (height < getThresholdHeight(540)) {
|
148
|
+
fs = MAX_FS_VALUES['540p'];
|
139
149
|
} else if (height <= 720) {
|
140
150
|
fs = MAX_FS_VALUES['720p'];
|
141
151
|
} else {
|
142
152
|
fs = MAX_FS_VALUES['1080p'];
|
143
153
|
}
|
144
154
|
|
155
|
+
this.maxFrameSize = fs;
|
145
156
|
this.receiveSlot?.setMaxFs(fs);
|
146
157
|
}
|
147
158
|
|
159
|
+
/**
|
160
|
+
* Get the current effective maxFs value that would be used in media requests
|
161
|
+
* @returns {number | undefined} The maxFs value, or undefined if no constraints
|
162
|
+
*/
|
163
|
+
public getEffectiveMaxFs(): number | undefined {
|
164
|
+
if (this.maxFrameSize > 0) {
|
165
|
+
return this.maxFrameSize;
|
166
|
+
}
|
167
|
+
|
168
|
+
if (this.options.resolution) {
|
169
|
+
return getMaxFs(this.options.resolution);
|
170
|
+
}
|
171
|
+
|
172
|
+
return undefined;
|
173
|
+
}
|
174
|
+
|
148
175
|
/**
|
149
176
|
* Invalidates the remote media by clearing the reference to a receive slot and
|
150
177
|
* cancelling the media request.
|
@@ -185,6 +212,9 @@ export class RemoteMedia extends EventsScope {
|
|
185
212
|
throw new Error('sendMediaRequest() called on an invalidated RemoteMedia instance');
|
186
213
|
}
|
187
214
|
|
215
|
+
// Use maxFrameSize from setSizeHint if available, otherwise fallback to options.resolution
|
216
|
+
const maxFs = this.getEffectiveMaxFs();
|
217
|
+
|
188
218
|
this.mediaRequestId = this.mediaRequestManager.addRequest(
|
189
219
|
{
|
190
220
|
policyInfo: {
|
@@ -192,9 +222,9 @@ export class RemoteMedia extends EventsScope {
|
|
192
222
|
csi,
|
193
223
|
},
|
194
224
|
receiveSlots: [this.receiveSlot],
|
195
|
-
codecInfo:
|
225
|
+
codecInfo: maxFs && {
|
196
226
|
codec: 'h264',
|
197
|
-
maxFs
|
227
|
+
maxFs,
|
198
228
|
},
|
199
229
|
},
|
200
230
|
commit
|
@@ -215,6 +215,9 @@ export class RemoteMediaGroup {
|
|
215
215
|
private sendActiveSpeakerMediaRequest(commit: boolean) {
|
216
216
|
this.cancelActiveSpeakerMediaRequest(false);
|
217
217
|
|
218
|
+
// Calculate the effective maxFs based on all unpinned RemoteMedia instances
|
219
|
+
const effectiveMaxFs = this.getEffectiveMaxFsForActiveSpeaker();
|
220
|
+
|
218
221
|
this.mediaRequestId = this.mediaRequestManager.addRequest(
|
219
222
|
{
|
220
223
|
policyInfo: {
|
@@ -230,9 +233,9 @@ export class RemoteMediaGroup {
|
|
230
233
|
receiveSlots: this.unpinnedRemoteMedia.map((remoteMedia) =>
|
231
234
|
remoteMedia.getUnderlyingReceiveSlot()
|
232
235
|
) as ReceiveSlot[],
|
233
|
-
codecInfo:
|
236
|
+
codecInfo: effectiveMaxFs && {
|
234
237
|
codec: 'h264',
|
235
|
-
maxFs:
|
238
|
+
maxFs: effectiveMaxFs,
|
236
239
|
},
|
237
240
|
},
|
238
241
|
commit
|
@@ -300,4 +303,36 @@ export class RemoteMediaGroup {
|
|
300
303
|
this.unpinnedRemoteMedia.includes(remoteMedia) || this.pinnedRemoteMedia.includes(remoteMedia)
|
301
304
|
);
|
302
305
|
}
|
306
|
+
|
307
|
+
/**
|
308
|
+
* Calculate the effective maxFs for the active speaker media request based on unpinned RemoteMedia instances
|
309
|
+
* @returns {number | undefined} The calculated maxFs value, or undefined if no constraints
|
310
|
+
* @private
|
311
|
+
*/
|
312
|
+
private getEffectiveMaxFsForActiveSpeaker(): number | undefined {
|
313
|
+
// Get all effective maxFs values from unpinned RemoteMedia instances
|
314
|
+
const maxFsValues = this.unpinnedRemoteMedia
|
315
|
+
.map((remoteMedia) => remoteMedia.getEffectiveMaxFs())
|
316
|
+
.filter((maxFs) => maxFs !== undefined);
|
317
|
+
|
318
|
+
// Use the highest maxFs value to ensure we don't under-request resolution for any instance
|
319
|
+
if (maxFsValues.length > 0) {
|
320
|
+
return Math.max(...maxFsValues);
|
321
|
+
}
|
322
|
+
|
323
|
+
// Fall back to group's resolution option
|
324
|
+
if (this.options.resolution) {
|
325
|
+
return getMaxFs(this.options.resolution);
|
326
|
+
}
|
327
|
+
|
328
|
+
return undefined;
|
329
|
+
}
|
330
|
+
|
331
|
+
/**
|
332
|
+
* Get the current effective maxFs that would be used for the active speaker media request
|
333
|
+
* @returns {number | undefined} The effective maxFs value
|
334
|
+
*/
|
335
|
+
public getEffectiveMaxFs(): number | undefined {
|
336
|
+
return this.getEffectiveMaxFsForActiveSpeaker();
|
337
|
+
}
|
303
338
|
}
|
@@ -7,10 +7,20 @@ import {
|
|
7
7
|
StreamState,
|
8
8
|
} from '@webex/internal-media-core';
|
9
9
|
|
10
|
+
/**
|
11
|
+
* This class is used to manage the sendSlots for the given media types.
|
12
|
+
*/
|
10
13
|
export default class SendSlotManager {
|
11
14
|
private readonly slots: Map<MediaType, SendSlot> = new Map();
|
12
15
|
private readonly LoggerProxy: any;
|
16
|
+
private readonly sourceStateOverrides: Map<MediaType, StreamState> = new Map();
|
13
17
|
|
18
|
+
/**
|
19
|
+
* Constructor for SendSlotManager
|
20
|
+
*
|
21
|
+
* @param {any} LoggerProxy is used to log the messages
|
22
|
+
* @constructor
|
23
|
+
*/
|
14
24
|
constructor(LoggerProxy: any) {
|
15
25
|
this.LoggerProxy = LoggerProxy;
|
16
26
|
}
|
@@ -93,7 +103,7 @@ export default class SendSlotManager {
|
|
93
103
|
public setSourceStateOverride(mediaType: MediaType, state: StreamState | null) {
|
94
104
|
if (mediaType !== MediaType.VideoMain) {
|
95
105
|
throw new Error(
|
96
|
-
`
|
106
|
+
`Invalid media type '${mediaType}'. Source state overrides are only applicable to ${MediaType.VideoMain}.`
|
97
107
|
);
|
98
108
|
}
|
99
109
|
|
@@ -103,17 +113,39 @@ export default class SendSlotManager {
|
|
103
113
|
throw new Error(`Slot for ${mediaType} does not exist`);
|
104
114
|
}
|
105
115
|
|
116
|
+
const currentStateOverride = this.getSourceStateOverride(mediaType);
|
117
|
+
if (currentStateOverride === state) {
|
118
|
+
return;
|
119
|
+
}
|
120
|
+
|
106
121
|
if (state) {
|
107
122
|
slot.setSourceStateOverride(state);
|
123
|
+
this.sourceStateOverrides.set(mediaType, state);
|
108
124
|
} else {
|
109
125
|
slot.clearSourceStateOverride();
|
126
|
+
this.sourceStateOverrides.delete(mediaType);
|
110
127
|
}
|
111
128
|
|
112
129
|
this.LoggerProxy.logger.info(
|
113
|
-
`
|
130
|
+
`SendSlotManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
|
114
131
|
);
|
115
132
|
}
|
116
133
|
|
134
|
+
/**
|
135
|
+
* Gets the source state override for the given media type.
|
136
|
+
* @param {MediaType} mediaType - The type of media to get the source state override for.
|
137
|
+
* @returns {StreamState | null} - The current source state override or null if not set.
|
138
|
+
*/
|
139
|
+
private getSourceStateOverride(mediaType: MediaType): StreamState | null {
|
140
|
+
if (mediaType !== MediaType.VideoMain) {
|
141
|
+
throw new Error(
|
142
|
+
`Invalid media type '${mediaType}'. Source state overrides are only applicable to ${MediaType.VideoMain}.`
|
143
|
+
);
|
144
|
+
}
|
145
|
+
|
146
|
+
return this.sourceStateOverrides.get(mediaType) || null;
|
147
|
+
}
|
148
|
+
|
117
149
|
/**
|
118
150
|
* This method publishes the given stream to the sendSlot for the given mediaType
|
119
151
|
* @param {MediaType} mediaType MediaType of the sendSlot to which a stream needs to be published (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
|
@@ -140,22 +140,14 @@ export default class Reachability extends EventsScope {
|
|
140
140
|
|
141
141
|
/**
|
142
142
|
* Checks if the given subnet is reachable
|
143
|
-
* @param {string}
|
143
|
+
* @param {string} selectedSubnetFirstOctet - selected subnet first octet, e.g. "10" for "10.X.X.X"
|
144
144
|
* @returns {boolean | null} true if reachable, false if not reachable, null if mediaServerIp is not provided
|
145
145
|
* @public
|
146
146
|
* @memberof Reachability
|
147
147
|
*/
|
148
|
-
public isSubnetReachable(
|
149
|
-
if (!mediaServerIp) {
|
150
|
-
LoggerProxy.logger.error(`Reachability:index#isSubnetReachable --> mediaServerIp is null`);
|
151
|
-
|
152
|
-
return null;
|
153
|
-
}
|
154
|
-
|
155
|
-
const subnetFirstOctet = mediaServerIp.split('.')[0];
|
156
|
-
|
148
|
+
public isSubnetReachable(selectedSubnetFirstOctet: string): boolean | null {
|
157
149
|
LoggerProxy.logger.info(
|
158
|
-
`Reachability:index#isSubnetReachable --> Looking for subnet: ${
|
150
|
+
`Reachability:index#isSubnetReachable --> Looking for subnet: ${selectedSubnetFirstOctet}.X.X.X`
|
159
151
|
);
|
160
152
|
|
161
153
|
const matchingReachedClusters = Object.values(this.clusterReachability).reduce(
|
@@ -167,7 +159,7 @@ export default class Reachability extends EventsScope {
|
|
167
159
|
const subnet = reachedSubnetsArray[i];
|
168
160
|
const reachedSubnetFirstOctet = subnet.split('.')[0];
|
169
161
|
|
170
|
-
if (
|
162
|
+
if (selectedSubnetFirstOctet === reachedSubnetFirstOctet) {
|
171
163
|
acc.add(cluster.name);
|
172
164
|
}
|
173
165
|
|
@@ -186,7 +178,7 @@ export default class Reachability extends EventsScope {
|
|
186
178
|
);
|
187
179
|
|
188
180
|
LoggerProxy.logger.info(
|
189
|
-
`Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${
|
181
|
+
`Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${selectedSubnetFirstOctet}.X.X.X`
|
190
182
|
);
|
191
183
|
|
192
184
|
return matchingReachedClusters.size > 0;
|
@@ -305,7 +305,7 @@ describe('plugin-meetings', () => {
|
|
305
305
|
{state: newControls.rdcControl}
|
306
306
|
);
|
307
307
|
});
|
308
|
-
|
308
|
+
|
309
309
|
it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
|
310
310
|
locusInfo.controls = {};
|
311
311
|
locusInfo.emitScoped = sinon.stub();
|
@@ -834,39 +834,6 @@ describe('plugin-meetings', () => {
|
|
834
834
|
);
|
835
835
|
});
|
836
836
|
|
837
|
-
it('should update the deltaParticipants object', () => {
|
838
|
-
const prev = locusInfo.deltaParticipants;
|
839
|
-
|
840
|
-
locusInfo.updateParticipantDeltas(newParticipants);
|
841
|
-
|
842
|
-
assert.notEqual(locusInfo.deltaParticipants, prev);
|
843
|
-
});
|
844
|
-
|
845
|
-
it('should update the delta property on all changed states', () => {
|
846
|
-
locusInfo.updateParticipantDeltas(newParticipants);
|
847
|
-
|
848
|
-
const [exampleParticipant] = locusInfo.deltaParticipants;
|
849
|
-
|
850
|
-
assert.isTrue(exampleParticipant.delta.audioStatus);
|
851
|
-
assert.isTrue(exampleParticipant.delta.videoSlidesStatus);
|
852
|
-
assert.isTrue(exampleParticipant.delta.videoStatus);
|
853
|
-
});
|
854
|
-
|
855
|
-
it('should include the person details of the changed participant', () => {
|
856
|
-
locusInfo.updateParticipantDeltas(newParticipants);
|
857
|
-
|
858
|
-
const [exampleParticipant] = locusInfo.deltaParticipants;
|
859
|
-
|
860
|
-
assert.equal(exampleParticipant.person, newParticipants[0].person);
|
861
|
-
});
|
862
|
-
|
863
|
-
it('should clear deltaParticipants when no changes occured', () => {
|
864
|
-
locusInfo.participants = [...newParticipants];
|
865
|
-
|
866
|
-
locusInfo.updateParticipantDeltas(locusInfo.participants);
|
867
|
-
|
868
|
-
assert.isTrue(locusInfo.deltaParticipants.length === 0);
|
869
|
-
});
|
870
837
|
|
871
838
|
it('should call with participant display name', () => {
|
872
839
|
const failureParticipant = [
|
@@ -2108,6 +2075,38 @@ describe('plugin-meetings', () => {
|
|
2108
2075
|
assert.isFunction(locusParser.onDeltaAction);
|
2109
2076
|
});
|
2110
2077
|
|
2078
|
+
it("#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo", () => {
|
2079
|
+
const callOrder = [];
|
2080
|
+
sinon.stub(locusInfo, "updateControls");
|
2081
|
+
sinon.stub(locusInfo, "updateConversationUrl");
|
2082
|
+
sinon.stub(locusInfo, "updateCreated");
|
2083
|
+
sinon.stub(locusInfo, "updateFullState");
|
2084
|
+
sinon.stub(locusInfo, "updateHostInfo");
|
2085
|
+
sinon.stub(locusInfo, "updateMeetingInfo").callsFake(() => {
|
2086
|
+
callOrder.push("updateMeetingInfo");
|
2087
|
+
});
|
2088
|
+
sinon.stub(locusInfo, "updateMediaShares");
|
2089
|
+
sinon.stub(locusInfo, "updateParticipantsUrl");
|
2090
|
+
sinon.stub(locusInfo, "updateReplace");
|
2091
|
+
sinon.stub(locusInfo, "updateSelf");
|
2092
|
+
sinon.stub(locusInfo, "updateLocusUrl").callsFake(() => {
|
2093
|
+
callOrder.push("updateLocusUrl");
|
2094
|
+
});
|
2095
|
+
sinon.stub(locusInfo, "updateAclUrl");
|
2096
|
+
sinon.stub(locusInfo, "updateBasequence");
|
2097
|
+
sinon.stub(locusInfo, "updateSequence");
|
2098
|
+
sinon.stub(locusInfo, "updateMemberShip");
|
2099
|
+
sinon.stub(locusInfo, "updateIdentifiers");
|
2100
|
+
sinon.stub(locusInfo, "updateEmbeddedApps");
|
2101
|
+
sinon.stub(locusInfo, "updateResources");
|
2102
|
+
sinon.stub(locusInfo, "compareAndUpdate");
|
2103
|
+
|
2104
|
+
locusInfo.updateLocusInfo(locus);
|
2105
|
+
|
2106
|
+
// Ensure updateLocusUrl is called before updateMeetingInfo if both are called
|
2107
|
+
assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
|
2108
|
+
});
|
2109
|
+
|
2111
2110
|
it('#updateLocusInfo ignores breakout LEFT message', () => {
|
2112
2111
|
const newLocus = {
|
2113
2112
|
self: {
|
@@ -2159,10 +2158,11 @@ describe('plugin-meetings', () => {
|
|
2159
2158
|
assert.notCalled(locusInfo.compareAndUpdate);
|
2160
2159
|
});
|
2161
2160
|
|
2161
|
+
|
2162
|
+
|
2162
2163
|
it('onFullLocus() updates the working-copy of locus parser', () => {
|
2163
2164
|
const eventType = 'fakeEvent';
|
2164
2165
|
|
2165
|
-
sandbox.stub(locusInfo, 'updateParticipantDeltas');
|
2166
2166
|
sandbox.stub(locusInfo, 'updateLocusInfo');
|
2167
2167
|
sandbox.stub(locusInfo, 'updateParticipants');
|
2168
2168
|
sandbox.stub(locusInfo, 'isMeetingActive');
|
@@ -2182,7 +2182,6 @@ describe('plugin-meetings', () => {
|
|
2182
2182
|
const oldWorkingCopy = locusParser.workingCopy;
|
2183
2183
|
|
2184
2184
|
const spies = [
|
2185
|
-
sandbox.stub(locusInfo, 'updateParticipantDeltas'),
|
2186
2185
|
sandbox.stub(locusInfo, 'updateLocusInfo'),
|
2187
2186
|
sandbox.stub(locusInfo, 'updateParticipants'),
|
2188
2187
|
sandbox.stub(locusInfo, 'isMeetingActive'),
|
@@ -2257,7 +2256,7 @@ describe('plugin-meetings', () => {
|
|
2257
2256
|
|
2258
2257
|
it('applyLocusDeltaData gets delta locus on DESYNC action if we have a syncUrl', () => {
|
2259
2258
|
const {DESYNC} = LocusDeltaParser.loci;
|
2260
|
-
const fakeDeltaLocus = {id: 'fake delta locus'};
|
2259
|
+
const fakeDeltaLocus = {baseSequence: {}, id: 'fake delta locus'};
|
2261
2260
|
const meeting = {
|
2262
2261
|
meetingRequest: {
|
2263
2262
|
getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
|
@@ -2392,25 +2391,22 @@ describe('plugin-meetings', () => {
|
|
2392
2391
|
};
|
2393
2392
|
});
|
2394
2393
|
|
2395
|
-
it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', () => {
|
2394
|
+
it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', async () => {
|
2396
2395
|
meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
|
2397
2396
|
|
2398
2397
|
locusInfo.locusParser.workingCopy = {}; // no syncUrl
|
2399
2398
|
|
2400
|
-
|
2401
|
-
// we will wait and stub it's last function to resolve this waiting promise.
|
2402
|
-
return new Promise((resolve) => {
|
2403
|
-
webex.meetings.destroy.callsFake(() => resolve());
|
2404
|
-
locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2405
|
-
}).then(() => {
|
2406
|
-
assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
|
2399
|
+
locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2407
2400
|
|
2408
|
-
|
2409
|
-
assert.notCalled(meeting.locusInfo.onFullLocus);
|
2410
|
-
assert.notCalled(locusInfo.locusParser.resume);
|
2401
|
+
await testUtils.flushPromises();
|
2411
2402
|
|
2412
|
-
|
2413
|
-
|
2403
|
+
assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
|
2404
|
+
|
2405
|
+
assert.notCalled(meeting.locusInfo.handleLocusDelta);
|
2406
|
+
assert.notCalled(meeting.locusInfo.onFullLocus);
|
2407
|
+
assert.notCalled(locusInfo.locusParser.resume);
|
2408
|
+
|
2409
|
+
assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
|
2414
2410
|
});
|
2415
2411
|
|
2416
2412
|
it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails, does a full locus sync', () => {
|
@@ -2447,39 +2443,62 @@ describe('plugin-meetings', () => {
|
|
2447
2443
|
});
|
2448
2444
|
});
|
2449
2445
|
|
2450
|
-
it('applyLocusDeltaData
|
2446
|
+
it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails with 403, it does not do a full locus sync', async () => {
|
2447
|
+
const fake403Error = new Error('fake error');
|
2448
|
+
fake403Error.statusCode = 403;
|
2449
|
+
|
2450
|
+
meeting.meetingRequest.getLocusDTO.onCall(0).rejects(fake403Error);
|
2451
|
+
|
2452
|
+
locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2453
|
+
|
2454
|
+
await testUtils.flushPromises();
|
2455
|
+
|
2456
|
+
assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'deltaSyncUrl'});
|
2457
|
+
|
2458
|
+
assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
|
2459
|
+
correlationId: meeting.correlationId,
|
2460
|
+
url: 'deltaSyncUrl',
|
2461
|
+
reason: 'fake error',
|
2462
|
+
errorName: 'Error',
|
2463
|
+
stack: sinon.match.any,
|
2464
|
+
code: sinon.match.any,
|
2465
|
+
});
|
2466
|
+
|
2467
|
+
assert.notCalled(meeting.locusInfo.handleLocusDelta);
|
2468
|
+
assert.notCalled(meeting.locusInfo.onFullLocus);
|
2469
|
+
assert.notCalled(locusInfo.locusParser.resume);
|
2470
|
+
});
|
2471
|
+
|
2472
|
+
it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', async () => {
|
2451
2473
|
meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
|
2452
2474
|
|
2453
|
-
|
2454
|
-
// we will wait and stub it's last function to resolve this waiting promise.
|
2455
|
-
return new Promise((resolve) => {
|
2456
|
-
webex.meetings.destroy.callsFake(() => resolve());
|
2457
|
-
locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2458
|
-
}).then(() => {
|
2459
|
-
assert.calledTwice(meeting.meetingRequest.getLocusDTO);
|
2475
|
+
locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2460
2476
|
|
2461
|
-
|
2462
|
-
{url: 'deltaSyncUrl'},
|
2463
|
-
]);
|
2464
|
-
assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
|
2465
|
-
{url: 'fullSyncUrl'},
|
2466
|
-
]);
|
2477
|
+
await testUtils.flushPromises();
|
2467
2478
|
|
2468
|
-
|
2469
|
-
correlationId: meeting.correlationId,
|
2470
|
-
url: 'deltaSyncUrl',
|
2471
|
-
reason: 'fake error',
|
2472
|
-
errorName: 'Error',
|
2473
|
-
stack: sinon.match.any,
|
2474
|
-
code: sinon.match.any,
|
2475
|
-
});
|
2479
|
+
assert.calledTwice(meeting.meetingRequest.getLocusDTO);
|
2476
2480
|
|
2477
|
-
|
2478
|
-
|
2479
|
-
|
2481
|
+
assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
|
2482
|
+
{url: 'deltaSyncUrl'},
|
2483
|
+
]);
|
2484
|
+
assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
|
2485
|
+
{url: 'fullSyncUrl'},
|
2486
|
+
]);
|
2480
2487
|
|
2481
|
-
|
2488
|
+
assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
|
2489
|
+
correlationId: meeting.correlationId,
|
2490
|
+
url: 'deltaSyncUrl',
|
2491
|
+
reason: 'fake error',
|
2492
|
+
errorName: 'Error',
|
2493
|
+
stack: sinon.match.any,
|
2494
|
+
code: sinon.match.any,
|
2482
2495
|
});
|
2496
|
+
|
2497
|
+
assert.notCalled(meeting.locusInfo.handleLocusDelta);
|
2498
|
+
assert.notCalled(meeting.locusInfo.onFullLocus);
|
2499
|
+
assert.notCalled(locusInfo.locusParser.resume);
|
2500
|
+
|
2501
|
+
assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
|
2483
2502
|
});
|
2484
2503
|
});
|
2485
2504
|
|
@@ -2509,9 +2528,7 @@ describe('plugin-meetings', () => {
|
|
2509
2528
|
});
|
2510
2529
|
|
2511
2530
|
it('onDeltaLocus merges delta participants with existing participants', () => {
|
2512
|
-
const FAKE_DELTA_PARTICIPANTS = [
|
2513
|
-
{id: '1111'}, {id: '2222'}
|
2514
|
-
]
|
2531
|
+
const FAKE_DELTA_PARTICIPANTS = [{id: '1111'}, {id: '2222'}];
|
2515
2532
|
fakeLocus.participants = FAKE_DELTA_PARTICIPANTS;
|
2516
2533
|
|
2517
2534
|
sinon.spy(locusInfo, 'mergeParticipants');
|
@@ -2519,9 +2536,87 @@ describe('plugin-meetings', () => {
|
|
2519
2536
|
const existingParticipants = locusInfo.participants;
|
2520
2537
|
|
2521
2538
|
locusInfo.onDeltaLocus(fakeLocus);
|
2522
|
-
assert.calledOnceWithExactly(
|
2539
|
+
assert.calledOnceWithExactly(
|
2540
|
+
locusInfo.mergeParticipants,
|
2541
|
+
existingParticipants,
|
2542
|
+
FAKE_DELTA_PARTICIPANTS
|
2543
|
+
);
|
2523
2544
|
assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
|
2524
2545
|
});
|
2546
|
+
|
2547
|
+
[true, false].forEach((isDelta) =>
|
2548
|
+
it(`applyLocusDeltaData - handles empty ${
|
2549
|
+
isDelta ? 'delta' : 'full'
|
2550
|
+
} DTO in response`, async () => {
|
2551
|
+
const {DESYNC} = LocusDeltaParser.loci;
|
2552
|
+
const fakeFullLocusDto = {};
|
2553
|
+
const meeting = {
|
2554
|
+
meetingRequest: {
|
2555
|
+
getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
|
2556
|
+
},
|
2557
|
+
locusInfo: {
|
2558
|
+
onFullLocus: sandbox.stub(),
|
2559
|
+
handleLocusDelta: sandbox.stub(),
|
2560
|
+
},
|
2561
|
+
locusUrl: 'fake locus FULL url',
|
2562
|
+
};
|
2563
|
+
|
2564
|
+
sinon.stub(locusInfo.locusParser, 'resume').resolves();
|
2565
|
+
|
2566
|
+
if (isDelta) {
|
2567
|
+
locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
|
2568
|
+
} else {
|
2569
|
+
locusInfo.locusParser.workingCopy = {}; // no syncUrl (to trigger FULL DTO request)
|
2570
|
+
}
|
2571
|
+
|
2572
|
+
await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2573
|
+
|
2574
|
+
await testUtils.flushPromises();
|
2575
|
+
|
2576
|
+
if (isDelta) {
|
2577
|
+
assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
|
2578
|
+
url: 'fake locus DELTA url',
|
2579
|
+
});
|
2580
|
+
} else {
|
2581
|
+
assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
|
2582
|
+
url: 'fake locus FULL url',
|
2583
|
+
});
|
2584
|
+
}
|
2585
|
+
assert.notCalled(meeting.locusInfo.handleLocusDelta);
|
2586
|
+
assert.notCalled(meeting.locusInfo.onFullLocus);
|
2587
|
+
assert.calledOnce(locusInfo.locusParser.resume);
|
2588
|
+
})
|
2589
|
+
);
|
2590
|
+
|
2591
|
+
it(`applyLocusDeltaData - handles the case when we get FULL DTO when we asked for DELTA DTO`, async () => {
|
2592
|
+
const {DESYNC} = LocusDeltaParser.loci;
|
2593
|
+
const fakeFullLocusDto = {someStuff: 'data'}; // non-empty DTO, without baseSequence
|
2594
|
+
const meeting = {
|
2595
|
+
meetingRequest: {
|
2596
|
+
getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
|
2597
|
+
},
|
2598
|
+
locusInfo: {
|
2599
|
+
onFullLocus: sandbox.stub(),
|
2600
|
+
handleLocusDelta: sandbox.stub(),
|
2601
|
+
},
|
2602
|
+
locusUrl: 'fake locus FULL url',
|
2603
|
+
};
|
2604
|
+
|
2605
|
+
sinon.stub(locusInfo.locusParser, 'resume').resolves();
|
2606
|
+
|
2607
|
+
locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
|
2608
|
+
|
2609
|
+
await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
|
2610
|
+
|
2611
|
+
await testUtils.flushPromises();
|
2612
|
+
|
2613
|
+
assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
|
2614
|
+
url: 'fake locus DELTA url',
|
2615
|
+
});
|
2616
|
+
assert.notCalled(meeting.locusInfo.handleLocusDelta);
|
2617
|
+
assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
|
2618
|
+
assert.calledOnce(locusInfo.locusParser.resume);
|
2619
|
+
});
|
2525
2620
|
});
|
2526
2621
|
|
2527
2622
|
describe('#updateLocusCache', () => {
|
@@ -2934,10 +3029,9 @@ describe('plugin-meetings', () => {
|
|
2934
3029
|
beforeEach(() => {
|
2935
3030
|
clock = sinon.useFakeTimers();
|
2936
3031
|
|
2937
|
-
sinon.stub(locusInfo, 'updateParticipantDeltas');
|
2938
3032
|
sinon.stub(locusInfo, 'updateParticipants');
|
2939
|
-
sinon.stub(locusInfo, 'isMeetingActive')
|
2940
|
-
sinon.stub(locusInfo, 'handleOneOnOneEvent')
|
3033
|
+
sinon.stub(locusInfo, 'isMeetingActive');
|
3034
|
+
sinon.stub(locusInfo, 'handleOneOnOneEvent');
|
2941
3035
|
(updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
|
2942
3036
|
syncRequestStub = sinon.stub().resolves({body: {}});
|
2943
3037
|
|